Public Access
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0016c8c889 | |||
| ccf68a1519 | |||
| 0a194828ee | |||
| a00cbf7d92 | |||
| 14ffe53158 | |||
| e20423f323 | |||
| 5e25c6e77b |
-12
@@ -1,12 +0,0 @@
|
|||||||
[submodule "templates/repos/Template-Client"]
|
|
||||||
path = templates/repos/Template-Client
|
|
||||||
url = https://git.mokoconsulting.tech/MokoConsulting/Template-Client.git
|
|
||||||
[submodule "templates/repos/Template-Generic"]
|
|
||||||
path = templates/repos/Template-Generic
|
|
||||||
url = https://git.mokoconsulting.tech/MokoConsulting/Template-Generic.git
|
|
||||||
[submodule "templates/repos/Template-Joomla"]
|
|
||||||
path = templates/repos/Template-Joomla
|
|
||||||
url = https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla.git
|
|
||||||
[submodule "templates/repos/Template-MCP"]
|
|
||||||
path = templates/repos/Template-MCP
|
|
||||||
url = https://git.mokoconsulting.tech/MokoConsulting/Template-MCP.git
|
|
||||||
@@ -44,7 +44,7 @@ composer check # Run all checks
|
|||||||
|
|
||||||
### CLI Framework
|
### CLI Framework
|
||||||
|
|
||||||
All CLI tools extend `MokoCli\CliFramework` (`lib/Enterprise/CliFramework.php`).
|
All CLI tools extend `MokoEnterprise\CliFramework` (`lib/Enterprise/CliFramework.php`).
|
||||||
Built-in flags: `--help`, `--verbose`, `--quiet`, `--dry-run`.
|
Built-in flags: `--help`, `--verbose`, `--quiet`, `--dry-run`.
|
||||||
After adding a CLI tool, register it in `bin/moko` COMMAND_MAP.
|
After adding a CLI tool, register it in `bin/moko` COMMAND_MAP.
|
||||||
|
|
||||||
@@ -73,4 +73,4 @@ PHPStan runs with `--memory-limit=512M`. CI enforces PHPCS errors; PHPStan is `c
|
|||||||
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
|
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
|
||||||
- **Wiki**: documentation lives in the Gitea wiki, not `docs/` files
|
- **Wiki**: documentation lives in the Gitea wiki, not `docs/` files
|
||||||
- **New CLI tools**: extend `CliFramework`, not `CLIApp` (legacy)
|
- **New CLI tools**: extend `CliFramework`, not `CLIApp` (legacy)
|
||||||
- **Standards**: [MokoCli](https://git.mokoconsulting.tech/MokoConsulting/mokoplatform/wiki/Home)
|
- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/mokoplatform/wiki/Home)
|
||||||
|
|||||||
@@ -57,13 +57,13 @@ jobs:
|
|||||||
- name: Determine target repos
|
- name: Determine target repos
|
||||||
id: repos
|
id: repos
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
API="${GITEA_URL}/api/v1"
|
API="${GITEA_URL}/api/v1"
|
||||||
|
|
||||||
# Platform/standards/infra repos to exclude
|
# Platform/standards/infra repos to exclude
|
||||||
EXCLUDE="gitea-org-config org-profile gitea-private .mokogitea-private mokoplatform MokoTesting"
|
EXCLUDE="gitea-org-config org-profile gitea-private .mokogitea-private mokoplatform MokoTesting"
|
||||||
EXCLUDE="$EXCLUDE MokoCli-Template-Client MokoCli-Template-Dolibarr MokoCli-Template-Generic MokoCli-Template-Joomla MokoDoliProjTemplate"
|
EXCLUDE="$EXCLUDE MokoStandards-Template-Client MokoStandards-Template-Dolibarr MokoStandards-Template-Generic MokoStandards-Template-Joomla MokoDoliProjTemplate"
|
||||||
|
|
||||||
if [ -n "${{ inputs.repos }}" ]; then
|
if [ -n "${{ inputs.repos }}" ]; then
|
||||||
# User-specified repos
|
# User-specified repos
|
||||||
@@ -105,7 +105,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Apply protection rules
|
- name: Apply protection rules
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
DRY_RUN: ${{ inputs.dry_run || 'false' }}
|
DRY_RUN: ${{ inputs.dry_run || 'false' }}
|
||||||
run: |
|
run: |
|
||||||
API="${GITEA_URL}/api/v1"
|
API="${GITEA_URL}/api/v1"
|
||||||
|
|||||||
@@ -84,8 +84,8 @@ jobs:
|
|||||||
echo "Running: php automation/bulk_sync.php ${{ steps.args.outputs.args }}"
|
echo "Running: php automation/bulk_sync.php ${{ steps.args.outputs.args }}"
|
||||||
php automation/bulk_sync.php ${{ steps.args.outputs.args }} 2>&1 | tee /tmp/bulk_sync.log
|
php automation/bulk_sync.php ${{ steps.args.outputs.args }} 2>&1 | tee /tmp/bulk_sync.log
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
GH_TOKEN: ${{ secrets.GH_PAT }}
|
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
GIT_PLATFORM: gitea
|
GIT_PLATFORM: gitea
|
||||||
GITEA_URL: https://git.mokoconsulting.tech
|
GITEA_URL: https://git.mokoconsulting.tech
|
||||||
GITEA_ORG: MokoConsulting
|
GITEA_ORG: MokoConsulting
|
||||||
@@ -112,7 +112,7 @@ jobs:
|
|||||||
bash automation/enforce_tags.sh || echo "Tag enforcement had errors (non-fatal)"
|
bash automation/enforce_tags.sh || echo "Tag enforcement had errors (non-fatal)"
|
||||||
fi
|
fi
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
GITEA_URL: https://git.mokoconsulting.tech
|
GITEA_URL: https://git.mokoconsulting.tech
|
||||||
GITEA_ORG: MokoConsulting
|
GITEA_ORG: MokoConsulting
|
||||||
|
|
||||||
|
|||||||
@@ -57,12 +57,12 @@ jobs:
|
|||||||
- name: Determine target repos
|
- name: Determine target repos
|
||||||
id: repos
|
id: repos
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
API="${GITEA_URL}/api/v1"
|
API="${GITEA_URL}/api/v1"
|
||||||
|
|
||||||
EXCLUDE="gitea-org-config org-profile gitea-private .mokogitea-private mokoplatform MokoTesting"
|
EXCLUDE="gitea-org-config org-profile gitea-private .mokogitea-private mokoplatform MokoTesting"
|
||||||
EXCLUDE="$EXCLUDE MokoCli-Template-Client MokoCli-Template-Dolibarr MokoCli-Template-Generic MokoCli-Template-Joomla MokoDoliProjTemplate"
|
EXCLUDE="$EXCLUDE MokoStandards-Template-Client MokoStandards-Template-Dolibarr MokoStandards-Template-Generic MokoStandards-Template-Joomla MokoDoliProjTemplate"
|
||||||
|
|
||||||
if [ -n "${{ inputs.repos }}" ]; then
|
if [ -n "${{ inputs.repos }}" ]; then
|
||||||
REPOS=$(echo "${{ inputs.repos }}" | tr ',' ' ')
|
REPOS=$(echo "${{ inputs.repos }}" | tr ',' ' ')
|
||||||
@@ -107,7 +107,7 @@ jobs:
|
|||||||
- name: Run Renovate
|
- name: Run Renovate
|
||||||
if: steps.repos.outputs.repo_list != ''
|
if: steps.repos.outputs.repo_list != ''
|
||||||
env:
|
env:
|
||||||
RENOVATE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
RENOVATE_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
RENOVATE_PLATFORM: gitea
|
RENOVATE_PLATFORM: gitea
|
||||||
RENOVATE_ENDPOINT: ${{ env.GITEA_URL }}/api/v1
|
RENOVATE_ENDPOINT: ${{ env.GITEA_URL }}/api/v1
|
||||||
RENOVATE_GIT_AUTHOR: 'Renovate Bot <renovate@mokoconsulting.tech>'
|
RENOVATE_GIT_AUTHOR: 'Renovate Bot <renovate@mokoconsulting.tech>'
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Sync all wikis
|
- name: Sync all wikis
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GH_PAT }}
|
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
run: |
|
run: |
|
||||||
if [ -z "$GH_TOKEN" ]; then
|
if [ -z "$GH_TOKEN" ]; then
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: mokocli.Release
|
# INGROUP: mokoplatform.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
# PATH: /.mokogitea/workflows/auto-bump.yml
|
# PATH: /.mokogitea/workflows/auto-bump.yml
|
||||||
# VERSION: 09.02.00
|
# VERSION: 09.23.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"
|
||||||
@@ -43,19 +43,21 @@ jobs:
|
|||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Setup mokocli tools
|
- name: Setup mokoplatform tools
|
||||||
run: |
|
run: |
|
||||||
if ! command -v composer &> /dev/null; then
|
if [ -f "/opt/mokoplatform/cli/version_bump.php" ] && [ -f "/opt/mokoplatform/vendor/autoload.php" ]; then
|
||||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
echo "Using pre-installed /opt/mokoplatform"
|
||||||
fi
|
echo "MOKO_CLI=/opt/mokoplatform/cli" >> "$GITHUB_ENV"
|
||||||
if [ -d "/opt/mokocli/cli" ]; then
|
|
||||||
echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV"
|
|
||||||
else
|
else
|
||||||
|
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
|
||||||
|
rm -rf /tmp/mokoplatform-api
|
||||||
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/mokoplatform.git" \
|
||||||
/tmp/mokocli
|
/tmp/mokoplatform-api
|
||||||
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
cd /tmp/mokoplatform-api && composer install --no-dev --no-interaction --quiet
|
||||||
echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV"
|
echo "MOKO_CLI=/tmp/mokoplatform-api/cli" >> "$GITHUB_ENV"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Bump version
|
- name: Bump version
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: mokocli.Release
|
# INGROUP: mokoplatform.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokoplatform
|
||||||
# PATH: /templates/workflows/universal/auto-release.yml.template
|
# PATH: /templates/workflows/universal/auto-release.yml.template
|
||||||
# VERSION: 05.00.00
|
# VERSION: 05.00.00
|
||||||
# BRIEF: Universal build & release � detects platform from manifest.xml
|
# BRIEF: Universal build & release � detects platform from manifest.xml
|
||||||
@@ -66,25 +66,25 @@ jobs:
|
|||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Setup mokocli tools
|
- name: Setup mokoplatform tools
|
||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
run: |
|
run: |
|
||||||
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
|
if [ -f /opt/mokoplatform/cli/version_bump.php ] && [ -f /opt/mokoplatform/vendor/autoload.php ]; then
|
||||||
echo Using pre-installed /opt/mokocli
|
echo Using pre-installed /opt/mokoplatform
|
||||||
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
|
echo MOKO_CLI=/opt/mokoplatform/cli >> $GITHUB_ENV
|
||||||
else
|
else
|
||||||
echo Falling back to fresh clone
|
echo Falling back to fresh clone
|
||||||
if ! command -v composer > /dev/null 2>&1; then
|
if ! command -v composer > /dev/null 2>&1; 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
|
||||||
rm -rf /tmp/mokocli
|
rm -rf /tmp/mokoplatform-api
|
||||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
|
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokoplatform.git
|
||||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokoplatform-api
|
||||||
cd /tmp/mokocli
|
cd /tmp/mokoplatform-api
|
||||||
composer install --no-dev --no-interaction --quiet
|
composer install --no-dev --no-interaction --quiet
|
||||||
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
echo MOKO_CLI=/tmp/mokoplatform-api/cli >> $GITHUB_ENV
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Rename branch to rc
|
- name: Rename branch to rc
|
||||||
@@ -109,40 +109,6 @@ jobs:
|
|||||||
--path . --stability rc --bump minor --branch rc \
|
--path . --stability rc --bump minor --branch rc \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
- name: Update RC release notes from CHANGELOG.md
|
|
||||||
run: |
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
|
||||||
|
|
||||||
# Extract [Unreleased] section from changelog
|
|
||||||
NOTES=""
|
|
||||||
if [ -f "CHANGELOG.md" ]; then
|
|
||||||
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
|
||||||
fi
|
|
||||||
[ -z "$NOTES" ] && NOTES="Release candidate"
|
|
||||||
|
|
||||||
# Find the RC release and update its body
|
|
||||||
RELEASE_ID=$(curl -sf -H "Authorization: token ${TOKEN}" \
|
|
||||||
"${API_BASE}/releases/tags/release-candidate" \
|
|
||||||
| python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
|
||||||
|
|
||||||
if [ -n "$RELEASE_ID" ]; then
|
|
||||||
python3 -c "
|
|
||||||
import json, urllib.request
|
|
||||||
body = open('/dev/stdin').read()
|
|
||||||
payload = json.dumps({'body': body}).encode()
|
|
||||||
req = urllib.request.Request(
|
|
||||||
'${API_BASE}/releases/${RELEASE_ID}',
|
|
||||||
data=payload, method='PATCH',
|
|
||||||
headers={
|
|
||||||
'Authorization': 'token ${TOKEN}',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
})
|
|
||||||
urllib.request.urlopen(req)
|
|
||||||
" <<< "$NOTES"
|
|
||||||
echo "RC release notes updated from CHANGELOG.md"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Summary
|
- name: Summary
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
@@ -183,95 +149,50 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "No conflict markers found"
|
echo "No conflict markers found"
|
||||||
|
|
||||||
- name: Setup mokocli tools
|
- name: Setup mokoplatform tools
|
||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
||||||
run: |
|
run: |
|
||||||
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
|
if [ -f /opt/mokoplatform/cli/version_bump.php ] && [ -f /opt/mokoplatform/vendor/autoload.php ]; then
|
||||||
echo Using pre-installed /opt/mokocli
|
echo Using pre-installed /opt/mokoplatform
|
||||||
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
|
echo MOKO_CLI=/opt/mokoplatform/cli >> $GITHUB_ENV
|
||||||
else
|
else
|
||||||
echo Falling back to fresh clone
|
echo Falling back to fresh clone
|
||||||
if ! command -v composer > /dev/null 2>&1; then
|
if ! command -v composer > /dev/null 2>&1; 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
|
||||||
rm -rf /tmp/mokocli
|
rm -rf /tmp/mokoplatform-api
|
||||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
|
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokoplatform.git
|
||||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokoplatform-api
|
||||||
cd /tmp/mokocli
|
cd /tmp/mokoplatform-api
|
||||||
composer install --no-dev --no-interaction --quiet
|
composer install --no-dev --no-interaction --quiet
|
||||||
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
echo MOKO_CLI=/tmp/mokoplatform-api/cli >> $GITHUB_ENV
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: "Detect platform"
|
|
||||||
id: platform
|
|
||||||
run: |
|
|
||||||
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
|
|
||||||
php ${MOKO_CLI}/manifest_read.php --path . --github-output 2>/dev/null || true
|
|
||||||
|
|
||||||
- name: "Determine version bump level"
|
|
||||||
id: bump
|
|
||||||
run: |
|
|
||||||
# Fix/patch branches: version was already bumped by pre-release, just strip suffix
|
|
||||||
# Feature/dev branches: bump minor for the new stable release
|
|
||||||
HEAD_REF="${{ github.event.pull_request.head.ref || 'dev' }}"
|
|
||||||
case "$HEAD_REF" in
|
|
||||||
fix/*|patch/*|hotfix/*|bugfix/*) BUMP="none" ;;
|
|
||||||
*) BUMP="minor" ;;
|
|
||||||
esac
|
|
||||||
echo "level=${BUMP}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "Bump level: ${BUMP} (from branch: ${HEAD_REF})"
|
|
||||||
|
|
||||||
- name: "Publish stable release"
|
- name: "Publish stable release"
|
||||||
run: |
|
run: |
|
||||||
BUMP_FLAG=""
|
|
||||||
if [ "${{ steps.bump.outputs.level }}" != "none" ]; then
|
|
||||||
BUMP_FLAG="--bump ${{ steps.bump.outputs.level }}"
|
|
||||||
fi
|
|
||||||
php ${MOKO_CLI}/release_publish.php \
|
php ${MOKO_CLI}/release_publish.php \
|
||||||
--path . --stability stable ${BUMP_FLAG} --branch main \
|
--path . --stability stable --bump minor --branch main \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
- name: "Read published version"
|
- name: Update release notes from CHANGELOG.md
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "")
|
|
||||||
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
|
||||||
[ -z "$VERSION" ] && VERSION="00.00.00" && echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "tag=stable" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "release_tag=stable" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "branch=main" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "Published version: ${VERSION}"
|
|
||||||
|
|
||||||
- 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}"
|
||||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
|
||||||
|
|
||||||
# Get the stable release info (version and ID)
|
|
||||||
RELEASE_JSON=$(curl -sf -H "Authorization: token ${TOKEN}" \
|
|
||||||
"${API_BASE}/releases/tags/stable" 2>/dev/null || echo '{}')
|
|
||||||
RELEASE_ID=$(python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" <<< "$RELEASE_JSON" 2>/dev/null || true)
|
|
||||||
# Extract version from release name (e.g. "06.17.00" or "v06.17.00")
|
|
||||||
VERSION=$(python3 -c "
|
|
||||||
import json, sys, re
|
|
||||||
r = json.load(sys.stdin)
|
|
||||||
name = r.get('name', '')
|
|
||||||
m = re.search(r'(\d+\.\d+\.\d+)', name)
|
|
||||||
print(m.group(1) if m else '')
|
|
||||||
" <<< "$RELEASE_JSON" 2>/dev/null || true)
|
|
||||||
|
|
||||||
# Extract [Unreleased] section from changelog
|
# Extract [Unreleased] section from changelog
|
||||||
NOTES=""
|
|
||||||
if [ -f "CHANGELOG.md" ]; then
|
if [ -f "CHANGELOG.md" ]; then
|
||||||
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
||||||
|
[ -z "$NOTES" ] && NOTES="Stable release"
|
||||||
|
else
|
||||||
|
NOTES="Stable release"
|
||||||
fi
|
fi
|
||||||
[ -z "$NOTES" ] && NOTES="Stable release"
|
|
||||||
|
|
||||||
# Update release body via API
|
# Update release body via API
|
||||||
|
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
|
"${API_BASE}/releases/tags/stable" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||||
|
|
||||||
if [ -n "$RELEASE_ID" ]; then
|
if [ -n "$RELEASE_ID" ]; then
|
||||||
python3 -c "
|
python3 -c "
|
||||||
import json, urllib.request
|
import json, urllib.request
|
||||||
@@ -281,7 +202,7 @@ jobs:
|
|||||||
'${API_BASE}/releases/${RELEASE_ID}',
|
'${API_BASE}/releases/${RELEASE_ID}',
|
||||||
data=payload, method='PATCH',
|
data=payload, method='PATCH',
|
||||||
headers={
|
headers={
|
||||||
'Authorization': 'token ${TOKEN}',
|
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
})
|
})
|
||||||
urllib.request.urlopen(req)
|
urllib.request.urlopen(req)
|
||||||
@@ -289,24 +210,6 @@ jobs:
|
|||||||
echo "Release notes updated from CHANGELOG.md"
|
echo "Release notes updated from CHANGELOG.md"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Promote [Unreleased] → [version] in CHANGELOG.md and reset
|
|
||||||
if [ -n "$VERSION" ] && [ -f "CHANGELOG.md" ]; then
|
|
||||||
DATE=$(date +%Y-%m-%d)
|
|
||||||
python3 -c "
|
|
||||||
import sys
|
|
||||||
version, date = sys.argv[1], sys.argv[2]
|
|
||||||
content = open('CHANGELOG.md').read()
|
|
||||||
old = '## [Unreleased]'
|
|
||||||
new = f'## [Unreleased]\n\n## [{version}] --- {date}'
|
|
||||||
content = content.replace(old, new, 1)
|
|
||||||
open('CHANGELOG.md', 'w').write(content)
|
|
||||||
" "$VERSION" "$DATE"
|
|
||||||
git add CHANGELOG.md
|
|
||||||
git commit -m "chore: promote changelog [Unreleased] → [${VERSION}]" || true
|
|
||||||
git push origin main || true
|
|
||||||
echo "Changelog promoted: [Unreleased] → [${VERSION}]"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
||||||
- name: "Step 9: Mirror release to GitHub"
|
- name: "Step 9: Mirror release to GitHub"
|
||||||
if: >-
|
if: >-
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoStandards.Universal
|
# INGROUP: MokoPlatform.Universal
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
# PATH: /.mokogitea/workflows/branch-cleanup.yml
|
# PATH: /.mokogitea/workflows/branch-cleanup.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: Delete feature branches after PR merge
|
# BRIEF: Delete feature branches after PR merge
|
||||||
|
|
||||||
name: "Branch Cleanup"
|
name: "Branch Cleanup"
|
||||||
|
|||||||
@@ -13,6 +13,19 @@
|
|||||||
name: "Generic: Project CI"
|
name: "Generic: Project CI"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
- dev/**
|
||||||
|
- rc/**
|
||||||
|
- version/**
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
- dev/**
|
||||||
|
- rc/**
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoStandards.Maintenance
|
# INGROUP: mokoplatform.Maintenance
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
# PATH: /.gitea/workflows/cleanup.yml
|
# PATH: /.mokogitea/workflows/cleanup.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
||||||
|
|
||||||
name: "Universal: Repository Cleanup"
|
name: "Universal: Repository Cleanup"
|
||||||
@@ -33,17 +33,17 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.GA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
|
||||||
- name: Delete merged branches
|
- name: Delete merged branches
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
GA_TOKEN: ${{ secrets.MOKOGITEA_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 ${GA_TOKEN}" \
|
BRANCHES=$(curl -sS -H "Authorization: token ${GITEA_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 ${GA_TOKEN}" \
|
curl -sS -X DELETE -H "Authorization: token ${GITEA_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.GA_TOKEN }}
|
GA_TOKEN: ${{ secrets.MOKOGITEA_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 ${GA_TOKEN}" \
|
RUNS=$(curl -sS -H "Authorization: token ${GITEA_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 ${GA_TOKEN}" \
|
curl -sS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
|
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
|
||||||
DELETED=$((DELETED + 1))
|
DELETED=$((DELETED + 1))
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoStandards.Security
|
# INGROUP: mokoplatform.Security
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokoplatform
|
||||||
# PATH: /templates/workflows/gitleaks.yml.template
|
# PATH: /templates/workflows/gitleaks.yml.template
|
||||||
# VERSION: 01.00.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
||||||
#
|
#
|
||||||
# +========================================================================+
|
# +========================================================================+
|
||||||
@@ -25,6 +25,10 @@
|
|||||||
name: "Universal: Secret Scanning"
|
name: "Universal: Secret Scanning"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- 'dev/**'
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 5 * * 1' # Weekly Monday 05:00 UTC
|
- cron: '0 5 * * 1' # Weekly Monday 05:00 UTC
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: mokocli.Automation
|
# INGROUP: mokoplatform.Automation
|
||||||
# VERSION: 09.32.01
|
# VERSION: 09.26.02
|
||||||
# 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"
|
||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Create branch and comment
|
- name: Create branch and comment
|
||||||
run: |
|
run: |
|
||||||
TOKEN="${{ secrets.GA_TOKEN }}"
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
ISSUE_NUM="${{ github.event.issue.number }}"
|
ISSUE_NUM="${{ github.event.issue.number }}"
|
||||||
ISSUE_TITLE="${{ github.event.issue.title }}"
|
ISSUE_TITLE="${{ github.event.issue.title }}"
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoStandards.Notifications
|
# INGROUP: mokoplatform.Notifications
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
# PATH: /.gitea/workflows/notify.yml
|
# PATH: /.mokogitea/workflows/notify.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
||||||
|
|
||||||
name: "Universal: Notifications"
|
name: "Universal: Notifications"
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.CI
|
# INGROUP: mokoplatform.CI
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokoplatform
|
||||||
# PATH: /templates/workflows/universal/pr-check.yml.template
|
# PATH: /templates/workflows/universal/pr-check.yml.template
|
||||||
# VERSION: 09.23.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: PR gate — branch policy + code validation before merge
|
# BRIEF: PR gate — branch policy + code validation before merge
|
||||||
@@ -96,32 +96,6 @@ jobs:
|
|||||||
echo "Branch policy: OK (${HEAD} → ${BASE})"
|
echo "Branch policy: OK (${HEAD} → ${BASE})"
|
||||||
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
|
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
# ── Secret Scanning ──────────────────────────────────────────────────
|
|
||||||
gitleaks:
|
|
||||||
name: Secret Scan
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Install Gitleaks
|
|
||||||
run: |
|
|
||||||
GITLEAKS_VERSION="8.21.2"
|
|
||||||
curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
|
|
||||||
| tar -xz -C /usr/local/bin gitleaks
|
|
||||||
|
|
||||||
- name: Scan PR commits for secrets
|
|
||||||
run: |
|
|
||||||
if gitleaks detect --source . --verbose \
|
|
||||||
--log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} 2>&1; then
|
|
||||||
echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "::error::Potential secrets detected in PR commits"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Code Validation ────────────────────────────────────────────────────
|
# ── Code Validation ────────────────────────────────────────────────────
|
||||||
validate:
|
validate:
|
||||||
name: Validate PR
|
name: Validate PR
|
||||||
|
|||||||
@@ -4,26 +4,23 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: mokocli.Release
|
# INGROUP: mokoplatform.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||||
# VERSION: 05.01.00
|
# VERSION: 05.01.00
|
||||||
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
|
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
|
||||||
|
|
||||||
name: "Universal: Pre-Release"
|
name: "Universal: Pre-Release"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
branches:
|
branches:
|
||||||
- dev
|
- dev
|
||||||
- 'fix/**'
|
pull_request_target:
|
||||||
- 'patch/**'
|
types: [synchronize, opened, reopened]
|
||||||
- 'hotfix/**'
|
branches:
|
||||||
- 'bugfix/**'
|
- main
|
||||||
- 'chore/**'
|
|
||||||
- alpha
|
|
||||||
- beta
|
|
||||||
- rc
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
stability:
|
stability:
|
||||||
@@ -46,11 +43,12 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})"
|
name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
|
||||||
runs-on: release
|
runs-on: release
|
||||||
if: >-
|
if: >-
|
||||||
github.event_name == 'workflow_dispatch' ||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
github.event_name == 'push'
|
(github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') ||
|
||||||
|
(github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main')
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -58,47 +56,40 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
ref: ${{ github.ref_name }}
|
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }}
|
||||||
|
|
||||||
- name: Setup mokocli tools
|
- name: Setup mokoplatform tools
|
||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
run: |
|
run: |
|
||||||
# Use pre-installed /opt/mokocli if available (updated by cron every 6h)
|
# Use pre-installed /opt/mokoplatform if available (updated by cron every 6h)
|
||||||
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/cli/manifest_element.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
|
if [ -f /opt/mokoplatform/cli/version_bump.php ] && [ -f /opt/mokoplatform/cli/manifest_element.php ] && [ -f /opt/mokoplatform/vendor/autoload.php ]; then
|
||||||
echo Using pre-installed /opt/mokocli
|
echo Using pre-installed /opt/mokoplatform
|
||||||
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
|
echo MOKO_CLI=/opt/mokoplatform/cli >> $GITHUB_ENV
|
||||||
else
|
else
|
||||||
echo Falling back to fresh clone
|
echo Falling back to fresh clone
|
||||||
if ! command -v composer > /dev/null 2>&1; then
|
if ! command -v composer > /dev/null 2>&1; 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
|
||||||
rm -rf /tmp/mokocli
|
rm -rf /tmp/mokoplatform-api
|
||||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
|
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokoplatform.git
|
||||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokoplatform-api
|
||||||
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
cd /tmp/mokoplatform-api && composer install --no-dev --no-interaction --quiet
|
||||||
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
echo MOKO_CLI=/tmp/mokoplatform-api/cli >> $GITHUB_ENV
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Detect platform
|
- name: Detect platform
|
||||||
id: platform
|
id: platform
|
||||||
run: |
|
run: |
|
||||||
# Auto-detect and update platform if not set in manifest
|
|
||||||
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
|
|
||||||
php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
||||||
|
|
||||||
- name: Resolve metadata and bump version
|
- name: Resolve metadata and bump version
|
||||||
id: meta
|
id: meta
|
||||||
run: |
|
run: |
|
||||||
# Auto-detect stability from branch name on push, or use input on dispatch
|
# Auto-detect stability: RC for PRs targeting main, else use input or default to development
|
||||||
if [ "${{ github.event_name }}" = "push" ]; then
|
if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then
|
||||||
case "${{ github.ref_name }}" in
|
STABILITY="release-candidate"
|
||||||
rc) STABILITY="release-candidate" ;;
|
|
||||||
alpha) STABILITY="alpha" ;;
|
|
||||||
beta) STABILITY="beta" ;;
|
|
||||||
*) STABILITY="development" ;;
|
|
||||||
esac
|
|
||||||
else
|
else
|
||||||
STABILITY="${{ inputs.stability || 'development' }}"
|
STABILITY="${{ inputs.stability || 'development' }}"
|
||||||
fi
|
fi
|
||||||
@@ -173,7 +164,7 @@ jobs:
|
|||||||
php ${MOKO_CLI}/release_create.php \
|
php ${MOKO_CLI}/release_create.php \
|
||||||
--path . --version "$VERSION" --tag "$TAG" \
|
--path . --version "$VERSION" --tag "$TAG" \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
|
--repo "${GITEA_REPO}" --branch dev --prerelease
|
||||||
|
|
||||||
- name: Update release notes from CHANGELOG.md
|
- name: Update release notes from CHANGELOG.md
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: mokocli.Universal
|
# INGROUP: MokoPlatform.Universal
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
# PATH: /.mokogitea/workflows/rc-revert.yml
|
# PATH: /.mokogitea/workflows/rc-revert.yml
|
||||||
# VERSION: 09.23.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
|
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: mokocli.Validation
|
# INGROUP: mokoplatform.Validation
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokoplatform
|
||||||
# PATH: /templates/workflows/joomla/repo_health.yml.template
|
# PATH: /templates/workflows/joomla/repo_health.yml.template
|
||||||
# VERSION: 09.23.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts.
|
# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts.
|
||||||
@@ -33,8 +33,7 @@ on:
|
|||||||
- scripts
|
- scripts
|
||||||
- repo
|
- repo
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
push:
|
||||||
- main
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoStandards.Security
|
# INGROUP: mokoplatform.Security
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
# PATH: /.gitea/workflows/security-audit.yml
|
# PATH: /.mokogitea/workflows/security-audit.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: Dependency vulnerability scanning for composer and npm packages
|
# BRIEF: Dependency vulnerability scanning for composer and npm packages
|
||||||
|
|
||||||
name: "Universal: Security Audit"
|
name: "Universal: Security Audit"
|
||||||
@@ -80,3 +80,19 @@ jobs:
|
|||||||
-H "Priority: high" \
|
-H "Priority: high" \
|
||||||
-d "Security audit found vulnerabilities. Review dependency updates." \
|
-d "Security audit found vulnerabilities. Review dependency updates." \
|
||||||
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
||||||
|
|
||||||
|
|
||||||
|
- name: Joomla version audit
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
if [ -f "monitoring/joomla-version-audit.php" ] && [ -n "$JOOMLA_SITES" ]; then
|
||||||
|
echo "$JOOMLA_SITES" > /tmp/sites.json
|
||||||
|
php monitoring/joomla-version-audit.php --sites /tmp/sites.json || true
|
||||||
|
echo "### Joomla Version Audit" >> $GITHUB_STEP_SUMMARY
|
||||||
|
rm -f /tmp/sites.json
|
||||||
|
else
|
||||||
|
echo "Joomla audit skipped (no script or JOOMLA_SITES_JSON not configured)"
|
||||||
|
fi
|
||||||
|
env:
|
||||||
|
JOOMLA_SITES: ${{ vars.JOOMLA_SITES_JSON }}
|
||||||
|
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: mokocli.Automation
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
|
||||||
# PATH: /.mokogitea/workflows/sync-feature-versions.yml
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Merge dev into open feature branches after version bumps
|
|
||||||
|
|
||||||
name: "Universal: Sync Feature Branch Versions"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- dev
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
env:
|
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
sync:
|
|
||||||
name: Sync feature branches with dev
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: >-
|
|
||||||
github.event_name == 'workflow_dispatch' ||
|
|
||||||
contains(github.event.head_commit.message, 'chore(version)')
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout dev
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
ref: dev
|
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
|
|
||||||
- name: Configure git
|
|
||||||
run: |
|
|
||||||
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"
|
|
||||||
|
|
||||||
- name: Merge dev into feature branches
|
|
||||||
run: |
|
|
||||||
echo "=== Syncing feature branches with dev ==="
|
|
||||||
|
|
||||||
# Fetch all remote branches
|
|
||||||
git fetch origin
|
|
||||||
|
|
||||||
# Find feature branches (feature/*, fix/*, patch/*, hotfix/*, bugfix/*, chore/*)
|
|
||||||
BRANCHES=$(git branch -r --list 'origin/feature/*' 'origin/fix/*' 'origin/patch/*' 'origin/hotfix/*' 'origin/bugfix/*' 'origin/chore/*' | sed 's|origin/||; s/^[[:space:]]*//')
|
|
||||||
|
|
||||||
if [ -z "$BRANCHES" ]; then
|
|
||||||
echo "No feature branches found — nothing to sync"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
SYNCED=0
|
|
||||||
SKIPPED=0
|
|
||||||
FAILED=0
|
|
||||||
|
|
||||||
for BRANCH in $BRANCHES; do
|
|
||||||
echo ""
|
|
||||||
echo "--- ${BRANCH} ---"
|
|
||||||
|
|
||||||
# Skip branches that are already up to date with dev
|
|
||||||
if git merge-base --is-ancestor dev "origin/${BRANCH}" 2>/dev/null; then
|
|
||||||
echo "Already up to date"
|
|
||||||
SKIPPED=$((SKIPPED + 1))
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Try to merge dev into the branch
|
|
||||||
git checkout "origin/${BRANCH}" -B "$BRANCH" 2>/dev/null
|
|
||||||
if git merge dev --no-edit -m "chore: merge dev into ${BRANCH} (version sync) [skip ci]" 2>/dev/null; then
|
|
||||||
git push origin "$BRANCH" 2>/dev/null
|
|
||||||
echo "Synced successfully"
|
|
||||||
SYNCED=$((SYNCED + 1))
|
|
||||||
else
|
|
||||||
git merge --abort 2>/dev/null || true
|
|
||||||
echo "Merge conflict — skipping (manual rebase needed)"
|
|
||||||
FAILED=$((FAILED + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Return to dev
|
|
||||||
git checkout dev 2>/dev/null || true
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== Summary ==="
|
|
||||||
echo "Synced: ${SYNCED}"
|
|
||||||
echo "Already current: ${SKIPPED}"
|
|
||||||
echo "Conflicts (skipped): ${FAILED}"
|
|
||||||
|
|
||||||
if [ "$FAILED" -gt 0 ]; then
|
|
||||||
echo "::warning::${FAILED} branch(es) had merge conflicts and need manual rebase"
|
|
||||||
fi
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: mokocli.Universal
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
|
||||||
# PATH: /.mokogitea/workflows/workflow-sync-trigger.yml
|
|
||||||
# VERSION: 01.01.00
|
|
||||||
# BRIEF: Trigger workflow sync to live repos when a PR is merged to main
|
|
||||||
|
|
||||||
name: "Universal: Workflow Sync Trigger"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [closed]
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
sync:
|
|
||||||
name: Sync workflows to live repos
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: >-
|
|
||||||
github.event.pull_request.merged == true &&
|
|
||||||
!contains(github.event.pull_request.title, '[skip sync]')
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Determine platform from repo name
|
|
||||||
id: platform
|
|
||||||
run: |
|
|
||||||
REPO="${{ github.event.repository.name }}"
|
|
||||||
case "$REPO" in
|
|
||||||
Template-Joomla) PLATFORM="joomla" ;;
|
|
||||||
Template-Dolibarr) PLATFORM="dolibarr" ;;
|
|
||||||
Template-Go) PLATFORM="go" ;;
|
|
||||||
Template-MCP) PLATFORM="mcp" ;;
|
|
||||||
Template-Generic) PLATFORM="" ;;
|
|
||||||
*) PLATFORM="" ;;
|
|
||||||
esac
|
|
||||||
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "Platform: ${PLATFORM:-all}"
|
|
||||||
|
|
||||||
- name: Clone mokocli
|
|
||||||
env:
|
|
||||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
run: |
|
|
||||||
GITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
|
|
||||||
git clone --depth 1 "${GITEA_URL}/MokoConsulting/mokocli.git" /tmp/mokocli
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
cd /tmp/mokocli
|
|
||||||
composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
|
||||||
|
|
||||||
- name: Run workflow sync
|
|
||||||
env:
|
|
||||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
run: |
|
|
||||||
ARGS="--token ${MOKOGITEA_TOKEN}"
|
|
||||||
ARGS="${ARGS} --org ${{ vars.GITEA_ORG || github.repository_owner }}"
|
|
||||||
ARGS="${ARGS} --phase repos"
|
|
||||||
|
|
||||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
|
||||||
if [ -n "$PLATFORM" ]; then
|
|
||||||
ARGS="${ARGS} --platform-filter ${PLATFORM}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
php /tmp/mokocli/cli/workflow_sync.php ${ARGS}
|
|
||||||
+53
-28
@@ -2,8 +2,8 @@
|
|||||||
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: MokoCli.Root
|
DEFGROUP: MokoStandards.Root
|
||||||
INGROUP: MokoCli
|
INGROUP: MokoStandards
|
||||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
PATH: /CHANGELOG.md
|
PATH: /CHANGELOG.md
|
||||||
BRIEF: Release changelog
|
BRIEF: Release changelog
|
||||||
@@ -12,32 +12,57 @@ BRIEF: Release changelog
|
|||||||
# Changelog
|
# Changelog
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
## [09.32.00] --- 2026-06-21
|
|
||||||
|
|
||||||
## [09.32.00] --- 2026-06-21
|
|
||||||
|
|
||||||
## [09.31.00] --- 2026-06-21
|
|
||||||
|
|
||||||
## [09.31.00] --- 2026-06-21
|
|
||||||
|
|
||||||
## [09.30.00] --- 2026-06-21
|
|
||||||
|
|
||||||
## [09.30.00] --- 2026-06-21
|
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- `security:advisories` command — cross-repo security advisory aggregator (#150)
|
- `cli/manifest_integrity.php` — org-wide manifest validation tool (564 lines)
|
||||||
- Scans org repos for known CVEs via `composer audit`
|
- `manifest_detect.php` — detect `display_name`, `target_version`, `php_minimum` fields
|
||||||
- Aggregates results into a single report with severity breakdown
|
|
||||||
- Auto-creates tracking issues for critical/high vulnerabilities (`--create-issues`)
|
|
||||||
- Checkpoint-based resumability with `--resume`
|
|
||||||
- Export to JSON/CSV with `--export`
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- `manifest:read` rewritten to use Gitea manifest API as primary source (#283)
|
- MokoSuite → MokoSuiteClient rename: updated MCP README reference, composer template, metadata_detect comment
|
||||||
- Falls back to auto-detection from source tree (Joomla, Dolibarr, generic)
|
- MCP servers extracted from monorepo to standalone `A:/MCP/` directories
|
||||||
- No longer requires `.mokogitea/manifest.xml` file
|
- All 9 MCP servers published to npm (`@mokoconsulting/`) and Gitea package registry
|
||||||
- Backward-compatible field aliases for existing CI consumers
|
- `.mcp.json` converted from local file paths to `npx -y @mokoconsulting/...@latest`
|
||||||
- Renamed `MokoStandards` namespace → `MokoCli` across all files
|
- `NPM_TOKEN` saved as MokoConsulting org secret for CI/CD
|
||||||
- Renamed `MokoEnterprise` namespace → `MokoCli` across all files
|
- Templates: Joomla Makefile and composer.json updated with MokoSuite references
|
||||||
- Renamed `MokoStandardsParser` class → `ManifestParser`
|
|
||||||
- Fixed `composer.json` autoload paths: `src/` → `source/`
|
### Removed
|
||||||
|
- `mcp/servers/` directory — all MCP server source moved to `A:/MCP/mcp_*/`
|
||||||
|
|
||||||
|
## [09.26.00] --- 2026-06-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- `workflow_sync.php` — cascading workflow sync from Generic → platform templates → live repos based on manifest.platform
|
||||||
|
- `platform_detect.php` — auto-detect repo platform type (joomla/dolibarr/go/mcp/platform/generic) from file structure, optionally update manifest
|
||||||
|
- Version prefix support in `version_read.php` and `version_bump.php` — repos with `<version_prefix>` in manifest (e.g. MokoGitea: `1.26.1+moko.`) get prefix-aware version scanning and bumping
|
||||||
|
- Platform types: joomla, dolibarr, go, mcp, platform, generic
|
||||||
|
- Template-Go and Template-MCP repos created
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- `auto-release.yml` — patch branches (fix/*, patch/*, hotfix/*, bugfix/*) use `--bump none` (pre-release already bumped); feature/dev branches bump minor
|
||||||
|
- `pre-release.yml` — triggers on push to dev, fix/**, patch/**, hotfix/**, bugfix/**, alpha, beta, rc branches
|
||||||
|
- Version format standardized: `[prefix]XX.YY.ZZ` in source files, suffix (`-dev`, `-rc`) added by release system only
|
||||||
|
|
||||||
|
## [09.26.00] --- 2026-06-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- `cli/manifest_detect.php` — auto-detect manifest fields from source files (Joomla, Dolibarr, Go, MCP/Node, generic)
|
||||||
|
- Supports `--json`, `--diff`, `--update`, `--github-output` modes
|
||||||
|
- Warns on missing core fields (platform, name, version, package_type, language, entry_point)
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- `mcp/servers/mokowaas_api/` — consolidated into mcp-mokowaas-api repo
|
||||||
|
|
||||||
|
## [09.25.00] --- 2026-06-04
|
||||||
|
|
||||||
|
## [09.23] --- 2026-05-31
|
||||||
|
|
||||||
|
## [09.22] --- 2026-05-31
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **refactor(cli):** migrate 64 legacy scripts to CliFramework (#235) — all tools in cli/, automation/, maintenance/, deploy/, release/ now extend CliFramework with free --help, --verbose, --quiet, --dry-run, --json, banners, and coloured logging
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- fix: auto-detect org/repo in updates_xml_build from manifest and git remote
|
||||||
|
- fix: restore hyphen in version suffixes
|
||||||
|
- fix: release names use standardized format
|
||||||
|
- fix: remove lesser stream copies, each stream updates independently
|
||||||
|
- fix: sort updates.xml entries dev first, stable last
|
||||||
|
|||||||
+161
-161
@@ -1,161 +1,161 @@
|
|||||||
# Contributing to Moko Consulting Projects
|
# Contributing to Moko Consulting Projects
|
||||||
|
|
||||||
Thank you for your interest in contributing. All Moko Consulting repositories follow this universal workflow and version policy.
|
Thank you for your interest in contributing. All Moko Consulting repositories follow this universal workflow and version policy.
|
||||||
|
|
||||||
## Branching Workflow
|
## Branching Workflow
|
||||||
|
|
||||||
```
|
```
|
||||||
feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge──> main
|
feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge──> main
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step by step
|
### Step by step
|
||||||
|
|
||||||
1. **Create a feature branch** from `dev`:
|
1. **Create a feature branch** from `dev`:
|
||||||
```bash
|
```bash
|
||||||
git checkout dev && git pull
|
git checkout dev && git pull
|
||||||
git checkout -b feature/my-change
|
git checkout -b feature/my-change
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Work and commit** on your feature branch. Push to origin.
|
2. **Work and commit** on your feature branch. Push to origin.
|
||||||
|
|
||||||
3. **Open a PR**: `feature/my-change` → `dev`. After review and checks, merge it.
|
3. **Open a PR**: `feature/my-change` → `dev`. After review and checks, merge it.
|
||||||
|
|
||||||
4. **When ready for release**, open a **draft PR**: `dev` → `main`.
|
4. **When ready for release**, open a **draft PR**: `dev` → `main`.
|
||||||
- This automatically renames the source branch to `rc` (release candidate)
|
- This automatically renames the source branch to `rc` (release candidate)
|
||||||
- An RC pre-release is built and uploaded
|
- An RC pre-release is built and uploaded
|
||||||
|
|
||||||
5. **Alpha and beta branches** are created by manually renaming the branch before the RC stage:
|
5. **Alpha and beta branches** are created by manually renaming the branch before the RC stage:
|
||||||
- Rename `dev` to `alpha` for early testing → alpha pre-release is built
|
- Rename `dev` to `alpha` for early testing → alpha pre-release is built
|
||||||
- Rename `alpha` to `beta` for feature-complete testing → beta pre-release is built
|
- Rename `alpha` to `beta` for feature-complete testing → beta pre-release is built
|
||||||
- When the draft PR is created, the branch is renamed to `rc`
|
- When the draft PR is created, the branch is renamed to `rc`
|
||||||
|
|
||||||
6. **Once PR checks pass** on the `rc` branch, mark the PR as ready and merge to `main`.
|
6. **Once PR checks pass** on the `rc` branch, mark the PR as ready and merge to `main`.
|
||||||
|
|
||||||
7. **Merging to main** triggers the stable release pipeline:
|
7. **Merging to main** triggers the stable release pipeline:
|
||||||
- Minor version bump (e.g., `02.09.xx` → `02.10.00`)
|
- Minor version bump (e.g., `02.09.xx` → `02.10.00`)
|
||||||
- Stability suffix stripped (clean version)
|
- Stability suffix stripped (clean version)
|
||||||
- Gitea release created with ZIP/tar.gz packages
|
- Gitea release created with ZIP/tar.gz packages
|
||||||
- `updates.xml` updated (Joomla extensions)
|
- `updates.xml` updated (Joomla extensions)
|
||||||
- `dev` branch recreated from `main`
|
- `dev` branch recreated from `main`
|
||||||
|
|
||||||
### Branch summary
|
### Branch summary
|
||||||
|
|
||||||
| Branch | Purpose | Created by |
|
| Branch | Purpose | Created by |
|
||||||
|--------|---------|-----------|
|
|--------|---------|-----------|
|
||||||
| `feature/*` | New features and fixes | Developer |
|
| `feature/*` | New features and fixes | Developer |
|
||||||
| `dev` | Integration branch | Auto-recreated after release |
|
| `dev` | Integration branch | Auto-recreated after release |
|
||||||
| `alpha` | Alpha pre-release testing | Manual rename from `dev` |
|
| `alpha` | Alpha pre-release testing | Manual rename from `dev` |
|
||||||
| `beta` | Beta pre-release testing | Manual rename from `alpha` |
|
| `beta` | Beta pre-release testing | Manual rename from `alpha` |
|
||||||
| `rc` | Release candidate | Auto-renamed on draft PR to main |
|
| `rc` | Release candidate | Auto-renamed on draft PR to main |
|
||||||
| `main` | Stable releases | Protected, merge only |
|
| `main` | Stable releases | Protected, merge only |
|
||||||
| `version/XX.YY.ZZ` | Archived release snapshots | Auto-created by CI |
|
| `version/XX.YY.ZZ` | Archived release snapshots | Auto-created by CI |
|
||||||
|
|
||||||
### Protected branches
|
### Protected branches
|
||||||
|
|
||||||
| Branch | Direct push | Merge via |
|
| Branch | Direct push | Merge via |
|
||||||
|--------|------------|-----------|
|
|--------|------------|-----------|
|
||||||
| `main` | Blocked (CI bot whitelisted) | PR merge only |
|
| `main` | Blocked (CI bot whitelisted) | PR merge only |
|
||||||
| `dev` | Blocked (CI bot whitelisted) | PR merge from feature/* |
|
| `dev` | Blocked (CI bot whitelisted) | PR merge from feature/* |
|
||||||
| `rc` | Blocked (CI bot whitelisted) | Auto-created on draft PR |
|
| `rc` | Blocked (CI bot whitelisted) | Auto-created on draft PR |
|
||||||
| `alpha` | Blocked (CI bot whitelisted) | Manual rename |
|
| `alpha` | Blocked (CI bot whitelisted) | Manual rename |
|
||||||
| `beta` | Blocked (CI bot whitelisted) | Manual rename |
|
| `beta` | Blocked (CI bot whitelisted) | Manual rename |
|
||||||
| `feature/*` | Open | N/A (source branch) |
|
| `feature/*` | Open | N/A (source branch) |
|
||||||
|
|
||||||
## Version Policy
|
## Version Policy
|
||||||
|
|
||||||
### Format
|
### Format
|
||||||
|
|
||||||
All versions use `XX.YY.ZZ` — three two-digit segments, zero-padded:
|
All versions use `XX.YY.ZZ` — three two-digit segments, zero-padded:
|
||||||
|
|
||||||
- **XX** — Major version (breaking changes)
|
- **XX** — Major version (breaking changes)
|
||||||
- **YY** — Minor version (new features, bumped on release to main)
|
- **YY** — Minor version (new features, bumped on release to main)
|
||||||
- **ZZ** — Patch version (auto-incremented on every push to dev/feature branches)
|
- **ZZ** — Patch version (auto-incremented on every push to dev/feature branches)
|
||||||
|
|
||||||
Rollover: patch `99` → `00` increments minor; minor `99` → `00` increments major.
|
Rollover: patch `99` → `00` increments minor; minor `99` → `00` increments major.
|
||||||
|
|
||||||
### Stability suffixes
|
### Stability suffixes
|
||||||
|
|
||||||
Each branch appends a suffix to indicate stability:
|
Each branch appends a suffix to indicate stability:
|
||||||
|
|
||||||
| Branch | Suffix | Example |
|
| Branch | Suffix | Example |
|
||||||
|--------|--------|---------|
|
|--------|--------|---------|
|
||||||
| `main` | (none) | `02.09.00` |
|
| `main` | (none) | `02.09.00` |
|
||||||
| `dev` | `-dev` | `02.09.01-dev` |
|
| `dev` | `-dev` | `02.09.01-dev` |
|
||||||
| `feature/*` | `-dev` | `02.09.01-dev` |
|
| `feature/*` | `-dev` | `02.09.01-dev` |
|
||||||
| `alpha` | `-alpha` | `02.09.01-alpha` |
|
| `alpha` | `-alpha` | `02.09.01-alpha` |
|
||||||
| `beta` | `-beta` | `02.09.01-beta` |
|
| `beta` | `-beta` | `02.09.01-beta` |
|
||||||
| `rc` | `-rc` | `02.09.01-rc` |
|
| `rc` | `-rc` | `02.09.01-rc` |
|
||||||
|
|
||||||
### Auto version bump
|
### Auto version bump
|
||||||
|
|
||||||
On every push to `dev`, `feature/*`, or `patch/*`:
|
On every push to `dev`, `feature/*`, or `patch/*`:
|
||||||
|
|
||||||
1. Patch version incremented
|
1. Patch version incremented
|
||||||
2. Stability suffix `-dev` applied
|
2. Stability suffix `-dev` applied
|
||||||
3. All version-bearing files updated (manifests, CHANGELOG, PHP headers, etc.)
|
3. All version-bearing files updated (manifests, CHANGELOG, PHP headers, etc.)
|
||||||
4. Commit created with `[skip ci]` to avoid loops
|
4. Commit created with `[skip ci]` to avoid loops
|
||||||
|
|
||||||
### Release version flow
|
### Release version flow
|
||||||
|
|
||||||
Version bumps happen at specific release events:
|
Version bumps happen at specific release events:
|
||||||
|
|
||||||
| Event | Bump | Example |
|
| Event | Bump | Example |
|
||||||
|-------|------|---------|
|
|-------|------|---------|
|
||||||
| Feature merged to dev | Patch bump after dev release | `02.09.01-dev` → release → `02.09.02-dev` |
|
| Feature merged to dev | Patch bump after dev release | `02.09.01-dev` → release → `02.09.02-dev` |
|
||||||
| Dev promoted to RC | Minor bump | `02.09.02-dev` → `02.10.00-rc` |
|
| Dev promoted to RC | Minor bump | `02.09.02-dev` → `02.10.00-rc` |
|
||||||
| RC merged to main | Minor bump | `02.10.00-rc` → `02.11.00` (stable) |
|
| RC merged to main | Minor bump | `02.10.00-rc` → `02.11.00` (stable) |
|
||||||
| Dev recreated from main | Patch bump | `02.11.00` → `02.11.01-dev` |
|
| Dev recreated from main | Patch bump | `02.11.00` → `02.11.01-dev` |
|
||||||
|
|
||||||
### Release stream copies
|
### Release stream copies
|
||||||
|
|
||||||
When a higher-stability release is published, copies are created for all lesser streams with the same base version:
|
When a higher-stability release is published, copies are created for all lesser streams with the same base version:
|
||||||
|
|
||||||
- **RC `02.10.00-rc`** also creates: `02.10.00-dev`, `02.10.00-alpha`, `02.10.00-beta`
|
- **RC `02.10.00-rc`** also creates: `02.10.00-dev`, `02.10.00-alpha`, `02.10.00-beta`
|
||||||
- **Stable `02.11.00`** also creates: `02.11.00-dev`, `02.11.00-alpha`, `02.11.00-beta`, `02.11.00-rc`
|
- **Stable `02.11.00`** also creates: `02.11.00-dev`, `02.11.00-alpha`, `02.11.00-beta`, `02.11.00-rc`
|
||||||
|
|
||||||
This ensures Joomla sites on ANY stability channel see the update (Joomla only shows versions higher than what's installed).
|
This ensures Joomla sites on ANY stability channel see the update (Joomla only shows versions higher than what's installed).
|
||||||
|
|
||||||
### Version files
|
### Version files
|
||||||
|
|
||||||
The version tools update all files containing version stamps:
|
The version tools update all files containing version stamps:
|
||||||
|
|
||||||
- `.mokogitea/manifest.xml` (canonical source)
|
- `.mokogitea/manifest.xml` (canonical source)
|
||||||
- Joomla XML manifests (`<version>` tag)
|
- Joomla XML manifests (`<version>` tag)
|
||||||
- `README.md`, `CHANGELOG.md` (`VERSION:` pattern)
|
- `README.md`, `CHANGELOG.md` (`VERSION:` pattern)
|
||||||
- `package.json`, `pyproject.toml`
|
- `package.json`, `pyproject.toml`
|
||||||
- Any text file with a `VERSION: XX.YY.ZZ` label
|
- Any text file with a `VERSION: XX.YY.ZZ` label
|
||||||
|
|
||||||
Files synced from other repos (with a `# REPO:` header) are not touched.
|
Files synced from other repos (with a `# REPO:` header) are not touched.
|
||||||
|
|
||||||
## Code Standards
|
## Code Standards
|
||||||
|
|
||||||
- **PHP**: PSR-12, tabs for indentation
|
- **PHP**: PSR-12, tabs for indentation
|
||||||
- **Copyright**: all files must include the Moko Consulting copyright header
|
- **Copyright**: all files must include the Moko Consulting copyright header
|
||||||
- **License**: SPDX identifier `GPL-3.0-or-later` (or as specified per repo)
|
- **License**: SPDX identifier `GPL-3.0-or-later` (or as specified per repo)
|
||||||
- **Attribution**: use `Authored-by: Moko Consulting` in commits, not individual names
|
- **Attribution**: use `Authored-by: Moko Consulting` in commits, not individual names
|
||||||
|
|
||||||
## Commit Messages
|
## Commit Messages
|
||||||
|
|
||||||
Use conventional commit format:
|
Use conventional commit format:
|
||||||
|
|
||||||
```
|
```
|
||||||
type(scope): short description
|
type(scope): short description
|
||||||
|
|
||||||
Optional body with context.
|
Optional body with context.
|
||||||
|
|
||||||
Authored-by: Moko Consulting
|
Authored-by: Moko Consulting
|
||||||
```
|
```
|
||||||
|
|
||||||
Types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `ci`
|
Types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `ci`
|
||||||
|
|
||||||
Special flags in commit messages:
|
Special flags in commit messages:
|
||||||
- `[skip ci]` — skip all CI workflows
|
- `[skip ci]` — skip all CI workflows
|
||||||
- `[skip bump]` — skip auto version bump only
|
- `[skip bump]` — skip auto version bump only
|
||||||
|
|
||||||
## Reporting Issues
|
## Reporting Issues
|
||||||
|
|
||||||
Use the repository's issue tracker with the appropriate template.
|
Use the repository's issue tracker with the appropriate template.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Moko Consulting <hello@mokoconsulting.tech>*
|
*Moko Consulting <hello@mokoconsulting.tech>*
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ DEFGROUP: MokoPlatform.Root
|
|||||||
INGROUP: MokoPlatform
|
INGROUP: MokoPlatform
|
||||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
PATH: /README.md
|
PATH: /README.md
|
||||||
VERSION: 09.32.01
|
VERSION: 09.26.02
|
||||||
BRIEF: Project overview and documentation
|
BRIEF: Project overview and documentation
|
||||||
-->
|
-->
|
||||||
|
|
||||||
@@ -16,8 +16,8 @@ BRIEF: Project overview and documentation
|
|||||||
|
|
||||||
PHP implementation of mokoplatform — enterprise standards, automation framework, workflow templates, and bulk sync tooling.
|
PHP implementation of mokoplatform — enterprise standards, automation framework, workflow templates, and bulk sync tooling.
|
||||||
|
|
||||||
> **Primary platform**: [Gitea — git.mokoconsulting.tech](https://git.mokoconsulting.tech/MokoConsulting/MokoCli-API)
|
> **Primary platform**: [Gitea — git.mokoconsulting.tech](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API)
|
||||||
> **Backup mirror**: [GitHub](https://github.com/MokoConsulting/MokoCli-API) *(read-only mirror)*
|
> **Backup mirror**: [GitHub](https://github.com/MokoConsulting/MokoStandards-API) *(read-only mirror)*
|
||||||
|
|
||||||
## What Lives Here
|
## What Lives Here
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ declare(strict_types=1);
|
|||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\{
|
use MokoEnterprise\{
|
||||||
AuditLogger,
|
AuditLogger,
|
||||||
CliFramework,
|
CliFramework,
|
||||||
Config,
|
Config,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ declare(strict_types=1);
|
|||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\{
|
use MokoEnterprise\{
|
||||||
ApiClient,
|
ApiClient,
|
||||||
AuditLogger,
|
AuditLogger,
|
||||||
CheckpointManager,
|
CheckpointManager,
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ declare(strict_types=1);
|
|||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
use MokoCli\ManifestParser;
|
use MokoEnterprise\MokoStandardsParser;
|
||||||
|
|
||||||
class EnrichManifestXmlCli extends CliFramework
|
class EnrichManifestXmlCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -43,7 +43,7 @@ class EnrichManifestXmlCli extends CliFramework
|
|||||||
$skipStr = $this->getArgument('--skip');
|
$skipStr = $this->getArgument('--skip');
|
||||||
$skipRepos = $skipStr !== '' ? array_map('trim', explode(',', $skipStr)) : [];
|
$skipRepos = $skipStr !== '' ? array_map('trim', explode(',', $skipStr)) : [];
|
||||||
|
|
||||||
$parser = new ManifestParser();
|
$parser = new MokoStandardsParser();
|
||||||
$tmpBase = sys_get_temp_dir() . '/moko-enrich-' . getmypid();
|
$tmpBase = sys_get_temp_dir() . '/moko-enrich-' . getmypid();
|
||||||
|
|
||||||
echo "=== mokoplatform XML Manifest Enrichment ===\n";
|
echo "=== mokoplatform XML Manifest Enrichment ===\n";
|
||||||
@@ -113,8 +113,8 @@ class EnrichManifestXmlCli extends CliFramework
|
|||||||
}
|
}
|
||||||
$enrichment['build']['language'] = $enrichment['build']['language']
|
$enrichment['build']['language'] = $enrichment['build']['language']
|
||||||
?? $repo['language']
|
?? $repo['language']
|
||||||
?? ManifestParser::platformLanguage($platform);
|
?? MokoStandardsParser::platformLanguage($platform);
|
||||||
$enrichment['build']['package_type'] = $enrichment['build']['package_type'] ?? ManifestParser::platformPackageType($platform);
|
$enrichment['build']['package_type'] = $enrichment['build']['package_type'] ?? MokoStandardsParser::platformPackageType($platform);
|
||||||
|
|
||||||
$enrichedXml = $this->enrichManifestXml($existingXml, $enrichment);
|
$enrichedXml = $this->enrichManifestXml($existingXml, $enrichment);
|
||||||
$dc = count($enrichment['deploy'] ?? []);
|
$dc = count($enrichment['deploy'] ?? []);
|
||||||
@@ -312,7 +312,7 @@ class EnrichManifestXmlCli extends CliFramework
|
|||||||
return $xml;
|
return $xml;
|
||||||
}
|
}
|
||||||
|
|
||||||
$ns = ManifestParser::NAMESPACE_URI;
|
$ns = MokoStandardsParser::NAMESPACE_URI;
|
||||||
$root = $dom->documentElement;
|
$root = $dom->documentElement;
|
||||||
|
|
||||||
foreach (['build', 'deploy', 'scripts'] as $tag) {
|
foreach (['build', 'deploy', 'scripts'] as $tag) {
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ declare(strict_types=1);
|
|||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
use MokoCli\ManifestParser;
|
use MokoEnterprise\MokoStandardsParser;
|
||||||
|
|
||||||
class EnrichMokostandardsXmlCli extends CliFramework
|
class EnrichMokostandardsXmlCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -43,7 +43,7 @@ class EnrichMokostandardsXmlCli extends CliFramework
|
|||||||
$skipStr = $this->getArgument('--skip');
|
$skipStr = $this->getArgument('--skip');
|
||||||
$skipRepos = $skipStr !== '' ? array_map('trim', explode(',', $skipStr)) : [];
|
$skipRepos = $skipStr !== '' ? array_map('trim', explode(',', $skipStr)) : [];
|
||||||
|
|
||||||
$parser = new ManifestParser();
|
$parser = new MokoStandardsParser();
|
||||||
$tmpBase = sys_get_temp_dir() . '/moko-enrich-' . getmypid();
|
$tmpBase = sys_get_temp_dir() . '/moko-enrich-' . getmypid();
|
||||||
|
|
||||||
echo "=== mokoplatform XML Manifest Enrichment ===\n";
|
echo "=== mokoplatform XML Manifest Enrichment ===\n";
|
||||||
@@ -113,8 +113,8 @@ class EnrichMokostandardsXmlCli extends CliFramework
|
|||||||
}
|
}
|
||||||
$enrichment['build']['language'] = $enrichment['build']['language']
|
$enrichment['build']['language'] = $enrichment['build']['language']
|
||||||
?? $repo['language']
|
?? $repo['language']
|
||||||
?? ManifestParser::platformLanguage($platform);
|
?? MokoStandardsParser::platformLanguage($platform);
|
||||||
$enrichment['build']['package_type'] = $enrichment['build']['package_type'] ?? ManifestParser::platformPackageType($platform);
|
$enrichment['build']['package_type'] = $enrichment['build']['package_type'] ?? MokoStandardsParser::platformPackageType($platform);
|
||||||
|
|
||||||
$enrichedXml = $this->enrichManifestXml($existingXml, $enrichment);
|
$enrichedXml = $this->enrichManifestXml($existingXml, $enrichment);
|
||||||
$dc = count($enrichment['deploy'] ?? []);
|
$dc = count($enrichment['deploy'] ?? []);
|
||||||
@@ -315,7 +315,7 @@ class EnrichMokostandardsXmlCli extends CliFramework
|
|||||||
return $xml;
|
return $xml;
|
||||||
}
|
}
|
||||||
|
|
||||||
$ns = ManifestParser::NAMESPACE_URI;
|
$ns = MokoStandardsParser::NAMESPACE_URI;
|
||||||
$root = $dom->documentElement;
|
$root = $dom->documentElement;
|
||||||
|
|
||||||
foreach (['build', 'deploy', 'scripts'] as $tag) {
|
foreach (['build', 'deploy', 'scripts'] as $tag) {
|
||||||
|
|||||||
@@ -25,12 +25,12 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
use MokoCli\CheckpointManager;
|
use MokoEnterprise\CheckpointManager;
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
use MokoCli\Config;
|
use MokoEnterprise\Config;
|
||||||
use MokoCli\PlatformAdapterFactory;
|
use MokoEnterprise\PlatformAdapterFactory;
|
||||||
use MokoCli\GitHubAdapter;
|
use MokoEnterprise\GitHubAdapter;
|
||||||
use MokoCli\MokoGiteaAdapter;
|
use MokoEnterprise\MokoGiteaAdapter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gitea Migration Script
|
* Gitea Migration Script
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ declare(strict_types=1);
|
|||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\{
|
use MokoEnterprise\{
|
||||||
ApiClient,
|
ApiClient,
|
||||||
AuditLogger,
|
AuditLogger,
|
||||||
CliFramework,
|
CliFramework,
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ declare(strict_types=1);
|
|||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
use MokoCli\ManifestParser;
|
use MokoEnterprise\MokoStandardsParser;
|
||||||
|
|
||||||
class PushManifestXmlCli extends CliFramework
|
class PushManifestXmlCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -44,7 +44,7 @@ class PushManifestXmlCli extends CliFramework
|
|||||||
$skipStr = $this->getArgument('--skip');
|
$skipStr = $this->getArgument('--skip');
|
||||||
$skipRepos = $skipStr !== '' ? array_map('trim', explode(',', $skipStr)) : [];
|
$skipRepos = $skipStr !== '' ? array_map('trim', explode(',', $skipStr)) : [];
|
||||||
|
|
||||||
$parser = new ManifestParser();
|
$parser = new MokoStandardsParser();
|
||||||
$tmpBase = sys_get_temp_dir() . '/moko-manifest-push-' . getmypid();
|
$tmpBase = sys_get_temp_dir() . '/moko-manifest-push-' . getmypid();
|
||||||
|
|
||||||
echo "=== mokoplatform XML Manifest Push ===\n";
|
echo "=== mokoplatform XML Manifest Push ===\n";
|
||||||
@@ -97,8 +97,8 @@ class PushManifestXmlCli extends CliFramework
|
|||||||
'description' => $repo['description'] ?? '',
|
'description' => $repo['description'] ?? '',
|
||||||
'license' => 'GPL-3.0-or-later',
|
'license' => 'GPL-3.0-or-later',
|
||||||
'topics' => $repo['topics'] ?? [],
|
'topics' => $repo['topics'] ?? [],
|
||||||
'language' => $repo['language'] ?? ManifestParser::platformLanguage($platform),
|
'language' => $repo['language'] ?? MokoStandardsParser::platformLanguage($platform),
|
||||||
'package_type' => ManifestParser::platformPackageType($platform),
|
'package_type' => MokoStandardsParser::platformPackageType($platform),
|
||||||
'last_synced' => date('c'),
|
'last_synced' => date('c'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ declare(strict_types=1);
|
|||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
use MokoCli\ManifestParser;
|
use MokoEnterprise\MokoStandardsParser;
|
||||||
|
|
||||||
class PushMokostandardsXmlCli extends CliFramework
|
class PushMokostandardsXmlCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -44,7 +44,7 @@ class PushMokostandardsXmlCli extends CliFramework
|
|||||||
$skipStr = $this->getArgument('--skip');
|
$skipStr = $this->getArgument('--skip');
|
||||||
$skipRepos = $skipStr !== '' ? array_map('trim', explode(',', $skipStr)) : [];
|
$skipRepos = $skipStr !== '' ? array_map('trim', explode(',', $skipStr)) : [];
|
||||||
|
|
||||||
$parser = new ManifestParser();
|
$parser = new MokoStandardsParser();
|
||||||
$tmpBase = sys_get_temp_dir() . '/moko-manifest-push-' . getmypid();
|
$tmpBase = sys_get_temp_dir() . '/moko-manifest-push-' . getmypid();
|
||||||
|
|
||||||
echo "=== mokoplatform XML Manifest Push ===\n";
|
echo "=== mokoplatform XML Manifest Push ===\n";
|
||||||
@@ -97,8 +97,8 @@ class PushMokostandardsXmlCli extends CliFramework
|
|||||||
'description' => $repo['description'] ?? '',
|
'description' => $repo['description'] ?? '',
|
||||||
'license' => 'GPL-3.0-or-later',
|
'license' => 'GPL-3.0-or-later',
|
||||||
'topics' => $repo['topics'] ?? [],
|
'topics' => $repo['topics'] ?? [],
|
||||||
'language' => $repo['language'] ?? ManifestParser::platformLanguage($platform),
|
'language' => $repo['language'] ?? MokoStandardsParser::platformLanguage($platform),
|
||||||
'package_type' => ManifestParser::platformPackageType($platform),
|
'package_type' => MokoStandardsParser::platformPackageType($platform),
|
||||||
'last_synced' => date('c'),
|
'last_synced' => date('c'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ declare(strict_types=1);
|
|||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\{ApiClient, AuditLogger, CliFramework, Config, GitPlatformAdapter, MetricsCollector, PlatformAdapterFactory};
|
use MokoEnterprise\{ApiClient, AuditLogger, CliFramework, Config, GitPlatformAdapter, MetricsCollector, PlatformAdapterFactory};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enterprise Repository Cleanup
|
* Enterprise Repository Cleanup
|
||||||
|
|||||||
@@ -9,11 +9,11 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: MokoCli.CLI
|
* DEFGROUP: MokoStandards.CLI
|
||||||
* INGROUP: MokoCli
|
* INGROUP: MokoStandards
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
* PATH: /bin/moko
|
* PATH: /bin/moko
|
||||||
* BRIEF: Unified CLI dispatcher — run any MokoCli script without needing GitHub Actions
|
* BRIEF: Unified CLI dispatcher — run any MokoStandards script without needing GitHub Actions
|
||||||
*
|
*
|
||||||
* USAGE
|
* USAGE
|
||||||
* php bin/moko <command> [options] (all platforms)
|
* php bin/moko <command> [options] (all platforms)
|
||||||
@@ -220,9 +220,6 @@ const COMMAND_MAP = [
|
|||||||
// Licensing
|
// Licensing
|
||||||
'license' => 'cli/license_manage.php',
|
'license' => 'cli/license_manage.php',
|
||||||
|
|
||||||
// Security
|
|
||||||
'security:advisories' => 'security/advisory_scan.php',
|
|
||||||
|
|
||||||
// Shell completion
|
// Shell completion
|
||||||
'completion' => 'cli/completion.php',
|
'completion' => 'cli/completion.php',
|
||||||
|
|
||||||
@@ -295,10 +292,10 @@ function printHelp(): void
|
|||||||
{
|
{
|
||||||
echo <<<'HELP'
|
echo <<<'HELP'
|
||||||
╔══════════════════════════════════════════════════════════╗
|
╔══════════════════════════════════════════════════════════╗
|
||||||
║ MokoCli CLI (bin/moko) ║
|
║ MokoStandards CLI (bin/moko) ║
|
||||||
╚══════════════════════════════════════════════════════════╝
|
╚══════════════════════════════════════════════════════════╝
|
||||||
|
|
||||||
Run any MokoCli script locally without GitHub Actions.
|
Run any MokoStandards script locally without GitHub Actions.
|
||||||
|
|
||||||
USAGE
|
USAGE
|
||||||
php bin/moko <command> [options] (all platforms)
|
php bin/moko <command> [options] (all platforms)
|
||||||
@@ -400,7 +397,7 @@ function loadPluginCommands(): array
|
|||||||
$commands = [];
|
$commands = [];
|
||||||
|
|
||||||
foreach (glob("{$pluginDir}/*Plugin.php") as $file) {
|
foreach (glob("{$pluginDir}/*Plugin.php") as $file) {
|
||||||
$className = 'MokoCli\\Plugins\\'
|
$className = 'MokoEnterprise\\Plugins\\'
|
||||||
. pathinfo($file, PATHINFO_FILENAME);
|
. pathinfo($file, PATHINFO_FILENAME);
|
||||||
|
|
||||||
if (!class_exists($className)) {
|
if (!class_exists($className)) {
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ declare(strict_types=1);
|
|||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
use MokoCli\Config;
|
use MokoEnterprise\Config;
|
||||||
use MokoCli\PlatformAdapterFactory;
|
use MokoEnterprise\PlatformAdapterFactory;
|
||||||
|
|
||||||
class ArchiveRepoCli extends CliFramework
|
class ArchiveRepoCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
+1
-1
@@ -25,7 +25,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CLI tool to search, filter, and export audit logs.
|
* CLI tool to search, filter, and export audit logs.
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
class BadgeUpdateCli extends CliFramework
|
class BadgeUpdateCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* INGROUP: mokoplatform
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/branch_rename.php
|
* PATH: /cli/branch_rename.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.26.02
|
||||||
* BRIEF: Rename a git branch via Gitea API (create new, update PR, delete old)
|
* BRIEF: Rename a git branch via Gitea API (create new, update PR, delete old)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
class BranchRenameCli extends CliFramework
|
class BranchRenameCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* INGROUP: mokoplatform
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/bulk_workflow_push.php
|
* PATH: /cli/bulk_workflow_push.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.26.02
|
||||||
* BRIEF: Push a workflow file to all governed repos via the Gitea Contents API
|
* BRIEF: Push a workflow file to all governed repos via the Gitea Contents API
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
class BulkWorkflowPushCli extends CliFramework
|
class BulkWorkflowPushCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* INGROUP: mokoplatform
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/bulk_workflow_trigger.php
|
* PATH: /cli/bulk_workflow_trigger.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.26.02
|
||||||
* BRIEF: Trigger a workflow across multiple repos at once
|
* BRIEF: Trigger a workflow across multiple repos at once
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
class BulkWorkflowTriggerCli extends CliFramework
|
class BulkWorkflowTriggerCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
class ChangelogPromoteCli extends CliFramework
|
class ChangelogPromoteCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
class ChangelogPruneCli extends CliFramework
|
class ChangelogPruneCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* INGROUP: mokoplatform
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/client_dashboard.php
|
* PATH: /cli/client_dashboard.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.26.02
|
||||||
* BRIEF: Generate unified client dashboard HTML
|
* BRIEF: Generate unified client dashboard HTML
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
class ClientDashboardCli extends CliFramework
|
class ClientDashboardCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
class ClientHealthCheckCli extends CliFramework
|
class ClientHealthCheckCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* INGROUP: mokoplatform
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/client_inventory.php
|
* PATH: /cli/client_inventory.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.26.02
|
||||||
* BRIEF: Discover and list all client-waas repos with their server configuration status
|
* BRIEF: Discover and list all client-waas repos with their server configuration status
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
class ClientInventoryCli extends CliFramework
|
class ClientInventoryCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* INGROUP: mokoplatform
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/client_provision.php
|
* PATH: /cli/client_provision.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.26.02
|
||||||
* BRIEF: Provision a new client environment end-to-end
|
* BRIEF: Provision a new client environment end-to-end
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
class ClientProvisionCli extends CliFramework
|
class ClientProvisionCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
class CompletionCli extends CliFramework
|
class CompletionCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
class CreateProjectCli extends CliFramework
|
class CreateProjectCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -80,10 +80,10 @@ class CreateProjectCli extends CliFramework
|
|||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
$config = \MokoCli\Config::load();
|
$config = \MokoEnterprise\Config::load();
|
||||||
$platformName = $config->getString('platform', 'gitea');
|
$platformName = $config->getString('platform', 'gitea');
|
||||||
try {
|
try {
|
||||||
$adapter = \MokoCli\PlatformAdapterFactory::create($config);
|
$adapter = \MokoEnterprise\PlatformAdapterFactory::create($config);
|
||||||
$api = $adapter->getApiClient();
|
$api = $adapter->getApiClient();
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->log('ERROR', "Platform initialization failed: " . $e->getMessage());
|
$this->log('ERROR', "Platform initialization failed: " . $e->getMessage());
|
||||||
@@ -205,7 +205,7 @@ class CreateProjectCli extends CliFramework
|
|||||||
return $data['data'] ?? [];
|
return $data['data'] ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function restGet(string $path, string $token, ?\MokoCli\ApiClient $apiClient = null): array
|
private function restGet(string $path, string $token, ?\MokoEnterprise\ApiClient $apiClient = null): array
|
||||||
{
|
{
|
||||||
if ($apiClient !== null) {
|
if ($apiClient !== null) {
|
||||||
try {
|
try {
|
||||||
@@ -217,7 +217,7 @@ class CreateProjectCli extends CliFramework
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function detectRepoPlatform(string $org, string $repo, string $token, ?\MokoCli\ApiClient $apiClient = null): string
|
private function detectRepoPlatform(string $org, string $repo, string $token, ?\MokoEnterprise\ApiClient $apiClient = null): string
|
||||||
{
|
{
|
||||||
foreach (['.github/.mokostandards', '.mokogitea/.mokostandards', '.mokostandards'] as $path) {
|
foreach (['.github/.mokostandards', '.mokogitea/.mokostandards', '.mokostandards'] as $path) {
|
||||||
$data = $this->restGet("repos/{$org}/{$repo}/contents/{$path}", $token, $apiClient);
|
$data = $this->restGet("repos/{$org}/{$repo}/contents/{$path}", $token, $apiClient);
|
||||||
|
|||||||
+4
-4
@@ -20,9 +20,9 @@ declare(strict_types=1);
|
|||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
use MokoCli\Config;
|
use MokoEnterprise\Config;
|
||||||
use MokoCli\PlatformAdapterFactory;
|
use MokoEnterprise\PlatformAdapterFactory;
|
||||||
|
|
||||||
class CreateRepoCli extends CliFramework
|
class CreateRepoCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -138,7 +138,7 @@ class CreateRepoCli extends CliFramework
|
|||||||
echo "Step 4: Creating README.md...\n";
|
echo "Step 4: Creating README.md...\n";
|
||||||
$baseUrl = $platformName === 'gitea' ? $config->getString('gitea.url', 'https://git.mokoconsulting.tech') : 'https://github.com';
|
$baseUrl = $platformName === 'gitea' ? $config->getString('gitea.url', 'https://git.mokoconsulting.tech') : 'https://github.com';
|
||||||
$repoUrl = "{$baseUrl}/{$org}/{$name}";
|
$repoUrl = "{$baseUrl}/{$org}/{$name}";
|
||||||
$standardsUrl = "{$baseUrl}/{$org}/MokoCli";
|
$standardsUrl = "{$baseUrl}/{$org}/MokoStandards";
|
||||||
$readmeContent = "<!--\n"
|
$readmeContent = "<!--\n"
|
||||||
. "Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>\n"
|
. "Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>\n"
|
||||||
. "SPDX-License-Identifier: GPL-3.0-or-later\n"
|
. "SPDX-License-Identifier: GPL-3.0-or-later\n"
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ require_once __DIR__ . '/../vendor/autoload.php';
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\{CliFramework, SourceResolver};
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
use phpseclib3\Net\SFTP;
|
use phpseclib3\Net\SFTP;
|
||||||
use phpseclib3\Crypt\PublicKeyLoader;
|
use phpseclib3\Crypt\PublicKeyLoader;
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
class DevBranchResetCli extends CliFramework
|
class DevBranchResetCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* INGROUP: mokoplatform
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/grafana_dashboard.php
|
* PATH: /cli/grafana_dashboard.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.26.02
|
||||||
* BRIEF: Manage Grafana dashboards via API
|
* BRIEF: Manage Grafana dashboards via API
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
class GrafanaDashboardCli extends CliFramework
|
class GrafanaDashboardCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* INGROUP: mokoplatform
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/joomla_build.php
|
* PATH: /cli/joomla_build.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.26.02
|
||||||
* BRIEF: Build a Joomla extension ZIP from manifest — all types supported
|
* BRIEF: Build a Joomla extension ZIP from manifest — all types supported
|
||||||
* NOTE: Called by pre-release and auto-release workflows.
|
* NOTE: Called by pre-release and auto-release workflows.
|
||||||
*/
|
*/
|
||||||
@@ -19,7 +19,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\{CliFramework, SourceResolver};
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class JoomlaBuildCli extends CliFramework
|
class JoomlaBuildCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
class JoomlaCompatCheckCli extends CliFramework
|
class JoomlaCompatCheckCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,507 +0,0 @@
|
|||||||
#!/usr/bin/env php
|
|
||||||
<?php
|
|
||||||
|
|
||||||
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* FILE INFORMATION
|
|
||||||
* DEFGROUP: mokoplatform.CLI
|
|
||||||
* INGROUP: mokoplatform
|
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
|
||||||
* PATH: /cli/joomla_metadata_validate.php
|
|
||||||
* VERSION: 09.32.01
|
|
||||||
* BRIEF: Validate MokoGitea repo metadata against Joomla extension manifest XML
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
|
||||||
|
|
||||||
class JoomlaMetadataValidateCli extends CliFramework
|
|
||||||
{
|
|
||||||
/** Joomla element prefix map — must match MokoGitea's cleanJoomlaElement() */
|
|
||||||
private const JOOMLA_PREFIX = [
|
|
||||||
'package' => 'pkg_',
|
|
||||||
'component' => 'com_',
|
|
||||||
'module' => 'mod_',
|
|
||||||
'template' => 'tpl_',
|
|
||||||
'library' => 'lib_',
|
|
||||||
'file' => 'file_',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected function configure(): void
|
|
||||||
{
|
|
||||||
$this->setDescription('Validate MokoGitea repo metadata against Joomla extension manifest XML');
|
|
||||||
$this->addArgument('--path', 'Repo root path (default: current directory)', '.');
|
|
||||||
$this->addArgument('--token', 'Gitea API token (or GITEA_TOKEN env)', '');
|
|
||||||
$this->addArgument('--org', 'Gitea org', 'MokoConsulting');
|
|
||||||
$this->addArgument('--repo', 'Repo name (auto-detected from git if empty)', '');
|
|
||||||
$this->addArgument('--api-base', 'Gitea API base URL', 'https://git.mokoconsulting.tech/api/v1');
|
|
||||||
$this->addArgument('--ci', 'CI mode: exit 1 on any error', false);
|
|
||||||
$this->addArgument('--json', 'Output as JSON', false);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function run(): int
|
|
||||||
{
|
|
||||||
$path = realpath($this->getArgument('--path')) ?: $this->getArgument('--path');
|
|
||||||
$token = $this->getArgument('--token') ?: getenv('GITEA_TOKEN') ?: '';
|
|
||||||
$org = $this->getArgument('--org');
|
|
||||||
$repoName = $this->getArgument('--repo');
|
|
||||||
$apiBase = rtrim($this->getArgument('--api-base'), '/');
|
|
||||||
$ciMode = (bool) $this->getArgument('--ci');
|
|
||||||
$jsonMode = (bool) $this->getArgument('--json');
|
|
||||||
|
|
||||||
if (!is_dir($path)) {
|
|
||||||
$this->log('ERROR', "Path does not exist: {$path}");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($repoName === '') {
|
|
||||||
$repoName = $this->detectRepoName($path);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Step 1: Find the Joomla extension manifest XML ──────────
|
|
||||||
$joomlaXml = $this->findJoomlaManifest($path);
|
|
||||||
|
|
||||||
if ($joomlaXml === null) {
|
|
||||||
$this->log('ERROR', 'No Joomla extension manifest XML found');
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->log('INFO', "Joomla manifest: {$joomlaXml['path']}");
|
|
||||||
|
|
||||||
// ── Step 2: Load MokoGitea metadata ─────────────────────────
|
|
||||||
$metadata = $this->loadMetadata($path, $org, $repoName, $token, $apiBase);
|
|
||||||
|
|
||||||
if ($metadata === null) {
|
|
||||||
$this->log('ERROR', 'Could not load MokoGitea metadata');
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Step 3: Compare ─────────────────────────────────────────
|
|
||||||
$results = $this->compare($metadata, $joomlaXml, $path);
|
|
||||||
|
|
||||||
// ── Step 4: Output ──────────────────────────────────────────
|
|
||||||
if ($jsonMode) {
|
|
||||||
echo json_encode([
|
|
||||||
'repo' => $repoName,
|
|
||||||
'results' => $results,
|
|
||||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
|
||||||
} else {
|
|
||||||
$this->printResults($repoName, $results);
|
|
||||||
}
|
|
||||||
|
|
||||||
$errors = count(array_filter($results, fn($r) => $r['status'] === 'error'));
|
|
||||||
|
|
||||||
return ($ciMode && $errors > 0) ? 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =================================================================
|
|
||||||
// Find Joomla manifest XML
|
|
||||||
// =================================================================
|
|
||||||
|
|
||||||
private function findJoomlaManifest(string $root): ?array
|
|
||||||
{
|
|
||||||
// Search common locations for a Joomla extension manifest
|
|
||||||
$candidates = [];
|
|
||||||
|
|
||||||
// Package manifest: source/pkg_*.xml
|
|
||||||
foreach (glob("{$root}/source/pkg_*.xml") as $file) {
|
|
||||||
$candidates[] = $file;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Component manifest: source/packages/com_*/[name].xml
|
|
||||||
foreach (glob("{$root}/source/packages/com_*/*.xml") as $file) {
|
|
||||||
$basename = basename($file);
|
|
||||||
// Skip access.xml, config.xml, etc.
|
|
||||||
if (in_array($basename, ['access.xml', 'config.xml'], true)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$candidates[] = $file;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Direct source/*.xml
|
|
||||||
foreach (glob("{$root}/source/*.xml") as $file) {
|
|
||||||
if (basename($file) !== 'pkg_mokosuitebackup.xml') {
|
|
||||||
// Already caught above
|
|
||||||
}
|
|
||||||
$candidates[] = $file;
|
|
||||||
}
|
|
||||||
|
|
||||||
// src/ fallback
|
|
||||||
foreach (glob("{$root}/src/pkg_*.xml") as $file) {
|
|
||||||
$candidates[] = $file;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the first one that has <extension type="...">
|
|
||||||
foreach (array_unique($candidates) as $file) {
|
|
||||||
$content = file_get_contents($file);
|
|
||||||
if ($content === false) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preg_match('/<extension\s[^>]*type=["\']([^"\']+)["\']/', $content, $typeMatch)) {
|
|
||||||
$xml = @simplexml_load_string($content);
|
|
||||||
if ($xml === false) {
|
|
||||||
$relPath = str_replace($root . '/', '', $file);
|
|
||||||
$relPath = str_replace($root . '\\', '', $relPath);
|
|
||||||
$this->log('WARN', "Skipping {$relPath}: malformed XML");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$type = strtolower($typeMatch[1]);
|
|
||||||
$relPath = str_replace($root . '/', '', $file);
|
|
||||||
$relPath = str_replace($root . '\\', '', $relPath);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'path' => $relPath,
|
|
||||||
'type' => $type,
|
|
||||||
'xml' => $xml,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =================================================================
|
|
||||||
// Load metadata (from API)
|
|
||||||
// =================================================================
|
|
||||||
|
|
||||||
private function loadMetadata(string $root, string $org, string $repoName, string $token, string $apiBase): ?array
|
|
||||||
{
|
|
||||||
if ($token === '') {
|
|
||||||
$this->log('ERROR', 'No API token provided (use --token or set GITEA_TOKEN env var)');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$url = "{$apiBase}/repos/{$org}/{$repoName}/metadata";
|
|
||||||
$ctx = stream_context_create([
|
|
||||||
'http' => [
|
|
||||||
'header' => "Authorization: token {$token}\r\nAccept: application/json\r\n",
|
|
||||||
'timeout' => 10,
|
|
||||||
'ignore_errors' => true,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$body = file_get_contents($url, false, $ctx);
|
|
||||||
|
|
||||||
// Extract HTTP status from response headers
|
|
||||||
$httpCode = 0;
|
|
||||||
if (isset($http_response_header[0]) && preg_match('/\d{3}/', $http_response_header[0], $m)) {
|
|
||||||
$httpCode = (int) $m[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($body === false) {
|
|
||||||
$this->log('ERROR', "Failed to connect to {$url} — check network or TLS configuration");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($httpCode === 404) {
|
|
||||||
$this->log('ERROR', "API endpoint not found: {$url}");
|
|
||||||
$this->log('ERROR', 'Server may need MokoGitea-Fork >= #650 (metadata endpoint rename)');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($httpCode === 401 || $httpCode === 403) {
|
|
||||||
$this->log('ERROR', "Authentication failed (HTTP {$httpCode}) — check your API token");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($httpCode >= 400) {
|
|
||||||
$this->log('ERROR', "API returned HTTP {$httpCode}: " . substr($body, 0, 200));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = json_decode($body, true);
|
|
||||||
if (!is_array($data)) {
|
|
||||||
$this->log('ERROR', "API returned invalid JSON from {$url}");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$data['source'] = 'api';
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =================================================================
|
|
||||||
// Compare metadata against Joomla manifest
|
|
||||||
// =================================================================
|
|
||||||
|
|
||||||
private function compare(array $metadata, array $joomlaXml, string $root): array
|
|
||||||
{
|
|
||||||
$results = [];
|
|
||||||
$xml = $joomlaXml['xml'];
|
|
||||||
$type = $joomlaXml['type'];
|
|
||||||
|
|
||||||
// 1. Extension type
|
|
||||||
$metaType = $this->normalizeExtensionType(
|
|
||||||
$metadata['extension_type'] ?? $metadata['package_type'] ?? ''
|
|
||||||
);
|
|
||||||
$results[] = [
|
|
||||||
'field' => 'extension_type',
|
|
||||||
'metadata' => $metaType,
|
|
||||||
'joomla' => $type,
|
|
||||||
'status' => ($metaType === $type) ? 'ok' : 'error',
|
|
||||||
'message' => ($metaType === $type)
|
|
||||||
? "matches <extension type=\"{$type}\">"
|
|
||||||
: "metadata has \"{$metaType}\" but Joomla manifest has \"{$type}\"",
|
|
||||||
];
|
|
||||||
|
|
||||||
// 2. Element name
|
|
||||||
$metaName = strtolower($metadata['name'] ?? '');
|
|
||||||
$metaElement = $this->deriveElement($metaType, $metaName);
|
|
||||||
$joomlaElement = $this->extractJoomlaElement($xml, $type);
|
|
||||||
|
|
||||||
$elementMatch = ($metaElement === $joomlaElement);
|
|
||||||
$results[] = [
|
|
||||||
'field' => 'element',
|
|
||||||
'metadata' => $metaElement,
|
|
||||||
'joomla' => $joomlaElement,
|
|
||||||
'status' => $elementMatch ? 'ok' : 'error',
|
|
||||||
'message' => $elementMatch
|
|
||||||
? "derived correctly"
|
|
||||||
: "metadata derives \"{$metaElement}\" but Joomla uses \"{$joomlaElement}\"",
|
|
||||||
];
|
|
||||||
|
|
||||||
// 3. Version
|
|
||||||
$metaVersion = $metadata['version'] ?? '';
|
|
||||||
$joomlaVersion = (string) ($xml->version ?? '');
|
|
||||||
|
|
||||||
if ($metaVersion !== '' && $joomlaVersion !== '') {
|
|
||||||
// Strip dev/rc suffixes for comparison (CI bumps these)
|
|
||||||
$metaBase = preg_replace('/-(dev|rc|alpha|beta)\d*$/', '', $metaVersion);
|
|
||||||
$joomlaBase = preg_replace('/-(dev|rc|alpha|beta)\d*$/', '', $joomlaVersion);
|
|
||||||
$versionMatch = ($metaBase === $joomlaBase);
|
|
||||||
|
|
||||||
$results[] = [
|
|
||||||
'field' => 'version',
|
|
||||||
'metadata' => $metaVersion,
|
|
||||||
'joomla' => $joomlaVersion,
|
|
||||||
'status' => $versionMatch ? 'ok' : 'warn',
|
|
||||||
'message' => $versionMatch
|
|
||||||
? 'matches (base version)'
|
|
||||||
: "metadata has \"{$metaVersion}\" but Joomla has \"{$joomlaVersion}\"",
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. PHP minimum (from composer.json)
|
|
||||||
$composerPhp = $this->readComposerPhpRequirement($root);
|
|
||||||
$metaPhp = $metadata['php_minimum'] ?? '';
|
|
||||||
|
|
||||||
if ($composerPhp !== '' && $metaPhp !== '') {
|
|
||||||
$phpMatch = ($metaPhp === $composerPhp);
|
|
||||||
$results[] = [
|
|
||||||
'field' => 'php_minimum',
|
|
||||||
'metadata' => $metaPhp,
|
|
||||||
'joomla' => $composerPhp . ' (composer.json)',
|
|
||||||
'status' => $phpMatch ? 'ok' : 'warn',
|
|
||||||
'message' => $phpMatch
|
|
||||||
? 'matches composer.json'
|
|
||||||
: "metadata has \"{$metaPhp}\" but composer.json requires \"{$composerPhp}\"",
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Description
|
|
||||||
$metaDesc = $metadata['description'] ?? '';
|
|
||||||
$joomlaDesc = (string) ($xml->description ?? '');
|
|
||||||
|
|
||||||
// Joomla descriptions are often language keys, skip those
|
|
||||||
if ($metaDesc !== '' && $joomlaDesc !== '' && !str_starts_with($joomlaDesc, 'COM_') && !str_starts_with($joomlaDesc, 'PKG_')) {
|
|
||||||
$descMatch = ($metaDesc === $joomlaDesc);
|
|
||||||
$results[] = [
|
|
||||||
'field' => 'description',
|
|
||||||
'metadata' => substr($metaDesc, 0, 60) . (strlen($metaDesc) > 60 ? '...' : ''),
|
|
||||||
'joomla' => substr($joomlaDesc, 0, 60) . (strlen($joomlaDesc) > 60 ? '...' : ''),
|
|
||||||
'status' => $descMatch ? 'ok' : 'info',
|
|
||||||
'message' => $descMatch ? 'matches' : 'descriptions differ (informational)',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $results;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =================================================================
|
|
||||||
// Helpers
|
|
||||||
// =================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalize extension_type — map MokoGitea types to Joomla types.
|
|
||||||
*/
|
|
||||||
private function normalizeExtensionType(string $type): string
|
|
||||||
{
|
|
||||||
return match (strtolower($type)) {
|
|
||||||
'joomla-extension' => 'package', // legacy mapping
|
|
||||||
default => strtolower($type),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Derive the Joomla element name from type + name.
|
|
||||||
* Replicates MokoGitea's cleanJoomlaElement() + prefix logic.
|
|
||||||
*/
|
|
||||||
private function deriveElement(string $type, string $name): string
|
|
||||||
{
|
|
||||||
// Clean: lowercase, strip non-alphanumeric except . _ -
|
|
||||||
$clean = strtolower($name);
|
|
||||||
$clean = preg_replace('/[^a-z0-9._-]/', '', $clean);
|
|
||||||
|
|
||||||
$prefix = self::JOOMLA_PREFIX[$type] ?? '';
|
|
||||||
|
|
||||||
return $prefix . $clean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract the element name from a Joomla manifest XML.
|
|
||||||
* Follows the same logic as Joomla's InstallerAdapter::getElement().
|
|
||||||
*/
|
|
||||||
private function extractJoomlaElement(\SimpleXMLElement $xml, string $type): string
|
|
||||||
{
|
|
||||||
switch ($type) {
|
|
||||||
case 'package':
|
|
||||||
$packagename = (string) ($xml->packagename ?? '');
|
|
||||||
if ($packagename !== '') {
|
|
||||||
return 'pkg_' . strtolower(preg_replace('/[^a-zA-Z0-9._-]/', '', $packagename));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'component':
|
|
||||||
$element = (string) ($xml->element ?? '');
|
|
||||||
if ($element !== '') {
|
|
||||||
$element = strtolower($element);
|
|
||||||
return str_starts_with($element, 'com_') ? $element : 'com_' . $element;
|
|
||||||
}
|
|
||||||
$name = (string) ($xml->name ?? '');
|
|
||||||
$name = strtolower(preg_replace('/[^a-zA-Z0-9._-]/', '', $name));
|
|
||||||
return str_starts_with($name, 'com_') ? $name : 'com_' . $name;
|
|
||||||
|
|
||||||
case 'module':
|
|
||||||
$element = (string) ($xml->element ?? '');
|
|
||||||
if ($element !== '') {
|
|
||||||
return strtolower($element);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'plugin':
|
|
||||||
// Plugins derive element from the file attribute
|
|
||||||
if (isset($xml->files)) {
|
|
||||||
foreach ($xml->files->children() as $file) {
|
|
||||||
$plugin = (string) ($file->attributes()->plugin ?? '');
|
|
||||||
if ($plugin !== '') {
|
|
||||||
return strtolower($plugin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'library':
|
|
||||||
$libname = (string) ($xml->libraryname ?? '');
|
|
||||||
if ($libname !== '') {
|
|
||||||
return strtolower($libname);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: use <name> tag
|
|
||||||
$name = (string) ($xml->name ?? '');
|
|
||||||
return strtolower(preg_replace('/[^a-zA-Z0-9._-]/', '', $name));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read PHP version requirement from composer.json.
|
|
||||||
*/
|
|
||||||
private function readComposerPhpRequirement(string $root): string
|
|
||||||
{
|
|
||||||
$composerFile = "{$root}/composer.json";
|
|
||||||
|
|
||||||
if (!is_file($composerFile)) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = json_decode(file_get_contents($composerFile), true);
|
|
||||||
|
|
||||||
if (!is_array($data)) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
$phpReq = $data['require']['php'] ?? '';
|
|
||||||
|
|
||||||
// Extract version number from constraint like ">=8.1"
|
|
||||||
if (preg_match('/(\d+\.\d+)/', $phpReq, $m)) {
|
|
||||||
return $m[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
private function detectRepoName(string $root): string
|
|
||||||
{
|
|
||||||
$gitConfig = "{$root}/.git/config";
|
|
||||||
|
|
||||||
if (!file_exists($gitConfig)) {
|
|
||||||
return basename($root);
|
|
||||||
}
|
|
||||||
|
|
||||||
$content = file_get_contents($gitConfig);
|
|
||||||
|
|
||||||
if (preg_match('/url\s*=\s*.*\/([^\/\s]+?)(?:\.git)?\s*$/m', $content, $m)) {
|
|
||||||
return $m[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
return basename($root);
|
|
||||||
}
|
|
||||||
|
|
||||||
// =================================================================
|
|
||||||
// Output
|
|
||||||
// =================================================================
|
|
||||||
|
|
||||||
private function printResults(string $repoName, array $results): void
|
|
||||||
{
|
|
||||||
$errors = count(array_filter($results, fn($r) => $r['status'] === 'error'));
|
|
||||||
$warns = count(array_filter($results, fn($r) => $r['status'] === 'warn'));
|
|
||||||
$oks = count(array_filter($results, fn($r) => $r['status'] === 'ok'));
|
|
||||||
|
|
||||||
$this->log('INFO', "Validating {$repoName} Joomla metadata...\n");
|
|
||||||
|
|
||||||
foreach ($results as $r) {
|
|
||||||
$icon = match ($r['status']) {
|
|
||||||
'ok' => "\xE2\x9C\x93", // ✓
|
|
||||||
'error' => "\xE2\x9C\x97", // ✗
|
|
||||||
'warn' => "\xE2\x9A\xA0", // ⚠
|
|
||||||
default => "\xE2\x84\xB9", // ℹ
|
|
||||||
};
|
|
||||||
|
|
||||||
$line = sprintf(
|
|
||||||
" %s %-16s %s",
|
|
||||||
$icon,
|
|
||||||
$r['field'],
|
|
||||||
$r['message']
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->log(
|
|
||||||
match ($r['status']) {
|
|
||||||
'error' => 'ERROR',
|
|
||||||
'warn' => 'WARN',
|
|
||||||
'ok' => 'OK',
|
|
||||||
default => 'INFO',
|
|
||||||
},
|
|
||||||
$line
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "\n";
|
|
||||||
|
|
||||||
if ($errors > 0) {
|
|
||||||
$this->log('ERROR', "{$errors} error(s) — update delivery will fail");
|
|
||||||
} elseif ($warns > 0) {
|
|
||||||
$this->log('WARN', "All critical checks passed, {$warns} warning(s)");
|
|
||||||
} else {
|
|
||||||
$this->log('OK', "All {$oks} checks passed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$app = new JoomlaMetadataValidateCli();
|
|
||||||
exit($app->execute());
|
|
||||||
@@ -25,7 +25,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
use MokoCli\{ApiClient, AuditLogger, CliFramework, Config, PlatformAdapterFactory, SourceResolver};
|
use MokoEnterprise\{ApiClient, AuditLogger, CliFramework, Config, PlatformAdapterFactory, SourceResolver};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Joomla Release Manager
|
* Joomla Release Manager
|
||||||
@@ -57,7 +57,7 @@ class JoomlaRelease extends CliFramework
|
|||||||
];
|
];
|
||||||
|
|
||||||
private ApiClient $api;
|
private ApiClient $api;
|
||||||
private \MokoCli\GitPlatformAdapter $adapter;
|
private \MokoEnterprise\GitPlatformAdapter $adapter;
|
||||||
|
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ declare(strict_types=1);
|
|||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
class LicenseManage extends CliFramework
|
class LicenseManage extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
+2
-747
@@ -1,749 +1,4 @@
|
|||||||
#!/usr/bin/env php
|
#!/usr/bin/env php
|
||||||
<?php
|
<?php
|
||||||
|
// Backward-compatibility wrapper — manifest_* renamed to metadata_*
|
||||||
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
require __DIR__ . '/metadata_detect.php';
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* FILE INFORMATION
|
|
||||||
* DEFGROUP: mokoplatform.CLI
|
|
||||||
* INGROUP: mokoplatform
|
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
|
||||||
* PATH: /cli/manifest_detect.php
|
|
||||||
* VERSION: 09.32.01
|
|
||||||
* BRIEF: Auto-detect manifest fields from source files and optionally push to API
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
|
||||||
|
|
||||||
use MokoCli\{CliFramework, SourceResolver};
|
|
||||||
|
|
||||||
class ManifestDetectCli extends CliFramework
|
|
||||||
{
|
|
||||||
protected function configure(): void
|
|
||||||
{
|
|
||||||
$this->setDescription('Auto-detect manifest fields from source files');
|
|
||||||
$this->addArgument('--path', 'Repository root path', '.');
|
|
||||||
$this->addArgument('--json', 'Output as JSON', false);
|
|
||||||
$this->addArgument('--diff', 'Show diff against current manifest API values', false);
|
|
||||||
$this->addArgument('--update', 'Push detected fields to manifest API', false);
|
|
||||||
$this->addArgument('--token', 'Gitea API token (or GITEA_TOKEN env)', '');
|
|
||||||
$this->addArgument('--api-base', 'Gitea API base URL', 'https://git.mokoconsulting.tech/api/v1');
|
|
||||||
$this->addArgument('--org', 'Gitea org', 'MokoConsulting');
|
|
||||||
$this->addArgument('--repo', 'Gitea repo name (auto-detected from remote if empty)', '');
|
|
||||||
$this->addArgument('--github-output', 'Append fields to $GITHUB_OUTPUT', false);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function run(): int
|
|
||||||
{
|
|
||||||
$path = $this->getArgument('--path');
|
|
||||||
$jsonMode = (bool) $this->getArgument('--json');
|
|
||||||
$diffMode = (bool) $this->getArgument('--diff');
|
|
||||||
$updateMode = (bool) $this->getArgument('--update');
|
|
||||||
$ghOutput = (bool) $this->getArgument('--github-output');
|
|
||||||
$token = $this->getArgument('--token') ?: getenv('GITEA_TOKEN') ?: '';
|
|
||||||
$apiBase = rtrim($this->getArgument('--api-base'), '/');
|
|
||||||
$org = $this->getArgument('--org');
|
|
||||||
$repoName = $this->getArgument('--repo');
|
|
||||||
|
|
||||||
$root = realpath($path) ?: $path;
|
|
||||||
|
|
||||||
if (!is_dir($root)) {
|
|
||||||
$this->log('ERROR', "Path does not exist: {$path}");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-detect repo name from git remote
|
|
||||||
if ($repoName === '') {
|
|
||||||
$repoName = $this->detectRepoName($root);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Detect all fields ───────────────────────────────────────
|
|
||||||
$detected = $this->detectAll($root, $repoName);
|
|
||||||
|
|
||||||
// ── Warn about missing fields ────────────────────────────────
|
|
||||||
$expected = ['platform', 'name', 'version', 'package_type', 'language', 'entry_point'];
|
|
||||||
foreach ($expected as $field) {
|
|
||||||
if (!isset($detected[$field]) || $detected[$field] === '') {
|
|
||||||
$this->log('WARN', "Could not detect: {$field}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Output ──────────────────────────────────────────────────
|
|
||||||
if ($diffMode || $updateMode) {
|
|
||||||
if ($token === '') {
|
|
||||||
$this->log('ERROR', 'API token required for --diff/--update (use --token or GITEA_TOKEN env)');
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if ($repoName === '') {
|
|
||||||
$this->log('ERROR', 'Could not determine repo name (use --repo)');
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
$current = $this->fetchManifest($apiBase, $org, $repoName, $token);
|
|
||||||
if ($current === null) {
|
|
||||||
$this->log('ERROR', 'Failed to fetch current manifest from API');
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
$changes = $this->computeDiff($current, $detected);
|
|
||||||
|
|
||||||
if ($diffMode) {
|
|
||||||
if (empty($changes)) {
|
|
||||||
$this->log('INFO', 'No differences — manifest matches source');
|
|
||||||
} else {
|
|
||||||
$this->sectionHeader('Manifest Drift');
|
|
||||||
foreach ($changes as $field => $info) {
|
|
||||||
$this->log('WARN', sprintf(
|
|
||||||
'%-20s API: %-30s Detected: %s',
|
|
||||||
$field,
|
|
||||||
$info['current'] === '' ? '(empty)' : $info['current'],
|
|
||||||
$info['detected']
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($updateMode) {
|
|
||||||
if (empty($changes)) {
|
|
||||||
$this->log('INFO', 'Nothing to update');
|
|
||||||
} else {
|
|
||||||
$update = array_map(fn($i) => $i['detected'], $changes);
|
|
||||||
$ok = $this->pushManifest($apiBase, $org, $repoName, $token, $current, $update);
|
|
||||||
if ($ok) {
|
|
||||||
$this->log('OK', 'Updated ' . count($update) . ' field(s): ' . implode(', ', array_keys($update)));
|
|
||||||
} else {
|
|
||||||
$this->log('ERROR', 'Failed to push manifest update');
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($ghOutput) {
|
|
||||||
$outputFile = getenv('GITHUB_OUTPUT');
|
|
||||||
$lines = [];
|
|
||||||
foreach ($detected as $k => $v) {
|
|
||||||
$envKey = str_replace('-', '_', $k);
|
|
||||||
$lines[] = "{$envKey}={$v}";
|
|
||||||
}
|
|
||||||
if ($outputFile !== false && $outputFile !== '') {
|
|
||||||
file_put_contents($outputFile, implode("\n", $lines) . "\n", FILE_APPEND);
|
|
||||||
$this->log('INFO', 'Wrote ' . count($detected) . ' fields to GITHUB_OUTPUT');
|
|
||||||
} else {
|
|
||||||
$this->log('WARN', 'GITHUB_OUTPUT not set — printing to stdout instead');
|
|
||||||
echo implode("\n", $lines) . "\n";
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($jsonMode) {
|
|
||||||
echo json_encode($detected, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
|
||||||
} else {
|
|
||||||
foreach ($detected as $k => $v) {
|
|
||||||
echo "{$k}={$v}\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =====================================================================
|
|
||||||
// Detection engine
|
|
||||||
// =====================================================================
|
|
||||||
|
|
||||||
private function detectAll(string $root, string $repoName): array
|
|
||||||
{
|
|
||||||
$platform = $this->detectPlatform($root);
|
|
||||||
|
|
||||||
$fields = [
|
|
||||||
'platform' => $platform,
|
|
||||||
'name' => '',
|
|
||||||
'description' => '',
|
|
||||||
'version' => '',
|
|
||||||
'element_name' => '',
|
|
||||||
'package_type' => '',
|
|
||||||
'language' => '',
|
|
||||||
'entry_point' => '',
|
|
||||||
'license_spdx' => '',
|
|
||||||
'display_name' => '',
|
|
||||||
'target_version' => '',
|
|
||||||
'php_minimum' => '',
|
|
||||||
];
|
|
||||||
|
|
||||||
switch ($platform) {
|
|
||||||
case 'joomla':
|
|
||||||
$this->detectJoomla($root, $repoName, $fields);
|
|
||||||
break;
|
|
||||||
case 'dolibarr':
|
|
||||||
$this->detectDolibarr($root, $repoName, $fields);
|
|
||||||
break;
|
|
||||||
case 'go':
|
|
||||||
$this->detectGo($root, $repoName, $fields);
|
|
||||||
break;
|
|
||||||
case 'mcp':
|
|
||||||
$this->detectNode($root, $repoName, $fields);
|
|
||||||
break;
|
|
||||||
case 'node':
|
|
||||||
$this->detectNode($root, $repoName, $fields);
|
|
||||||
$fields['platform'] = 'node';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
$this->detectGeneric($root, $repoName, $fields);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallbacks
|
|
||||||
if ($fields['name'] === '') {
|
|
||||||
$fields['name'] = $repoName ?: basename($root);
|
|
||||||
}
|
|
||||||
if ($fields['entry_point'] === '') {
|
|
||||||
$fields['entry_point'] = $this->detectEntryPoint($root);
|
|
||||||
}
|
|
||||||
if ($fields['license_spdx'] === '') {
|
|
||||||
$fields['license_spdx'] = $this->detectLicense($root);
|
|
||||||
}
|
|
||||||
// description: only from platform-specific source, never guessed
|
|
||||||
|
|
||||||
// Strip empty values
|
|
||||||
return array_filter($fields, fn($v) => $v !== '');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Platform detection ──────────────────────────────────────────
|
|
||||||
|
|
||||||
private function detectPlatform(string $root): string
|
|
||||||
{
|
|
||||||
// Joomla: look for pkg_*.xml or extension XML in source dirs
|
|
||||||
$joomlaXmls = array_merge(
|
|
||||||
SourceResolver::globSource($root, 'pkg_*.xml'),
|
|
||||||
glob("{$root}/pkg_*.xml") ?: []
|
|
||||||
);
|
|
||||||
if (!empty($joomlaXmls)) {
|
|
||||||
return 'joomla';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check source dirs for any Joomla extension XML
|
|
||||||
foreach (SourceResolver::globSource($root, '*.xml') as $xmlFile) {
|
|
||||||
$content = file_get_contents($xmlFile);
|
|
||||||
if (strpos($content, '<extension') !== false) {
|
|
||||||
return 'joomla';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dolibarr: mod*.class.php with DolibarrModules
|
|
||||||
$modFiles = array_merge(
|
|
||||||
SourceResolver::globSource($root, 'core/modules/mod*.class.php'),
|
|
||||||
glob("{$root}/core/modules/mod*.class.php") ?: []
|
|
||||||
);
|
|
||||||
foreach ($modFiles as $file) {
|
|
||||||
if (strpos(file_get_contents($file), 'DolibarrModules') !== false) {
|
|
||||||
return 'dolibarr';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Go
|
|
||||||
if (file_exists("{$root}/go.mod")) {
|
|
||||||
return 'go';
|
|
||||||
}
|
|
||||||
|
|
||||||
// MCP: package.json with mcp-related content
|
|
||||||
if (file_exists("{$root}/package.json")) {
|
|
||||||
$pkg = json_decode(file_get_contents("{$root}/package.json"), true) ?? [];
|
|
||||||
$deps = array_merge(
|
|
||||||
array_keys($pkg['dependencies'] ?? []),
|
|
||||||
array_keys($pkg['devDependencies'] ?? [])
|
|
||||||
);
|
|
||||||
foreach ($deps as $dep) {
|
|
||||||
if (strpos($dep, '@modelcontextprotocol/') === 0 || $dep === '@anthropic/mcp-sdk') {
|
|
||||||
return 'mcp';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 'node';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Python
|
|
||||||
if (file_exists("{$root}/pyproject.toml") || file_exists("{$root}/setup.py")) {
|
|
||||||
return 'python';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'generic';
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Joomla ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private function detectJoomla(string $root, string $repoName, array &$fields): void
|
|
||||||
{
|
|
||||||
$fields['language'] = 'PHP';
|
|
||||||
|
|
||||||
// Find the primary extension manifest XML
|
|
||||||
$extManifest = $this->findJoomlaManifest($root);
|
|
||||||
if ($extManifest === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$xml = file_get_contents($extManifest);
|
|
||||||
|
|
||||||
// Type
|
|
||||||
$extType = '';
|
|
||||||
if (preg_match('/type="([^"]*)"/', $xml, $m)) {
|
|
||||||
$extType = $m[1];
|
|
||||||
}
|
|
||||||
$fields['package_type'] = $extType;
|
|
||||||
|
|
||||||
// Element name
|
|
||||||
$element = '';
|
|
||||||
if (preg_match('/<element>([^<]+)<\/element>/', $xml, $m)) {
|
|
||||||
$element = $m[1];
|
|
||||||
}
|
|
||||||
if ($element === '' && preg_match('/module="([^"]*)"/', $xml, $m)) {
|
|
||||||
$element = $m[1];
|
|
||||||
}
|
|
||||||
if ($element === '' && preg_match('/plugin="([^"]*)"/', $xml, $m)) {
|
|
||||||
$element = $m[1];
|
|
||||||
}
|
|
||||||
if ($extType === 'package' && preg_match('/<packagename>([^<]+)<\/packagename>/', $xml, $m)) {
|
|
||||||
$element = $m[1];
|
|
||||||
}
|
|
||||||
if ($element === '') {
|
|
||||||
$element = strtolower(basename($extManifest, '.xml'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure element has type prefix (API stores full element_name like pkg_mokosuite)
|
|
||||||
$prefixMap = [
|
|
||||||
'package' => 'pkg_', 'component' => 'com_', 'module' => 'mod_',
|
|
||||||
'template' => 'tpl_', 'library' => 'lib_', 'file' => 'file_',
|
|
||||||
];
|
|
||||||
if (isset($prefixMap[$extType])) {
|
|
||||||
$prefix = $prefixMap[$extType];
|
|
||||||
// Only add prefix if not already present (check all known prefixes)
|
|
||||||
$hasPrefix = false;
|
|
||||||
foreach ($prefixMap as $p) {
|
|
||||||
if (strpos($element, $p) === 0) { $hasPrefix = true; break; }
|
|
||||||
}
|
|
||||||
if (strpos($element, 'plg_') === 0) { $hasPrefix = true; }
|
|
||||||
if (!$hasPrefix) {
|
|
||||||
$element = $prefix . $element;
|
|
||||||
}
|
|
||||||
} elseif ($extType === 'plugin') {
|
|
||||||
$folder = '';
|
|
||||||
if (preg_match('/group="([^"]*)"/', $xml, $gm)) {
|
|
||||||
$folder = $gm[1];
|
|
||||||
}
|
|
||||||
if ($folder !== '' && strpos($element, 'plg_') !== 0) {
|
|
||||||
$element = "plg_{$folder}_" . $element;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$fields['element_name'] = $element;
|
|
||||||
|
|
||||||
// Name
|
|
||||||
if (preg_match('/<name>([^<]+)<\/name>/', $xml, $m)) {
|
|
||||||
$fields['name'] = trim($m[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Version
|
|
||||||
if (preg_match('/<version>([^<]+)<\/version>/', $xml, $m)) {
|
|
||||||
$fields['version'] = trim($m[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Description
|
|
||||||
if (preg_match('/<description>([^<]+)<\/description>/', $xml, $m)) {
|
|
||||||
$desc = trim($m[1]);
|
|
||||||
// Skip language string keys like COM_MOKOSUITE_DESCRIPTION
|
|
||||||
if (strpos($desc, '_') === false || strlen($desc) > 60) {
|
|
||||||
$fields['description'] = $desc;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display name for update feeds
|
|
||||||
if (!empty($fields['name'])) {
|
|
||||||
$name = $fields['name'];
|
|
||||||
// If name already has "Type - " prefix, use as-is
|
|
||||||
if (preg_match('/^(Package|Component|Module|Plugin|Template|Library)\s*-\s*/i', $name)) {
|
|
||||||
$fields['display_name'] = $name;
|
|
||||||
} elseif (!empty($extType)) {
|
|
||||||
$fields['display_name'] = ucfirst($extType) . ' - ' . $name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Target Joomla version
|
|
||||||
if (preg_match('/<targetplatform\s[^>]*version="([^"]+)"/', $xml, $m)) {
|
|
||||||
$fields['target_version'] = trim($m[1]);
|
|
||||||
} else {
|
|
||||||
// Default for Joomla 5/6
|
|
||||||
$fields['target_version'] = '(5|6)\..*';
|
|
||||||
}
|
|
||||||
|
|
||||||
// PHP minimum
|
|
||||||
if (preg_match('/<php_minimum>([^<]+)<\/php_minimum>/', $xml, $m)) {
|
|
||||||
$fields['php_minimum'] = trim($m[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// License
|
|
||||||
if (preg_match('/<license>([^<]+)<\/license>/', $xml, $m)) {
|
|
||||||
$fields['license_spdx'] = $this->normalizeLicense(trim($m[1]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function findJoomlaManifest(string $root): ?string
|
|
||||||
{
|
|
||||||
// Priority: pkg_*.xml (package manifest)
|
|
||||||
$pkgXmls = array_merge(
|
|
||||||
SourceResolver::globSource($root, 'pkg_*.xml'),
|
|
||||||
glob("{$root}/pkg_*.xml") ?: []
|
|
||||||
);
|
|
||||||
if (!empty($pkgXmls)) {
|
|
||||||
return $pkgXmls[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Any extension XML in source dir
|
|
||||||
foreach (SourceResolver::globSource($root, '*.xml') as $file) {
|
|
||||||
$content = file_get_contents($file);
|
|
||||||
if (strpos($content, '<extension') !== false) {
|
|
||||||
return $file;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Root level
|
|
||||||
foreach (glob("{$root}/*.xml") ?: [] as $file) {
|
|
||||||
$content = file_get_contents($file);
|
|
||||||
if (strpos($content, '<extension') !== false) {
|
|
||||||
return $file;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Dolibarr ────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private function detectDolibarr(string $root, string $repoName, array &$fields): void
|
|
||||||
{
|
|
||||||
$fields['language'] = 'PHP';
|
|
||||||
$fields['package_type'] = 'dolibarr-module';
|
|
||||||
|
|
||||||
$modFile = $this->findDolibarrModule($root);
|
|
||||||
if ($modFile === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$content = file_get_contents($modFile);
|
|
||||||
|
|
||||||
// Element name from class file
|
|
||||||
$modBasename = basename($modFile, '.class.php');
|
|
||||||
$fields['element_name'] = strtolower(preg_replace('/^mod/', '', $modBasename));
|
|
||||||
|
|
||||||
// Name
|
|
||||||
if (preg_match('/\$this->name\s*=\s*[\'"]([^\'"]+)[\'"]/', $content, $m)) {
|
|
||||||
$fields['name'] = $m[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Version
|
|
||||||
if (preg_match('/\$this->version\s*=\s*[\'"]([^\'"]+)[\'"]/', $content, $m)) {
|
|
||||||
$fields['version'] = $m[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Description
|
|
||||||
if (preg_match('/\$this->description\s*=\s*[\'"]([^\'"]+)[\'"]/', $content, $m)) {
|
|
||||||
$desc = $m[1];
|
|
||||||
if (strpos($desc, '$') === false) {
|
|
||||||
$fields['description'] = $desc;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// License
|
|
||||||
if (preg_match('/SPDX-License-Identifier:\s*(\S+)/', $content, $m)) {
|
|
||||||
$fields['license_spdx'] = $m[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function findDolibarrModule(string $root): ?string
|
|
||||||
{
|
|
||||||
$candidates = array_merge(
|
|
||||||
SourceResolver::globSource($root, 'core/modules/mod*.class.php'),
|
|
||||||
glob("{$root}/core/modules/mod*.class.php") ?: []
|
|
||||||
);
|
|
||||||
foreach ($candidates as $file) {
|
|
||||||
if (strpos(file_get_contents($file), 'DolibarrModules') !== false) {
|
|
||||||
return $file;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Go ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private function detectGo(string $root, string $repoName, array &$fields): void
|
|
||||||
{
|
|
||||||
$fields['language'] = 'Go';
|
|
||||||
$fields['package_type'] = 'application';
|
|
||||||
$fields['entry_point'] = './';
|
|
||||||
|
|
||||||
$goMod = "{$root}/go.mod";
|
|
||||||
if (!file_exists($goMod)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$content = file_get_contents($goMod);
|
|
||||||
|
|
||||||
// Module path → name
|
|
||||||
if (preg_match('/^module\s+(\S+)/m', $content, $m)) {
|
|
||||||
$modulePath = $m[1];
|
|
||||||
$parts = explode('/', $modulePath);
|
|
||||||
$fields['name'] = end($parts);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Go version
|
|
||||||
if (preg_match('/^go\s+(\S+)/m', $content, $m)) {
|
|
||||||
// This is Go language version, not the project version
|
|
||||||
// Project version comes from git tags or source files
|
|
||||||
}
|
|
||||||
|
|
||||||
// License
|
|
||||||
$fields['license_spdx'] = $this->detectLicense($root);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Node / MCP ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private function detectNode(string $root, string $repoName, array &$fields): void
|
|
||||||
{
|
|
||||||
$pkgFile = "{$root}/package.json";
|
|
||||||
if (!file_exists($pkgFile)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$pkg = json_decode(file_get_contents($pkgFile), true) ?? [];
|
|
||||||
|
|
||||||
$fields['name'] = $pkg['name'] ?? '';
|
|
||||||
// Strip npm scope
|
|
||||||
if (strpos($fields['name'], '/') !== false) {
|
|
||||||
$fields['name'] = explode('/', $fields['name'])[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
$fields['version'] = $pkg['version'] ?? '';
|
|
||||||
$fields['description'] = $pkg['description'] ?? '';
|
|
||||||
$fields['license_spdx'] = $pkg['license'] ?? '';
|
|
||||||
|
|
||||||
// Language detection
|
|
||||||
if (file_exists("{$root}/tsconfig.json")) {
|
|
||||||
$fields['language'] = 'TypeScript';
|
|
||||||
} else {
|
|
||||||
$fields['language'] = 'JavaScript';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Package type
|
|
||||||
$deps = array_merge(
|
|
||||||
array_keys($pkg['dependencies'] ?? []),
|
|
||||||
array_keys($pkg['devDependencies'] ?? [])
|
|
||||||
);
|
|
||||||
$isMcp = false;
|
|
||||||
foreach ($deps as $dep) {
|
|
||||||
if (strpos($dep, '@modelcontextprotocol/') === 0 || $dep === '@anthropic/mcp-sdk') {
|
|
||||||
$isMcp = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$fields['package_type'] = $isMcp ? 'mcp-server' : 'application';
|
|
||||||
|
|
||||||
// Entry point
|
|
||||||
if (file_exists("{$root}/dist")) {
|
|
||||||
$fields['entry_point'] = 'dist/';
|
|
||||||
} elseif (file_exists("{$root}/src")) {
|
|
||||||
$fields['entry_point'] = 'src/';
|
|
||||||
} else {
|
|
||||||
$fields['entry_point'] = './';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Generic ─────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private function detectGeneric(string $root, string $repoName, array &$fields): void
|
|
||||||
{
|
|
||||||
$fields['package_type'] = 'generic';
|
|
||||||
|
|
||||||
// Try to detect language from file extensions
|
|
||||||
$fields['language'] = $this->detectLanguageFromFiles($root);
|
|
||||||
$fields['license_spdx'] = $this->detectLicense($root);
|
|
||||||
}
|
|
||||||
|
|
||||||
// =====================================================================
|
|
||||||
// Shared detection helpers
|
|
||||||
// =====================================================================
|
|
||||||
|
|
||||||
private function detectEntryPoint(string $root): string
|
|
||||||
{
|
|
||||||
$abs = SourceResolver::resolveAbsolute($root);
|
|
||||||
if ($abs !== null) {
|
|
||||||
return basename($abs) . '/';
|
|
||||||
}
|
|
||||||
if (is_dir("{$root}/dist")) return 'dist/';
|
|
||||||
if (is_dir("{$root}/src")) return 'src/';
|
|
||||||
return './';
|
|
||||||
}
|
|
||||||
|
|
||||||
private function detectLicense(string $root): string
|
|
||||||
{
|
|
||||||
// Check LICENSE file
|
|
||||||
foreach (['LICENSE', 'LICENSE.md', 'LICENSE.txt', 'COPYING'] as $name) {
|
|
||||||
$file = "{$root}/{$name}";
|
|
||||||
if (!file_exists($file)) continue;
|
|
||||||
$content = file_get_contents($file);
|
|
||||||
|
|
||||||
// SPDX header
|
|
||||||
if (preg_match('/SPDX-License-Identifier:\s*(\S+)/', $content, $m)) {
|
|
||||||
return $m[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Common license patterns
|
|
||||||
if (strpos($content, 'GNU GENERAL PUBLIC LICENSE') !== false) {
|
|
||||||
if (strpos($content, 'Version 3') !== false) return 'GPL-3.0-or-later';
|
|
||||||
if (strpos($content, 'Version 2') !== false) return 'GPL-2.0-or-later';
|
|
||||||
}
|
|
||||||
if (strpos($content, 'MIT License') !== false) return 'MIT';
|
|
||||||
if (strpos($content, 'Apache License') !== false && strpos($content, 'Version 2.0') !== false) return 'Apache-2.0';
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private function detectLanguageFromFiles(string $root): string
|
|
||||||
{
|
|
||||||
$counts = ['PHP' => 0, 'Go' => 0, 'TypeScript' => 0, 'JavaScript' => 0, 'Python' => 0, 'Shell' => 0];
|
|
||||||
|
|
||||||
$extensions = [
|
|
||||||
'php' => 'PHP', 'go' => 'Go', 'ts' => 'TypeScript',
|
|
||||||
'js' => 'JavaScript', 'py' => 'Python', 'sh' => 'Shell',
|
|
||||||
];
|
|
||||||
|
|
||||||
// Quick scan: only check top two levels
|
|
||||||
foreach (glob("{$root}/*") ?: [] as $item) {
|
|
||||||
$ext = pathinfo($item, PATHINFO_EXTENSION);
|
|
||||||
if (isset($extensions[$ext])) {
|
|
||||||
$counts[$extensions[$ext]]++;
|
|
||||||
}
|
|
||||||
if (is_dir($item) && basename($item)[0] !== '.') {
|
|
||||||
foreach (glob("{$item}/*") ?: [] as $subItem) {
|
|
||||||
$ext = pathinfo($subItem, PATHINFO_EXTENSION);
|
|
||||||
if (isset($extensions[$ext])) {
|
|
||||||
$counts[$extensions[$ext]]++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
arsort($counts);
|
|
||||||
$top = key($counts);
|
|
||||||
return $counts[$top] > 0 ? $top : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
private function normalizeLicense(string $license): string
|
|
||||||
{
|
|
||||||
$lower = strtolower($license);
|
|
||||||
$isGpl = strpos($lower, 'gpl') !== false || strpos($lower, 'general public license') !== false;
|
|
||||||
if ($isGpl && strpos($lower, '3') !== false) return 'GPL-3.0-or-later';
|
|
||||||
if ($isGpl && strpos($lower, '2') !== false) return 'GPL-2.0-or-later';
|
|
||||||
if ($lower === 'mit' || strpos($lower, 'mit license') !== false) return 'MIT';
|
|
||||||
if (strpos($lower, 'apache') !== false) return 'Apache-2.0';
|
|
||||||
return $license;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function detectRepoName(string $root): string
|
|
||||||
{
|
|
||||||
$gitConfig = "{$root}/.git/config";
|
|
||||||
if (!file_exists($gitConfig)) {
|
|
||||||
return basename($root);
|
|
||||||
}
|
|
||||||
|
|
||||||
$content = file_get_contents($gitConfig);
|
|
||||||
if (preg_match('/url\s*=\s*.*\/([^\/\s]+?)(?:\.git)?\s*$/m', $content, $m)) {
|
|
||||||
return $m[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
return basename($root);
|
|
||||||
}
|
|
||||||
|
|
||||||
// =====================================================================
|
|
||||||
// API interaction
|
|
||||||
// =====================================================================
|
|
||||||
|
|
||||||
private function fetchManifest(string $apiBase, string $org, string $repo, string $token): ?array
|
|
||||||
{
|
|
||||||
$url = "{$apiBase}/repos/{$org}/{$repo}/manifest";
|
|
||||||
$ctx = stream_context_create([
|
|
||||||
'http' => [
|
|
||||||
'header' => "Authorization: token {$token}\r\nAccept: application/json\r\n",
|
|
||||||
'timeout' => 10,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$body = @file_get_contents($url, false, $ctx);
|
|
||||||
if ($body === false) return null;
|
|
||||||
|
|
||||||
return json_decode($body, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function computeDiff(array $current, array $detected): array
|
|
||||||
{
|
|
||||||
// Map detected keys to API keys (underscores match)
|
|
||||||
$changes = [];
|
|
||||||
|
|
||||||
foreach ($detected as $key => $value) {
|
|
||||||
$apiKey = $key;
|
|
||||||
$currentVal = $current[$apiKey] ?? '';
|
|
||||||
|
|
||||||
// Only flag as changed if detected value is non-empty and differs
|
|
||||||
if ($value !== '' && $value !== $currentVal) {
|
|
||||||
// Don't overwrite a non-empty API value with a detected value
|
|
||||||
// unless the API value is actually empty
|
|
||||||
if ($currentVal === '' || $this->shouldOverride($key, $currentVal, $value)) {
|
|
||||||
$changes[$key] = [
|
|
||||||
'current' => $currentVal,
|
|
||||||
'detected' => $value,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $changes;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function shouldOverride(string $field, string $current, string $detected): bool
|
|
||||||
{
|
|
||||||
// Version: detected from source is authoritative
|
|
||||||
if ($field === 'version') return true;
|
|
||||||
|
|
||||||
// These fields: source files are authoritative
|
|
||||||
if (in_array($field, ['element_name', 'package_type', 'language', 'entry_point'], true)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For other fields, only fill empty — don't overwrite manual edits
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function pushManifest(string $apiBase, string $org, string $repo, string $token, array $current, array $update): bool
|
|
||||||
{
|
|
||||||
$merged = array_merge($current, $update);
|
|
||||||
$url = "{$apiBase}/repos/{$org}/{$repo}/manifest";
|
|
||||||
$payload = json_encode($merged);
|
|
||||||
|
|
||||||
$ctx = stream_context_create([
|
|
||||||
'http' => [
|
|
||||||
'method' => 'PUT',
|
|
||||||
'header' => "Authorization: token {$token}\r\nContent-Type: application/json\r\nAccept: application/json\r\n",
|
|
||||||
'content' => $payload,
|
|
||||||
'timeout' => 10,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$body = @file_get_contents($url, false, $ctx);
|
|
||||||
return $body !== false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$app = new ManifestDetectCli();
|
|
||||||
exit($app->execute());
|
|
||||||
|
|||||||
+2
-189
@@ -1,191 +1,4 @@
|
|||||||
#!/usr/bin/env php
|
#!/usr/bin/env php
|
||||||
<?php
|
<?php
|
||||||
|
// Backward-compatibility wrapper — manifest_* renamed to metadata_*
|
||||||
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
require __DIR__ . '/metadata_element.php';
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* FILE INFORMATION
|
|
||||||
* DEFGROUP: mokoplatform.CLI
|
|
||||||
* INGROUP: mokoplatform
|
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
|
||||||
* PATH: /cli/manifest_element.php
|
|
||||||
* BRIEF: Extract element name, type, type prefix, and ZIP name from manifest
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
|
||||||
|
|
||||||
use MokoCli\{CliFramework, SourceResolver};
|
|
||||||
|
|
||||||
class ManifestElementCli extends CliFramework
|
|
||||||
{
|
|
||||||
protected function configure(): void
|
|
||||||
{
|
|
||||||
$this->setDescription('Extract element name, type, type prefix, and ZIP name from manifest');
|
|
||||||
$this->addArgument('--path', 'Repository root', '.');
|
|
||||||
$this->addArgument('--version', 'Version string', null);
|
|
||||||
$this->addArgument('--stability', 'Stability level', 'stable');
|
|
||||||
$this->addArgument('--repo', 'Repository name', '');
|
|
||||||
$this->addArgument('--github-output', 'Export results to $GITHUB_OUTPUT', false);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function run(): int
|
|
||||||
{
|
|
||||||
$path = $this->getArgument('--path');
|
|
||||||
$version = $this->getArgument('--version');
|
|
||||||
$stability = $this->getArgument('--stability');
|
|
||||||
$repoName = $this->getArgument('--repo');
|
|
||||||
$githubOutput = (bool) $this->getArgument('--github-output');
|
|
||||||
$root = realpath($path) ?: $path;
|
|
||||||
$platform = 'generic';
|
|
||||||
$manifestXml = "{$root}/.mokogitea/manifest.xml";
|
|
||||||
if (file_exists($manifestXml)) {
|
|
||||||
$content = file_get_contents($manifestXml);
|
|
||||||
if (preg_match('/<platform>([^<]+)<\/platform>/', $content, $pm)) {
|
|
||||||
$platform = trim($pm[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$extManifest = null;
|
|
||||||
$manifestFiles = array_merge(SourceResolver::globSource($root, 'pkg_*.xml'), SourceResolver::globSource($root, '*.xml'), glob("{$root}/*.xml") ?: []);
|
|
||||||
foreach ($manifestFiles as $file) {
|
|
||||||
$c = file_get_contents($file);
|
|
||||||
if (strpos($c, '<extension') !== false) {
|
|
||||||
$extManifest = $file;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$modFile = null;
|
|
||||||
$modFiles = array_merge(
|
|
||||||
SourceResolver::globSource($root, 'core/modules/mod*.class.php'),
|
|
||||||
glob("{$root}/core/modules/mod*.class.php") ?: []
|
|
||||||
);
|
|
||||||
foreach ($modFiles as $file) {
|
|
||||||
$c = file_get_contents($file);
|
|
||||||
if (strpos($c, 'extends DolibarrModules') !== false) {
|
|
||||||
$modFile = $file;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$extElement = '';
|
|
||||||
$extType = '';
|
|
||||||
$extFolder = '';
|
|
||||||
$extName = '';
|
|
||||||
switch (true) {
|
|
||||||
case in_array($platform, ['joomla', 'waas-component'], true) && $extManifest !== null:
|
|
||||||
$xml = file_get_contents($extManifest);
|
|
||||||
if (preg_match('/type="([^"]*)"/', $xml, $tm)) {
|
|
||||||
$extType = $tm[1];
|
|
||||||
}
|
|
||||||
if (preg_match('/group="([^"]*)"/', $xml, $gm)) {
|
|
||||||
$extFolder = $gm[1];
|
|
||||||
}
|
|
||||||
if (preg_match('/<element>([^<]+)<\/element>/', $xml, $em)) {
|
|
||||||
$extElement = $em[1];
|
|
||||||
}
|
|
||||||
if (empty($extElement) && preg_match('/module="([^"]*)"/', $xml, $mm)) {
|
|
||||||
$extElement = $mm[1];
|
|
||||||
}
|
|
||||||
if (empty($extElement) && preg_match('/plugin="([^"]*)"/', $xml, $pm2)) {
|
|
||||||
$extElement = $pm2[1];
|
|
||||||
}
|
|
||||||
if ($extType === 'package' && preg_match('/<packagename>([^<]+)<\/packagename>/', $xml, $pn)) {
|
|
||||||
$extElement = $pn[1];
|
|
||||||
}
|
|
||||||
if (empty($extElement)) {
|
|
||||||
$extElement = strtolower(basename($extManifest, '.xml'));
|
|
||||||
if (in_array($extElement, ['templatedetails', 'manifest'], true)) {
|
|
||||||
$extElement = strtolower(str_replace([' ', '-'], '', $repoName ?: basename($root)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (preg_match('/<name>([^<]+)<\/name>/', $xml, $nm)) {
|
|
||||||
$extName = trim($nm[1]);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case in_array($platform, ['dolibarr', 'crm-module'], true) && $modFile !== null:
|
|
||||||
$extType = 'dolibarr-module';
|
|
||||||
$modBasename = basename($modFile, '.class.php');
|
|
||||||
$extElement = strtolower(preg_replace('/^mod/', '', $modBasename));
|
|
||||||
$modContent = file_get_contents($modFile);
|
|
||||||
if (preg_match('/\$this->name\s*=\s*[\'"]([^\'"]+)[\'"]/', $modContent, $nm)) {
|
|
||||||
$extName = $nm[1];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
$extElement = strtolower(str_replace([' ', '-'], '', $repoName ?: basename($root)));
|
|
||||||
$extType = 'generic';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
$extElement = preg_replace('/^(pkg_|com_|mod_|plg_[a-z]+_|tpl_|lib_)/', '', $extElement);
|
|
||||||
$typePrefix = '';
|
|
||||||
switch ($extType) {
|
|
||||||
case 'plugin':
|
|
||||||
$typePrefix = "plg_{$extFolder}_";
|
|
||||||
break;
|
|
||||||
case 'module':
|
|
||||||
$typePrefix = 'mod_';
|
|
||||||
break;
|
|
||||||
case 'component':
|
|
||||||
$typePrefix = 'com_';
|
|
||||||
break;
|
|
||||||
case 'template':
|
|
||||||
$typePrefix = 'tpl_';
|
|
||||||
break;
|
|
||||||
case 'library':
|
|
||||||
$typePrefix = 'lib_';
|
|
||||||
break;
|
|
||||||
case 'package':
|
|
||||||
$typePrefix = 'pkg_';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
$suffixMap = [
|
|
||||||
'development' => '-dev',
|
|
||||||
'dev' => '-dev',
|
|
||||||
'alpha' => '-alpha',
|
|
||||||
'beta' => '-beta',
|
|
||||||
'rc' => '-rc',
|
|
||||||
'release-candidate' => '-rc',
|
|
||||||
'stable' => '',
|
|
||||||
];
|
|
||||||
$suffix = $suffixMap[$stability] ?? '';
|
|
||||||
$zipName = '';
|
|
||||||
if ($version !== null) {
|
|
||||||
$zipName = "{$typePrefix}{$extElement}-{$version}{$suffix}.zip";
|
|
||||||
}
|
|
||||||
if (empty($extName)) {
|
|
||||||
$extName = $repoName ?: basename($root);
|
|
||||||
}
|
|
||||||
$outputs = [
|
|
||||||
'platform' => $platform,
|
|
||||||
'ext_element' => $extElement,
|
|
||||||
'ext_type' => $extType,
|
|
||||||
'ext_folder' => $extFolder,
|
|
||||||
'ext_name' => $extName,
|
|
||||||
'type_prefix' => $typePrefix,
|
|
||||||
'zip_name' => $zipName,
|
|
||||||
];
|
|
||||||
if ($githubOutput) {
|
|
||||||
$ghOutput = getenv('GITHUB_OUTPUT');
|
|
||||||
$lines = [];
|
|
||||||
foreach ($outputs as $key => $value) {
|
|
||||||
$lines[] = "{$key}={$value}";
|
|
||||||
}
|
|
||||||
if ($ghOutput) {
|
|
||||||
file_put_contents($ghOutput, implode("\n", $lines) . "\n", FILE_APPEND);
|
|
||||||
} else {
|
|
||||||
foreach ($outputs as $key => $value) {
|
|
||||||
echo "::set-output name={$key}::{$value}\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
foreach ($outputs as $key => $value) {
|
|
||||||
echo "{$key}={$value}\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$app = new ManifestElementCli();
|
|
||||||
exit($app->execute());
|
|
||||||
|
|||||||
+2
-562
@@ -1,564 +1,4 @@
|
|||||||
#!/usr/bin/env php
|
#!/usr/bin/env php
|
||||||
<?php
|
<?php
|
||||||
|
// Backward-compatibility wrapper — manifest_* renamed to metadata_*
|
||||||
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
require __DIR__ . '/metadata_integrity.php';
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* FILE INFORMATION
|
|
||||||
* DEFGROUP: mokoplatform.CLI
|
|
||||||
* INGROUP: mokoplatform
|
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
|
||||||
* PATH: /cli/manifest_integrity.php
|
|
||||||
* VERSION: 09.32.01
|
|
||||||
* BRIEF: Cross-check manifest API fields against repo contents across the org
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
|
||||||
|
|
||||||
class ManifestIntegrityCli extends CliFramework
|
|
||||||
{
|
|
||||||
protected function configure(): void
|
|
||||||
{
|
|
||||||
$this->setDescription('Cross-check manifest fields against repo contents across the org');
|
|
||||||
$this->addArgument('--path', 'Single repo path (local mode)', '');
|
|
||||||
$this->addArgument('--org', 'Gitea org (bulk mode)', 'MokoConsulting');
|
|
||||||
$this->addArgument('--repo', 'Single repo name (remote mode)', '');
|
|
||||||
$this->addArgument('--token', 'Gitea API token (or GITEA_TOKEN env)', '');
|
|
||||||
$this->addArgument('--api-base', 'Gitea API base URL', 'https://git.mokoconsulting.tech/api/v1');
|
|
||||||
$this->addArgument('--fix', 'Push fixes for detected drift', false);
|
|
||||||
$this->addArgument('--json', 'Output as JSON', false);
|
|
||||||
$this->addArgument('--quiet', 'Only show repos with issues', false);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function run(): int
|
|
||||||
{
|
|
||||||
$path = $this->getArgument('--path');
|
|
||||||
$org = $this->getArgument('--org');
|
|
||||||
$repoName = $this->getArgument('--repo');
|
|
||||||
$token = $this->getArgument('--token') ?: getenv('GITEA_TOKEN') ?: '';
|
|
||||||
$apiBase = rtrim($this->getArgument('--api-base'), '/');
|
|
||||||
$fixMode = (bool) $this->getArgument('--fix');
|
|
||||||
$jsonMode = (bool) $this->getArgument('--json');
|
|
||||||
$quiet = (bool) $this->getArgument('--quiet');
|
|
||||||
|
|
||||||
if ($token === '') {
|
|
||||||
$this->log('ERROR', 'API token required (use --token or GITEA_TOKEN env)');
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Mode selection ──────────────────────────────────────────
|
|
||||||
if ($path !== '') {
|
|
||||||
// Local mode: detect from source + compare to API
|
|
||||||
return $this->checkLocal($path, $org, $repoName, $token, $apiBase, $fixMode, $jsonMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($repoName !== '') {
|
|
||||||
// Single remote repo
|
|
||||||
return $this->checkRemoteRepo($org, $repoName, $token, $apiBase, $fixMode, $jsonMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bulk mode: all repos in org
|
|
||||||
return $this->checkOrg($org, $token, $apiBase, $fixMode, $jsonMode, $quiet);
|
|
||||||
}
|
|
||||||
|
|
||||||
// =====================================================================
|
|
||||||
// Local mode — detect from source, compare to API
|
|
||||||
// =====================================================================
|
|
||||||
|
|
||||||
private function checkLocal(string $path, string $org, string $repoName, string $token, string $apiBase, bool $fix, bool $json): int
|
|
||||||
{
|
|
||||||
$root = realpath($path) ?: $path;
|
|
||||||
if (!is_dir($root)) {
|
|
||||||
$this->log('ERROR', "Path does not exist: {$path}");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($repoName === '') {
|
|
||||||
$repoName = $this->detectRepoName($root);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run manifest_detect logic
|
|
||||||
$detected = $this->runDetect($root, $repoName);
|
|
||||||
$current = $this->fetchManifest($apiBase, $org, $repoName, $token);
|
|
||||||
|
|
||||||
if ($current === null) {
|
|
||||||
$this->log('ERROR', "Failed to fetch manifest for {$org}/{$repoName}");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
$issues = $this->validate($current, $detected, $repoName);
|
|
||||||
|
|
||||||
if ($json) {
|
|
||||||
echo json_encode(['repo' => $repoName, 'issues' => $issues], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
|
||||||
} else {
|
|
||||||
$this->printIssues($repoName, $issues);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($fix && !empty($issues)) {
|
|
||||||
return $this->applyFixes($apiBase, $org, $repoName, $token, $current, $issues);
|
|
||||||
}
|
|
||||||
|
|
||||||
return empty($issues) ? 0 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =====================================================================
|
|
||||||
// Remote single repo mode — fetch source files via API
|
|
||||||
// =====================================================================
|
|
||||||
|
|
||||||
private function checkRemoteRepo(string $org, string $repoName, string $token, string $apiBase, bool $fix, bool $json): int
|
|
||||||
{
|
|
||||||
$current = $this->fetchManifest($apiBase, $org, $repoName, $token);
|
|
||||||
if ($current === null) {
|
|
||||||
$this->log('ERROR', "Failed to fetch manifest for {$org}/{$repoName}");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
$issues = $this->validateManifestOnly($current, $repoName);
|
|
||||||
|
|
||||||
if ($json) {
|
|
||||||
echo json_encode(['repo' => $repoName, 'issues' => $issues], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
|
||||||
} else {
|
|
||||||
$this->printIssues($repoName, $issues);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($fix && !empty($issues)) {
|
|
||||||
return $this->applyFixes($apiBase, $org, $repoName, $token, $current, $issues);
|
|
||||||
}
|
|
||||||
|
|
||||||
return empty($issues) ? 0 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =====================================================================
|
|
||||||
// Bulk org mode — check all repos
|
|
||||||
// =====================================================================
|
|
||||||
|
|
||||||
private function checkOrg(string $org, string $token, string $apiBase, bool $fix, bool $json, bool $quiet): int
|
|
||||||
{
|
|
||||||
$repos = $this->fetchOrgRepos($apiBase, $org, $token);
|
|
||||||
if ($repos === null) {
|
|
||||||
$this->log('ERROR', "Failed to fetch repos for org {$org}");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->log('INFO', "Manifest Integrity Check — {$org} (" . count($repos) . " repos)");
|
|
||||||
|
|
||||||
$allResults = [];
|
|
||||||
$totalIssues = 0;
|
|
||||||
$reposWithIssues = 0;
|
|
||||||
|
|
||||||
foreach ($repos as $repo) {
|
|
||||||
$name = $repo['name'];
|
|
||||||
$manifest = $this->fetchManifest($apiBase, $org, $name, $token);
|
|
||||||
|
|
||||||
if ($manifest === null) {
|
|
||||||
if (!$quiet) {
|
|
||||||
$this->log('WARN', "{$name}: no manifest");
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$issues = $this->validateManifestOnly($manifest, $name);
|
|
||||||
|
|
||||||
if (!empty($issues)) {
|
|
||||||
$reposWithIssues++;
|
|
||||||
$totalIssues += count($issues);
|
|
||||||
|
|
||||||
if ($json) {
|
|
||||||
$allResults[] = ['repo' => $name, 'issues' => $issues];
|
|
||||||
} else {
|
|
||||||
$this->printIssues($name, $issues);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($fix) {
|
|
||||||
$this->applyFixes($apiBase, $org, $name, $token, $manifest, $issues);
|
|
||||||
}
|
|
||||||
} elseif (!$quiet && !$json) {
|
|
||||||
$this->log('OK', "{$name}: clean");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($json) {
|
|
||||||
echo json_encode($allResults, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
|
||||||
} else {
|
|
||||||
echo "\n";
|
|
||||||
$level = $reposWithIssues > 0 ? 'WARN' : 'OK';
|
|
||||||
$this->log($level, sprintf(
|
|
||||||
'Summary: %d repos checked, %d with issues (%d total issues)',
|
|
||||||
count($repos),
|
|
||||||
$reposWithIssues,
|
|
||||||
$totalIssues
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
return $reposWithIssues > 0 ? 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =====================================================================
|
|
||||||
// Validation rules
|
|
||||||
// =====================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Full validation: compare API manifest against locally-detected fields.
|
|
||||||
*/
|
|
||||||
private function validate(array $current, array $detected, string $repoName): array
|
|
||||||
{
|
|
||||||
$issues = [];
|
|
||||||
|
|
||||||
// Required fields that should never be empty
|
|
||||||
$required = ['platform', 'name', 'version', 'package_type', 'language', 'entry_point'];
|
|
||||||
foreach ($required as $field) {
|
|
||||||
if (empty($current[$field])) {
|
|
||||||
$fix = $detected[$field] ?? null;
|
|
||||||
$issues[] = [
|
|
||||||
'field' => $field,
|
|
||||||
'severity' => 'error',
|
|
||||||
'message' => 'Missing required field',
|
|
||||||
'current' => '',
|
|
||||||
'fix' => $fix,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Drift detection: detected value differs from API
|
|
||||||
foreach ($detected as $field => $detectedValue) {
|
|
||||||
$currentValue = $current[$field] ?? '';
|
|
||||||
if ($detectedValue !== '' && $currentValue !== '' && $detectedValue !== $currentValue) {
|
|
||||||
// Version drift is expected on dev branches (suffix)
|
|
||||||
if ($field === 'version' && strpos($detectedValue, $currentValue) === 0) {
|
|
||||||
continue; // e.g., detected "02.34.50-dev" vs API "02.34.50"
|
|
||||||
}
|
|
||||||
if ($field === 'version' && strpos($currentValue, $detectedValue) === 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$issues[] = [
|
|
||||||
'field' => $field,
|
|
||||||
'severity' => 'warn',
|
|
||||||
'message' => 'Drift: source differs from manifest',
|
|
||||||
'current' => $currentValue,
|
|
||||||
'fix' => $detectedValue,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Platform-specific structure validation
|
|
||||||
$platform = $current['platform'] ?? '';
|
|
||||||
$issues = array_merge($issues, $this->validatePlatformStructure($platform, $current, $repoName));
|
|
||||||
|
|
||||||
return $issues;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* API-only validation: check manifest fields for completeness and consistency
|
|
||||||
* without access to source files.
|
|
||||||
*/
|
|
||||||
private function validateManifestOnly(array $manifest, string $repoName): array
|
|
||||||
{
|
|
||||||
$issues = [];
|
|
||||||
|
|
||||||
// Required fields
|
|
||||||
$required = ['platform', 'name', 'version', 'language'];
|
|
||||||
foreach ($required as $field) {
|
|
||||||
if (empty($manifest[$field])) {
|
|
||||||
$issues[] = [
|
|
||||||
'field' => $field,
|
|
||||||
'severity' => 'error',
|
|
||||||
'message' => 'Missing required field',
|
|
||||||
'current' => '',
|
|
||||||
'fix' => null,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recommended fields
|
|
||||||
$recommended = ['package_type', 'entry_point', 'license_spdx', 'description'];
|
|
||||||
foreach ($recommended as $field) {
|
|
||||||
if (empty($manifest[$field])) {
|
|
||||||
$issues[] = [
|
|
||||||
'field' => $field,
|
|
||||||
'severity' => 'info',
|
|
||||||
'message' => 'Recommended field is empty',
|
|
||||||
'current' => '',
|
|
||||||
'fix' => null,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Platform-specific checks
|
|
||||||
$platform = $manifest['platform'] ?? '';
|
|
||||||
$issues = array_merge($issues, $this->validatePlatformStructure($platform, $manifest, $repoName));
|
|
||||||
|
|
||||||
return $issues;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Platform-specific validation rules.
|
|
||||||
*/
|
|
||||||
private function validatePlatformStructure(string $platform, array $manifest, string $repoName): array
|
|
||||||
{
|
|
||||||
$issues = [];
|
|
||||||
|
|
||||||
switch ($platform) {
|
|
||||||
case 'joomla':
|
|
||||||
case 'waas-component':
|
|
||||||
// Joomla repos must have element_name
|
|
||||||
if (empty($manifest['element_name'])) {
|
|
||||||
$issues[] = [
|
|
||||||
'field' => 'element_name',
|
|
||||||
'severity' => 'error',
|
|
||||||
'message' => 'Joomla repos require element_name',
|
|
||||||
'current' => '',
|
|
||||||
'fix' => null,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
// Language should be PHP
|
|
||||||
if (!empty($manifest['language']) && $manifest['language'] !== 'PHP') {
|
|
||||||
$issues[] = [
|
|
||||||
'field' => 'language',
|
|
||||||
'severity' => 'warn',
|
|
||||||
'message' => 'Joomla repos should have language=PHP',
|
|
||||||
'current' => $manifest['language'],
|
|
||||||
'fix' => 'PHP',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'dolibarr':
|
|
||||||
case 'crm-module':
|
|
||||||
if (!empty($manifest['language']) && $manifest['language'] !== 'PHP') {
|
|
||||||
$issues[] = [
|
|
||||||
'field' => 'language',
|
|
||||||
'severity' => 'warn',
|
|
||||||
'message' => 'Dolibarr repos should have language=PHP',
|
|
||||||
'current' => $manifest['language'],
|
|
||||||
'fix' => 'PHP',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'go':
|
|
||||||
if (!empty($manifest['language']) && $manifest['language'] !== 'Go') {
|
|
||||||
$issues[] = [
|
|
||||||
'field' => 'language',
|
|
||||||
'severity' => 'warn',
|
|
||||||
'message' => 'Go repos should have language=Go',
|
|
||||||
'current' => $manifest['language'],
|
|
||||||
'fix' => 'Go',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'mcp':
|
|
||||||
if (!empty($manifest['language']) && !in_array($manifest['language'], ['TypeScript', 'JavaScript'], true)) {
|
|
||||||
$issues[] = [
|
|
||||||
'field' => 'language',
|
|
||||||
'severity' => 'warn',
|
|
||||||
'message' => 'MCP repos should have language=TypeScript or JavaScript',
|
|
||||||
'current' => $manifest['language'],
|
|
||||||
'fix' => null,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Version format check: should be XX.YY.ZZ
|
|
||||||
$version = $manifest['version'] ?? '';
|
|
||||||
if ($version !== '' && !preg_match('/^\d{2}\.\d{2}\.\d{2}/', $version)) {
|
|
||||||
// Allow semver for node/go repos
|
|
||||||
if (!in_array($platform, ['mcp', 'node', 'go'], true)) {
|
|
||||||
$issues[] = [
|
|
||||||
'field' => 'version',
|
|
||||||
'severity' => 'info',
|
|
||||||
'message' => 'Version does not match XX.YY.ZZ format',
|
|
||||||
'current' => $version,
|
|
||||||
'fix' => null,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $issues;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =====================================================================
|
|
||||||
// Output
|
|
||||||
// =====================================================================
|
|
||||||
|
|
||||||
private function printIssues(string $repoName, array $issues): void
|
|
||||||
{
|
|
||||||
if (empty($issues)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$errors = count(array_filter($issues, fn($i) => $i['severity'] === 'error'));
|
|
||||||
$warns = count(array_filter($issues, fn($i) => $i['severity'] === 'warn'));
|
|
||||||
$infos = count($issues) - $errors - $warns;
|
|
||||||
|
|
||||||
echo "\n";
|
|
||||||
$summary = [];
|
|
||||||
if ($errors > 0) $summary[] = "{$errors} error(s)";
|
|
||||||
if ($warns > 0) $summary[] = "{$warns} warning(s)";
|
|
||||||
if ($infos > 0) $summary[] = "{$infos} info";
|
|
||||||
$this->log($errors > 0 ? 'ERROR' : 'WARN', "{$repoName} — " . implode(', ', $summary));
|
|
||||||
|
|
||||||
foreach ($issues as $issue) {
|
|
||||||
$icon = match ($issue['severity']) {
|
|
||||||
'error' => 'ERROR',
|
|
||||||
'warn' => 'WARN',
|
|
||||||
default => 'INFO',
|
|
||||||
};
|
|
||||||
$msg = sprintf(' %-18s %s', $issue['field'], $issue['message']);
|
|
||||||
if ($issue['current'] !== '') {
|
|
||||||
$msg .= " (current: {$issue['current']})";
|
|
||||||
}
|
|
||||||
if ($issue['fix'] !== null) {
|
|
||||||
$msg .= " → fix: {$issue['fix']}";
|
|
||||||
}
|
|
||||||
$this->log($icon, $msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =====================================================================
|
|
||||||
// Fix application
|
|
||||||
// =====================================================================
|
|
||||||
|
|
||||||
private function applyFixes(string $apiBase, string $org, string $repo, string $token, array $current, array $issues): int
|
|
||||||
{
|
|
||||||
$fixes = [];
|
|
||||||
foreach ($issues as $issue) {
|
|
||||||
if ($issue['fix'] !== null && $issue['fix'] !== '') {
|
|
||||||
$fixes[$issue['field']] = $issue['fix'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($fixes)) {
|
|
||||||
$this->log('INFO', "{$repo}: no auto-fixable issues");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$merged = array_merge($current, $fixes);
|
|
||||||
$url = "{$apiBase}/repos/{$org}/{$repo}/manifest";
|
|
||||||
$payload = json_encode($merged);
|
|
||||||
|
|
||||||
$ctx = stream_context_create([
|
|
||||||
'http' => [
|
|
||||||
'method' => 'PUT',
|
|
||||||
'header' => "Authorization: token {$token}\r\nContent-Type: application/json\r\nAccept: application/json\r\n",
|
|
||||||
'content' => $payload,
|
|
||||||
'timeout' => 10,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$body = @file_get_contents($url, false, $ctx);
|
|
||||||
if ($body === false) {
|
|
||||||
$this->log('ERROR', "{$repo}: failed to push fixes");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->log('OK', "{$repo}: fixed " . implode(', ', array_keys($fixes)));
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =====================================================================
|
|
||||||
// API helpers
|
|
||||||
// =====================================================================
|
|
||||||
|
|
||||||
private function fetchManifest(string $apiBase, string $org, string $repo, string $token): ?array
|
|
||||||
{
|
|
||||||
$url = "{$apiBase}/repos/{$org}/{$repo}/manifest";
|
|
||||||
$ctx = stream_context_create([
|
|
||||||
'http' => [
|
|
||||||
'header' => "Authorization: token {$token}\r\nAccept: application/json\r\n",
|
|
||||||
'timeout' => 10,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$body = @file_get_contents($url, false, $ctx);
|
|
||||||
if ($body === false) return null;
|
|
||||||
|
|
||||||
$data = json_decode($body, true);
|
|
||||||
return is_array($data) ? $data : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function fetchOrgRepos(string $apiBase, string $org, string $token): ?array
|
|
||||||
{
|
|
||||||
$allRepos = [];
|
|
||||||
$page = 1;
|
|
||||||
$limit = 50;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
$url = "{$apiBase}/orgs/{$org}/repos?page={$page}&limit={$limit}";
|
|
||||||
$ctx = stream_context_create([
|
|
||||||
'http' => [
|
|
||||||
'header' => "Authorization: token {$token}\r\nAccept: application/json\r\n",
|
|
||||||
'timeout' => 15,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$body = @file_get_contents($url, false, $ctx);
|
|
||||||
if ($body === false) return null;
|
|
||||||
|
|
||||||
$repos = json_decode($body, true);
|
|
||||||
if (!is_array($repos) || empty($repos)) break;
|
|
||||||
|
|
||||||
$allRepos = array_merge($allRepos, $repos);
|
|
||||||
|
|
||||||
if (count($repos) < $limit) break;
|
|
||||||
$page++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter out archived and empty repos
|
|
||||||
return array_filter($allRepos, fn($r) => !($r['archived'] ?? false) && !($r['empty'] ?? false));
|
|
||||||
}
|
|
||||||
|
|
||||||
// =====================================================================
|
|
||||||
// Detection (delegates to manifest_detect logic)
|
|
||||||
// =====================================================================
|
|
||||||
|
|
||||||
private function runDetect(string $root, string $repoName): array
|
|
||||||
{
|
|
||||||
$script = __DIR__ . '/manifest_detect.php';
|
|
||||||
$redirect = PHP_OS_FAMILY === 'Windows' ? '2>NUL' : '2>/dev/null';
|
|
||||||
$cmd = sprintf(
|
|
||||||
'php %s --path %s --repo %s --json --quiet %s',
|
|
||||||
escapeshellarg($script),
|
|
||||||
escapeshellarg($root),
|
|
||||||
escapeshellarg($repoName),
|
|
||||||
$redirect
|
|
||||||
);
|
|
||||||
|
|
||||||
$output = shell_exec($cmd) ?? '';
|
|
||||||
|
|
||||||
// Extract JSON object from output (skip banner/log lines)
|
|
||||||
if (preg_match('/\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/s', $output, $m)) {
|
|
||||||
$data = json_decode($m[0], true);
|
|
||||||
if (is_array($data)) {
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function detectRepoName(string $root): string
|
|
||||||
{
|
|
||||||
$gitConfig = "{$root}/.git/config";
|
|
||||||
if (!file_exists($gitConfig)) {
|
|
||||||
return basename($root);
|
|
||||||
}
|
|
||||||
|
|
||||||
$content = file_get_contents($gitConfig);
|
|
||||||
if (preg_match('/url\s*=\s*.*\/([^\/\s]+?)(?:\.git)?\s*$/m', $content, $m)) {
|
|
||||||
return $m[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
return basename($root);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$app = new ManifestIntegrityCli();
|
|
||||||
exit($app->execute());
|
|
||||||
|
|||||||
+2
-278
@@ -1,280 +1,4 @@
|
|||||||
#!/usr/bin/env php
|
#!/usr/bin/env php
|
||||||
<?php
|
<?php
|
||||||
|
// Backward-compatibility wrapper — manifest_* renamed to metadata_*
|
||||||
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
require __DIR__ . '/metadata_licensing.php';
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* FILE INFORMATION
|
|
||||||
* DEFGROUP: mokoplatform.CLI
|
|
||||||
* INGROUP: mokoplatform
|
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
|
||||||
* PATH: /cli/manifest_licensing.php
|
|
||||||
* VERSION: 09.32.01
|
|
||||||
* BRIEF: Ensure licensing tags (updateservers, dlid) in Joomla extension manifests
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
|
||||||
|
|
||||||
use MokoCli\{CliFramework, SourceResolver};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the <licensing> block from .mokogitea/manifest.xml and ensures that the
|
|
||||||
* Joomla extension manifest contains the correct <updateservers> and <dlid> tags.
|
|
||||||
*
|
|
||||||
* manifest.xml licensing block example:
|
|
||||||
*
|
|
||||||
* <licensing>
|
|
||||||
* <enabled>true</enabled>
|
|
||||||
* <dlid>true</dlid>
|
|
||||||
* <update-server>https://git.mokoconsulting.tech/{org}/{repo}/updates.xml</update-server>
|
|
||||||
* <update-server-name>MyExtension Updates</update-server-name>
|
|
||||||
* </licensing>
|
|
||||||
*
|
|
||||||
* Supports {org} and {repo} placeholders in update-server URL, resolved from
|
|
||||||
* the manifest's <identity> block or git remote.
|
|
||||||
*/
|
|
||||||
class ManifestLicensingCli extends CliFramework
|
|
||||||
{
|
|
||||||
protected function configure(): void
|
|
||||||
{
|
|
||||||
$this->setDescription('Ensure licensing tags (updateservers, dlid) in Joomla extension manifests');
|
|
||||||
$this->addArgument('--path', 'Repository root path', '.');
|
|
||||||
$this->addArgument('--fix', 'Apply fixes (default: dry-run check only)', false);
|
|
||||||
$this->addArgument('--github-output', 'Write results to $GITHUB_OUTPUT', false);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function run(): int
|
|
||||||
{
|
|
||||||
$root = realpath($this->getArgument('--path')) ?: $this->getArgument('--path');
|
|
||||||
$fix = (bool) $this->getArgument('--fix');
|
|
||||||
$ghOutput = (bool) $this->getArgument('--github-output');
|
|
||||||
|
|
||||||
// ── 1. Read manifest.xml ──────────────────────────────────────────
|
|
||||||
$manifestFile = "{$root}/.mokogitea/manifest.xml";
|
|
||||||
|
|
||||||
if (!file_exists($manifestFile)) {
|
|
||||||
$this->log('WARN', "No manifest.xml found at {$manifestFile}");
|
|
||||||
$this->outputResult($ghOutput, 'skipped', 'No manifest.xml');
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$xml = @simplexml_load_file($manifestFile);
|
|
||||||
|
|
||||||
if ($xml === false) {
|
|
||||||
$this->log('ERROR', "Failed to parse {$manifestFile}");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 2. Check if licensing is enabled ──────────────────────────────
|
|
||||||
if (!isset($xml->licensing) || (string) ($xml->licensing->enabled ?? '') !== 'true') {
|
|
||||||
$this->log('INFO', 'Licensing not enabled in manifest.xml — skipping');
|
|
||||||
$this->outputResult($ghOutput, 'skipped', 'Licensing not enabled');
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$licensingNode = $xml->licensing;
|
|
||||||
$dlidEnabled = ((string) ($licensingNode->dlid ?? 'true')) === 'true';
|
|
||||||
$updateServerUrl = (string) ($licensingNode->{'update-server'} ?? '');
|
|
||||||
$updateServerName = (string) ($licensingNode->{'update-server-name'} ?? '');
|
|
||||||
|
|
||||||
// ── 3. Resolve placeholders ───────────────────────────────────────
|
|
||||||
$org = (string) ($xml->identity->org ?? '');
|
|
||||||
$repo = (string) ($xml->identity->name ?? '');
|
|
||||||
|
|
||||||
// Fallback to git remote if manifest doesn't have org/name
|
|
||||||
if (empty($org) || empty($repo)) {
|
|
||||||
$remote = trim((string) @shell_exec("cd " . escapeshellarg($root) . " && git remote get-url origin 2>/dev/null"));
|
|
||||||
|
|
||||||
if (preg_match('#[/:]([^/]+)/([^/.]+?)(?:\.git)?$#', $remote, $m)) {
|
|
||||||
if (empty($org)) {
|
|
||||||
$org = $m[1];
|
|
||||||
}
|
|
||||||
if (empty($repo)) {
|
|
||||||
$repo = $m[2];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default update server URL if not specified
|
|
||||||
if (empty($updateServerUrl) && !empty($org) && !empty($repo)) {
|
|
||||||
$updateServerUrl = "https://git.mokoconsulting.tech/{$org}/{$repo}/updates.xml";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve {org} and {repo} placeholders
|
|
||||||
$updateServerUrl = str_replace(['{org}', '{repo}'], [$org, $repo], $updateServerUrl);
|
|
||||||
|
|
||||||
// Default server name from display-name or repo name
|
|
||||||
if (empty($updateServerName)) {
|
|
||||||
$displayName = (string) ($xml->identity->{'display-name'} ?? $repo);
|
|
||||||
$updateServerName = $displayName . ' Updates';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($updateServerUrl)) {
|
|
||||||
$this->log('ERROR', 'Cannot determine update server URL — set <update-server> in manifest.xml or ensure org/repo are available');
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->log('INFO', "Licensing enabled — org={$org}, repo={$repo}");
|
|
||||||
$this->log('INFO', "Update server: {$updateServerUrl}");
|
|
||||||
$this->log('INFO', "DLID required: " . ($dlidEnabled ? 'yes' : 'no'));
|
|
||||||
|
|
||||||
// ── 4. Find Joomla extension manifests ────────────────────────────
|
|
||||||
$xmlFiles = array_merge(
|
|
||||||
SourceResolver::globSource($root, '*.xml'),
|
|
||||||
SourceResolver::globSource($root, 'packages/*/*.xml'),
|
|
||||||
glob("{$root}/*.xml") ?: []
|
|
||||||
);
|
|
||||||
|
|
||||||
$packageManifest = null;
|
|
||||||
|
|
||||||
foreach ($xmlFiles as $file) {
|
|
||||||
$content = file_get_contents($file);
|
|
||||||
|
|
||||||
if (!str_contains($content, '<extension')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the package manifest (type="package") or the main extension manifest
|
|
||||||
if (str_contains($content, 'type="package"')) {
|
|
||||||
$packageManifest = $file;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: first extension manifest found
|
|
||||||
if ($packageManifest === null) {
|
|
||||||
$packageManifest = $file;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($packageManifest === null) {
|
|
||||||
$this->log('WARN', 'No Joomla extension manifest found');
|
|
||||||
$this->outputResult($ghOutput, 'skipped', 'No extension manifest');
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$relPath = str_replace($root . '/', '', str_replace('\\', '/', $packageManifest));
|
|
||||||
$this->log('INFO', "Package manifest: {$relPath}");
|
|
||||||
|
|
||||||
// ── 5. Check and fix the manifest ─────────────────────────────────
|
|
||||||
$content = file_get_contents($packageManifest);
|
|
||||||
$original = $content;
|
|
||||||
$changes = [];
|
|
||||||
|
|
||||||
// --- 5a. Ensure <updateservers> block with correct URL ---
|
|
||||||
if (preg_match('#<updateservers>\s*</updateservers>#s', $content)) {
|
|
||||||
// Empty updateservers block — inject the server
|
|
||||||
$replacement = "<updateservers>\n"
|
|
||||||
. " <server type=\"extension\" name=\"{$updateServerName}\">{$updateServerUrl}</server>\n"
|
|
||||||
. " </updateservers>";
|
|
||||||
$content = preg_replace('#<updateservers>\s*</updateservers>#s', $replacement, $content);
|
|
||||||
$changes[] = 'Added update server URL to empty <updateservers>';
|
|
||||||
} elseif (!str_contains($content, '<updateservers>')) {
|
|
||||||
// No updateservers at all — add before </extension>
|
|
||||||
$serverBlock = "\n <updateservers>\n"
|
|
||||||
. " <server type=\"extension\" name=\"{$updateServerName}\">{$updateServerUrl}</server>\n"
|
|
||||||
. " </updateservers>\n";
|
|
||||||
$content = str_replace('</extension>', $serverBlock . '</extension>', $content);
|
|
||||||
$changes[] = 'Added <updateservers> block';
|
|
||||||
} else {
|
|
||||||
// updateservers exists — verify URL is correct
|
|
||||||
if (preg_match('#<server[^>]*>([^<]+)</server>#', $content, $m)) {
|
|
||||||
if ($m[1] !== $updateServerUrl) {
|
|
||||||
$content = preg_replace(
|
|
||||||
'#(<server[^>]*>)[^<]+(</server>)#',
|
|
||||||
"\${1}{$updateServerUrl}\${2}",
|
|
||||||
$content
|
|
||||||
);
|
|
||||||
$changes[] = "Updated server URL: {$m[1]} → {$updateServerUrl}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 5b. Ensure <dlid> tag if required ---
|
|
||||||
if ($dlidEnabled) {
|
|
||||||
if (!str_contains($content, '<dlid')) {
|
|
||||||
// Add before <updateservers> if present, otherwise before </extension>
|
|
||||||
$dlidTag = ' <dlid prefix="dlid=" suffix=""/>' . "\n";
|
|
||||||
|
|
||||||
if (str_contains($content, '<updateservers>')) {
|
|
||||||
$content = str_replace('<updateservers>', $dlidTag . "\n <updateservers>", $content);
|
|
||||||
} else {
|
|
||||||
$content = str_replace('</extension>', $dlidTag . '</extension>', $content);
|
|
||||||
}
|
|
||||||
|
|
||||||
$changes[] = 'Added <dlid> tag';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 5c. Ensure <blockChildUninstall> for packages ---
|
|
||||||
if (str_contains($content, 'type="package"') && !str_contains($content, '<blockChildUninstall>')) {
|
|
||||||
$blockTag = ' <blockChildUninstall>true</blockChildUninstall>' . "\n";
|
|
||||||
|
|
||||||
if (str_contains($content, '<dlid')) {
|
|
||||||
// Add after <dlid>
|
|
||||||
$content = preg_replace(
|
|
||||||
'#(<dlid[^/]*/>\s*\n)#',
|
|
||||||
"\${1}{$blockTag}",
|
|
||||||
$content
|
|
||||||
);
|
|
||||||
} elseif (str_contains($content, '<updateservers>')) {
|
|
||||||
$content = str_replace('<updateservers>', $blockTag . "\n <updateservers>", $content);
|
|
||||||
} else {
|
|
||||||
$content = str_replace('</extension>', $blockTag . '</extension>', $content);
|
|
||||||
}
|
|
||||||
|
|
||||||
$changes[] = 'Added <blockChildUninstall>true</blockChildUninstall>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 6. Report and apply ───────────────────────────────────────────
|
|
||||||
if (empty($changes)) {
|
|
||||||
$this->log('INFO', 'All licensing tags are correct — no changes needed');
|
|
||||||
$this->outputResult($ghOutput, 'ok', 'No changes needed');
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($changes as $change) {
|
|
||||||
$this->log($fix ? 'INFO' : 'WARN', ($fix ? 'Fixed: ' : 'Needs fix: ') . $change);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($fix) {
|
|
||||||
file_put_contents($packageManifest, $content);
|
|
||||||
$this->log('INFO', "Wrote {$relPath} with " . count($changes) . " change(s)");
|
|
||||||
$this->outputResult($ghOutput, 'fixed', implode('; ', $changes));
|
|
||||||
} else {
|
|
||||||
$this->log('WARN', 'Run with --fix to apply changes');
|
|
||||||
$this->outputResult($ghOutput, 'needs-fix', implode('; ', $changes));
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write result to $GITHUB_OUTPUT if requested.
|
|
||||||
*/
|
|
||||||
private function outputResult(bool $ghOutput, string $status, string $detail): void
|
|
||||||
{
|
|
||||||
if (!$ghOutput) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$outputFile = getenv('GITHUB_OUTPUT');
|
|
||||||
|
|
||||||
if ($outputFile === false || $outputFile === '') {
|
|
||||||
echo "licensing_status={$status}\n";
|
|
||||||
echo "licensing_detail={$detail}\n";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$fh = fopen($outputFile, 'a');
|
|
||||||
fwrite($fh, "licensing_status={$status}\n");
|
|
||||||
fwrite($fh, "licensing_detail={$detail}\n");
|
|
||||||
fclose($fh);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$app = new ManifestLicensingCli();
|
|
||||||
exit($app->execute());
|
|
||||||
|
|||||||
+2
-472
@@ -1,474 +1,4 @@
|
|||||||
#!/usr/bin/env php
|
#!/usr/bin/env php
|
||||||
<?php
|
<?php
|
||||||
|
// Backward-compatibility wrapper — manifest_* renamed to metadata_*
|
||||||
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
require __DIR__ . '/metadata_read.php';
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* FILE INFORMATION
|
|
||||||
* DEFGROUP: mokocli.CLI
|
|
||||||
* INGROUP: mokocli
|
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
|
||||||
* PATH: /cli/manifest_read.php
|
|
||||||
* VERSION: 09.32.01
|
|
||||||
* BRIEF: Read repo metadata from Gitea manifest API, auto-detect the rest
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
|
||||||
|
|
||||||
class ManifestReadCli extends CliFramework
|
|
||||||
{
|
|
||||||
/** Joomla extension XML element names searched in root and source/ dirs. */
|
|
||||||
private const JOOMLA_XML_ROOTS = ['extension', 'install'];
|
|
||||||
|
|
||||||
protected function configure(): void
|
|
||||||
{
|
|
||||||
$this->setDescription('Read repo metadata from Gitea API with auto-detection fallback');
|
|
||||||
$this->addArgument('--path', 'Repository root path', '.');
|
|
||||||
$this->addArgument('--field', 'Single field name to output', '');
|
|
||||||
$this->addArgument('--all', 'Print all fields as KEY=VALUE lines', false);
|
|
||||||
$this->addArgument('--github-output', 'Append all fields to $GITHUB_OUTPUT', false);
|
|
||||||
$this->addArgument('--json', 'Output all fields as JSON', false);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function run(): int
|
|
||||||
{
|
|
||||||
$path = $this->getArgument('--path');
|
|
||||||
$field = $this->getArgument('--field');
|
|
||||||
$showAll = $this->getArgument('--all');
|
|
||||||
$ghOut = $this->getArgument('--github-output');
|
|
||||||
$jsonMode = $this->getArgument('--json');
|
|
||||||
|
|
||||||
$mode = match (true) {
|
|
||||||
(bool) $ghOut => 'github-output',
|
|
||||||
(bool) $showAll => 'all',
|
|
||||||
(bool) $jsonMode => 'json',
|
|
||||||
default => 'field',
|
|
||||||
};
|
|
||||||
|
|
||||||
$root = realpath($path) ?: $path;
|
|
||||||
|
|
||||||
// ── 1. Resolve org/repo ──────────────────────────────────────────
|
|
||||||
[$org, $repo] = $this->resolveOrgRepo($root);
|
|
||||||
|
|
||||||
// ── 2. Primary: Gitea manifest API ───────────────────────────────
|
|
||||||
$fields = null;
|
|
||||||
if ($org !== '' && $repo !== '') {
|
|
||||||
$fields = $this->fetchFromApi($org, $repo);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 3. Fallback: auto-detect from source tree ────────────────────
|
|
||||||
if ($fields === null) {
|
|
||||||
$this->log('INFO', 'API unavailable — falling back to source-tree detection');
|
|
||||||
$fields = $this->autoDetect($root, $repo);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($fields)) {
|
|
||||||
$this->log('ERROR', "Could not resolve metadata for {$root}");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provide backward-compatible aliases (hyphenated → underscore)
|
|
||||||
$fields = $this->addAliases($fields);
|
|
||||||
|
|
||||||
// Strip empty values
|
|
||||||
$fields = array_filter($fields, fn($v) => $v !== '' && $v !== null);
|
|
||||||
|
|
||||||
// ── 4. Output ────────────────────────────────────────────────────
|
|
||||||
return $this->outputFields($fields, $mode, $field);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Gitea manifest API ───────────────────────────────────────────────
|
|
||||||
|
|
||||||
private function fetchFromApi(string $org, string $repo): ?array
|
|
||||||
{
|
|
||||||
$token = getenv('GA_TOKEN') ?: getenv('GITEA_TOKEN') ?: '';
|
|
||||||
$baseUrl = getenv('GITEA_URL') ?: 'https://git.mokoconsulting.tech';
|
|
||||||
$baseUrl = rtrim($baseUrl, '/');
|
|
||||||
|
|
||||||
if ($token === '') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$url = "{$baseUrl}/api/v1/repos/{$org}/{$repo}/manifest";
|
|
||||||
$ctx = stream_context_create([
|
|
||||||
'http' => [
|
|
||||||
'header' => "Authorization: token {$token}\r\nAccept: application/json\r\n",
|
|
||||||
'timeout' => 10,
|
|
||||||
'ignore_errors' => true,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$body = @file_get_contents($url, false, $ctx);
|
|
||||||
if ($body === false) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check HTTP status from response headers
|
|
||||||
$status = 0;
|
|
||||||
if (isset($http_response_header[0])) {
|
|
||||||
preg_match('/\d{3}/', $http_response_header[0], $m);
|
|
||||||
$status = (int) ($m[0] ?? 0);
|
|
||||||
}
|
|
||||||
if ($status < 200 || $status >= 300) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = json_decode($body, true);
|
|
||||||
if (!is_array($data) || empty($data)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->log('INFO', "Loaded metadata from Gitea manifest API ({$org}/{$repo})");
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Auto-detection fallback ──────────────────────────────────────────
|
|
||||||
|
|
||||||
private function autoDetect(string $root, string $repoName): array
|
|
||||||
{
|
|
||||||
$fields = [
|
|
||||||
'name' => $repoName ?: basename($root),
|
|
||||||
'org' => 'MokoConsulting',
|
|
||||||
];
|
|
||||||
|
|
||||||
// Resolve source directory (source/ or src/)
|
|
||||||
$srcDir = null;
|
|
||||||
foreach (['source', 'src'] as $candidate) {
|
|
||||||
if (is_dir("{$root}/{$candidate}")) {
|
|
||||||
$srcDir = $candidate;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Try Joomla detection ─────────────────────────────────────
|
|
||||||
$joomlaResult = $this->detectJoomla($root, $srcDir);
|
|
||||||
if ($joomlaResult !== null) {
|
|
||||||
$fields = array_merge($fields, $joomlaResult);
|
|
||||||
$this->log('INFO', "Auto-detected platform: joomla ({$fields['extension_type']} — {$fields['element_name']})");
|
|
||||||
return $fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Try Dolibarr detection ───────────────────────────────────
|
|
||||||
$dolibarrResult = $this->detectDolibarr($root);
|
|
||||||
if ($dolibarrResult !== null) {
|
|
||||||
$fields = array_merge($fields, $dolibarrResult);
|
|
||||||
$this->log('INFO', "Auto-detected platform: dolibarr");
|
|
||||||
return $fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Generic fallback ─────────────────────────────────────────
|
|
||||||
$fields['platform'] = $this->detectGenericPlatform($root);
|
|
||||||
$fields['element_name'] = strtolower($fields['name']);
|
|
||||||
$fields['extension_type'] = 'application';
|
|
||||||
$fields['language'] = $this->detectLanguage($root);
|
|
||||||
if ($srcDir !== null) {
|
|
||||||
$fields['entry_point'] = "{$srcDir}/";
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->log('INFO', "Auto-detected platform: {$fields['platform']}");
|
|
||||||
return $fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect Joomla platform by scanning for extension XML manifests.
|
|
||||||
*
|
|
||||||
* Searches root and source/ dirs for XML files containing <extension type="...">.
|
|
||||||
* Extracts element name from the filename (pkg_*, com_*, mod_*, plg_*, tpl_*) or
|
|
||||||
* from the <element> tag inside the manifest.
|
|
||||||
*/
|
|
||||||
private function detectJoomla(string $root, ?string $srcDir): ?array
|
|
||||||
{
|
|
||||||
$searchDirs = [$root];
|
|
||||||
if ($srcDir !== null) {
|
|
||||||
$searchDirs[] = "{$root}/{$srcDir}";
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($searchDirs as $dir) {
|
|
||||||
$xmlFiles = glob("{$dir}/*.xml") ?: [];
|
|
||||||
foreach ($xmlFiles as $xmlFile) {
|
|
||||||
$content = @file_get_contents($xmlFile);
|
|
||||||
if ($content === false) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match <extension type="component|module|plugin|package|template|file|library">
|
|
||||||
if (!preg_match('/<extension\s+[^>]*type="([^"]+)"/', $content, $typeMatch)) {
|
|
||||||
// Also try legacy <install type="...">
|
|
||||||
if (!preg_match('/<install\s+[^>]*type="([^"]+)"/', $content, $typeMatch)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$extType = strtolower($typeMatch[1]);
|
|
||||||
$basename = pathinfo($xmlFile, PATHINFO_FILENAME);
|
|
||||||
|
|
||||||
// Try to extract element name from XML <element> tag
|
|
||||||
$xml = @simplexml_load_string($content);
|
|
||||||
$element = '';
|
|
||||||
if ($xml !== false) {
|
|
||||||
// Package manifests have <files><file ...>element</file></files>
|
|
||||||
// Component/module manifests have <element> or use filename
|
|
||||||
$element = (string) ($xml->element ?? '');
|
|
||||||
if ($element === '') {
|
|
||||||
$element = strtolower($basename);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$element = strtolower($basename);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Derive display name
|
|
||||||
$displayName = (string) ($xml->name ?? ucfirst(str_replace('_', ' ', $basename)));
|
|
||||||
|
|
||||||
return [
|
|
||||||
'platform' => 'joomla',
|
|
||||||
'extension_type' => $extType,
|
|
||||||
'element_name' => $element,
|
|
||||||
'display_name' => $displayName,
|
|
||||||
'language' => 'PHP',
|
|
||||||
'entry_point' => ($srcDir ?? '.') . '/',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also check for pkg_*.xml pattern specifically
|
|
||||||
$pkgFiles = glob("{$dir}/pkg_*.xml") ?: [];
|
|
||||||
if (!empty($pkgFiles)) {
|
|
||||||
$basename = pathinfo($pkgFiles[0], PATHINFO_FILENAME);
|
|
||||||
return [
|
|
||||||
'platform' => 'joomla',
|
|
||||||
'extension_type' => 'package',
|
|
||||||
'element_name' => strtolower($basename),
|
|
||||||
'display_name' => ucfirst(str_replace('_', ' ', $basename)),
|
|
||||||
'language' => 'PHP',
|
|
||||||
'entry_point' => ($srcDir ?? '.') . '/',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for com_*/manifest.xml pattern (component subdirectory)
|
|
||||||
$comDirs = glob("{$root}/com_*", GLOB_ONLYDIR) ?: [];
|
|
||||||
foreach ($comDirs as $comDir) {
|
|
||||||
$comManifest = glob("{$comDir}/*.xml") ?: [];
|
|
||||||
foreach ($comManifest as $xmlFile) {
|
|
||||||
$content = @file_get_contents($xmlFile);
|
|
||||||
if ($content && preg_match('/<extension\s+[^>]*type="component"/', $content)) {
|
|
||||||
return [
|
|
||||||
'platform' => 'joomla',
|
|
||||||
'extension_type' => 'component',
|
|
||||||
'element_name' => strtolower(basename($comDir)),
|
|
||||||
'display_name' => ucfirst(str_replace('com_', '', basename($comDir))),
|
|
||||||
'language' => 'PHP',
|
|
||||||
'entry_point' => ($srcDir ?? '.') . '/',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect Dolibarr platform by scanning for module descriptor files.
|
|
||||||
*/
|
|
||||||
private function detectDolibarr(string $root): ?array
|
|
||||||
{
|
|
||||||
// Look for mod*.class.php containing DolibarrModules
|
|
||||||
$searchPaths = [
|
|
||||||
"{$root}/core/modules/mod*.class.php",
|
|
||||||
"{$root}/*/core/modules/mod*.class.php",
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($searchPaths as $pattern) {
|
|
||||||
$files = glob($pattern) ?: [];
|
|
||||||
foreach ($files as $file) {
|
|
||||||
$content = @file_get_contents($file);
|
|
||||||
if ($content && str_contains($content, 'DolibarrModules')) {
|
|
||||||
$modName = pathinfo($file, PATHINFO_FILENAME);
|
|
||||||
// modMyModule.class → mymodule
|
|
||||||
$element = strtolower(preg_replace('/^mod/', '', str_replace('.class', '', $modName)));
|
|
||||||
|
|
||||||
return [
|
|
||||||
'platform' => 'dolibarr',
|
|
||||||
'extension_type' => 'module',
|
|
||||||
'element_name' => $element,
|
|
||||||
'display_name' => ucfirst($element),
|
|
||||||
'language' => 'PHP',
|
|
||||||
'entry_point' => './',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Secondary: check for update.txt (Dolibarr marker)
|
|
||||||
if (file_exists("{$root}/update.txt")) {
|
|
||||||
return [
|
|
||||||
'platform' => 'dolibarr',
|
|
||||||
'extension_type' => 'module',
|
|
||||||
'element_name' => strtolower(basename($root)),
|
|
||||||
'display_name' => basename($root),
|
|
||||||
'language' => 'PHP',
|
|
||||||
'entry_point' => './',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect generic platform type (php, nodejs, python, etc.) from project files.
|
|
||||||
*/
|
|
||||||
private function detectGenericPlatform(string $root): string
|
|
||||||
{
|
|
||||||
if (file_exists("{$root}/composer.json")) {
|
|
||||||
return 'php';
|
|
||||||
}
|
|
||||||
if (file_exists("{$root}/package.json")) {
|
|
||||||
return 'nodejs';
|
|
||||||
}
|
|
||||||
if (file_exists("{$root}/pyproject.toml") || file_exists("{$root}/setup.py")) {
|
|
||||||
return 'python';
|
|
||||||
}
|
|
||||||
if (file_exists("{$root}/go.mod")) {
|
|
||||||
return 'go';
|
|
||||||
}
|
|
||||||
if (file_exists("{$root}/Cargo.toml")) {
|
|
||||||
return 'rust';
|
|
||||||
}
|
|
||||||
return 'generic';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect primary language from project files.
|
|
||||||
*/
|
|
||||||
private function detectLanguage(string $root): string
|
|
||||||
{
|
|
||||||
if (file_exists("{$root}/composer.json")) {
|
|
||||||
return 'PHP';
|
|
||||||
}
|
|
||||||
if (file_exists("{$root}/tsconfig.json")) {
|
|
||||||
return 'TypeScript';
|
|
||||||
}
|
|
||||||
if (file_exists("{$root}/package.json")) {
|
|
||||||
return 'JavaScript';
|
|
||||||
}
|
|
||||||
if (file_exists("{$root}/pyproject.toml") || file_exists("{$root}/setup.py")) {
|
|
||||||
return 'Python';
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Org/repo resolution ──────────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve org and repo name from environment or git remote.
|
|
||||||
*
|
|
||||||
* @return array{0: string, 1: string} [org, repo]
|
|
||||||
*/
|
|
||||||
private function resolveOrgRepo(string $root): array
|
|
||||||
{
|
|
||||||
// 1. GITHUB_REPOSITORY env (set in Gitea Actions / GitHub Actions)
|
|
||||||
$envRepo = getenv('GITHUB_REPOSITORY') ?: '';
|
|
||||||
if ($envRepo !== '' && str_contains($envRepo, '/')) {
|
|
||||||
return explode('/', $envRepo, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Parse git remote origin URL
|
|
||||||
$remoteUrl = trim((string) shell_exec(
|
|
||||||
'git -C ' . escapeshellarg($root) . ' remote get-url origin 2>/dev/null'
|
|
||||||
));
|
|
||||||
|
|
||||||
if ($remoteUrl !== '') {
|
|
||||||
// SSH: git@host:Org/Repo.git or HTTPS: https://host/Org/Repo.git
|
|
||||||
if (preg_match('#[/:]([^/]+)/([^/]+?)(?:\.git)?$#', $remoteUrl, $m)) {
|
|
||||||
return [$m[1], $m[2]];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['', basename($root)];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Backward-compatible aliases ──────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add hyphenated aliases for underscore fields (backward compat with old manifest.xml consumers).
|
|
||||||
* Also map old field names to new ones.
|
|
||||||
*/
|
|
||||||
private function addAliases(array $fields): array
|
|
||||||
{
|
|
||||||
// Map API field names → old manifest.xml hyphenated names
|
|
||||||
$aliases = [
|
|
||||||
'display_name' => 'display-name',
|
|
||||||
'license_spdx' => 'license-spdx',
|
|
||||||
'license_name' => 'license',
|
|
||||||
'standards_version' => 'standards-version',
|
|
||||||
'standards_source' => 'standards-source',
|
|
||||||
'extension_type' => 'package-type',
|
|
||||||
'entry_point' => 'entry-point',
|
|
||||||
'element_name' => 'name',
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($aliases as $newKey => $oldKey) {
|
|
||||||
if (isset($fields[$newKey]) && !isset($fields[$oldKey])) {
|
|
||||||
$fields[$oldKey] = $fields[$newKey];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Output ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private function outputFields(array $fields, string $mode, string $field): int
|
|
||||||
{
|
|
||||||
switch ($mode) {
|
|
||||||
case 'field':
|
|
||||||
if ($field === '') {
|
|
||||||
$this->log('ERROR', "Usage: manifest:read --path <dir> --field <name>");
|
|
||||||
$this->log('ERROR', " manifest:read --path <dir> --all");
|
|
||||||
$this->log('ERROR', " manifest:read --path <dir> --json");
|
|
||||||
$this->log('ERROR', " manifest:read --path <dir> --github-output");
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
echo ($fields[$field] ?? '') . "\n";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'all':
|
|
||||||
foreach ($fields as $k => $v) {
|
|
||||||
echo "{$k}={$v}\n";
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'json':
|
|
||||||
echo json_encode($fields, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'github-output':
|
|
||||||
$outputFile = getenv('GITHUB_OUTPUT') ?: getenv('GITEA_OUTPUT') ?: '';
|
|
||||||
$lines = [];
|
|
||||||
foreach ($fields as $k => $v) {
|
|
||||||
$envKey = str_replace('-', '_', $k);
|
|
||||||
$lines[$envKey] = "{$envKey}={$v}\n";
|
|
||||||
}
|
|
||||||
// Deduplicate (aliases may collide after underscore conversion)
|
|
||||||
$output = implode('', $lines);
|
|
||||||
|
|
||||||
if ($outputFile === '') {
|
|
||||||
$this->log('WARNING', 'GITHUB_OUTPUT not set — printing to stdout');
|
|
||||||
echo $output;
|
|
||||||
} else {
|
|
||||||
file_put_contents($outputFile, $output, FILE_APPEND);
|
|
||||||
$this->log('INFO', "Wrote " . count($lines) . " fields to GITHUB_OUTPUT");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$app = new ManifestReadCli();
|
|
||||||
exit($app->execute());
|
|
||||||
|
|||||||
@@ -0,0 +1,749 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* FILE INFORMATION
|
||||||
|
* DEFGROUP: mokoplatform.CLI
|
||||||
|
* INGROUP: mokoplatform
|
||||||
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
|
* PATH: /cli/manifest_detect.php
|
||||||
|
* VERSION: 09.26.02
|
||||||
|
* BRIEF: Auto-detect manifest fields from source files and optionally push to API
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
|
class ManifestDetectCli extends CliFramework
|
||||||
|
{
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this->setDescription('Auto-detect manifest fields from source files');
|
||||||
|
$this->addArgument('--path', 'Repository root path', '.');
|
||||||
|
$this->addArgument('--json', 'Output as JSON', false);
|
||||||
|
$this->addArgument('--diff', 'Show diff against current manifest API values', false);
|
||||||
|
$this->addArgument('--update', 'Push detected fields to manifest API', false);
|
||||||
|
$this->addArgument('--token', 'Gitea API token (or GITEA_TOKEN env)', '');
|
||||||
|
$this->addArgument('--api-base', 'Gitea API base URL', 'https://git.mokoconsulting.tech/api/v1');
|
||||||
|
$this->addArgument('--org', 'Gitea org', 'MokoConsulting');
|
||||||
|
$this->addArgument('--repo', 'Gitea repo name (auto-detected from remote if empty)', '');
|
||||||
|
$this->addArgument('--github-output', 'Append fields to $GITHUB_OUTPUT', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function run(): int
|
||||||
|
{
|
||||||
|
$path = $this->getArgument('--path');
|
||||||
|
$jsonMode = (bool) $this->getArgument('--json');
|
||||||
|
$diffMode = (bool) $this->getArgument('--diff');
|
||||||
|
$updateMode = (bool) $this->getArgument('--update');
|
||||||
|
$ghOutput = (bool) $this->getArgument('--github-output');
|
||||||
|
$token = $this->getArgument('--token') ?: getenv('GITEA_TOKEN') ?: '';
|
||||||
|
$apiBase = rtrim($this->getArgument('--api-base'), '/');
|
||||||
|
$org = $this->getArgument('--org');
|
||||||
|
$repoName = $this->getArgument('--repo');
|
||||||
|
|
||||||
|
$root = realpath($path) ?: $path;
|
||||||
|
|
||||||
|
if (!is_dir($root)) {
|
||||||
|
$this->log('ERROR', "Path does not exist: {$path}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-detect repo name from git remote
|
||||||
|
if ($repoName === '') {
|
||||||
|
$repoName = $this->detectRepoName($root);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Detect all fields ───────────────────────────────────────
|
||||||
|
$detected = $this->detectAll($root, $repoName);
|
||||||
|
|
||||||
|
// ── Warn about missing fields ────────────────────────────────
|
||||||
|
$expected = ['platform', 'name', 'version', 'package_type', 'language', 'entry_point'];
|
||||||
|
foreach ($expected as $field) {
|
||||||
|
if (!isset($detected[$field]) || $detected[$field] === '') {
|
||||||
|
$this->log('WARN', "Could not detect: {$field}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Output ──────────────────────────────────────────────────
|
||||||
|
if ($diffMode || $updateMode) {
|
||||||
|
if ($token === '') {
|
||||||
|
$this->log('ERROR', 'API token required for --diff/--update (use --token or GITEA_TOKEN env)');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if ($repoName === '') {
|
||||||
|
$this->log('ERROR', 'Could not determine repo name (use --repo)');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$current = $this->fetchManifest($apiBase, $org, $repoName, $token);
|
||||||
|
if ($current === null) {
|
||||||
|
$this->log('ERROR', 'Failed to fetch current manifest from API');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$changes = $this->computeDiff($current, $detected);
|
||||||
|
|
||||||
|
if ($diffMode) {
|
||||||
|
if (empty($changes)) {
|
||||||
|
$this->log('INFO', 'No differences — manifest matches source');
|
||||||
|
} else {
|
||||||
|
$this->sectionHeader('Manifest Drift');
|
||||||
|
foreach ($changes as $field => $info) {
|
||||||
|
$this->log('WARN', sprintf(
|
||||||
|
'%-20s API: %-30s Detected: %s',
|
||||||
|
$field,
|
||||||
|
$info['current'] === '' ? '(empty)' : $info['current'],
|
||||||
|
$info['detected']
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($updateMode) {
|
||||||
|
if (empty($changes)) {
|
||||||
|
$this->log('INFO', 'Nothing to update');
|
||||||
|
} else {
|
||||||
|
$update = array_map(fn($i) => $i['detected'], $changes);
|
||||||
|
$ok = $this->pushManifest($apiBase, $org, $repoName, $token, $current, $update);
|
||||||
|
if ($ok) {
|
||||||
|
$this->log('OK', 'Updated ' . count($update) . ' field(s): ' . implode(', ', array_keys($update)));
|
||||||
|
} else {
|
||||||
|
$this->log('ERROR', 'Failed to push manifest update');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ghOutput) {
|
||||||
|
$outputFile = getenv('GITHUB_OUTPUT');
|
||||||
|
$lines = [];
|
||||||
|
foreach ($detected as $k => $v) {
|
||||||
|
$envKey = str_replace('-', '_', $k);
|
||||||
|
$lines[] = "{$envKey}={$v}";
|
||||||
|
}
|
||||||
|
if ($outputFile !== false && $outputFile !== '') {
|
||||||
|
file_put_contents($outputFile, implode("\n", $lines) . "\n", FILE_APPEND);
|
||||||
|
$this->log('INFO', 'Wrote ' . count($detected) . ' fields to GITHUB_OUTPUT');
|
||||||
|
} else {
|
||||||
|
$this->log('WARN', 'GITHUB_OUTPUT not set — printing to stdout instead');
|
||||||
|
echo implode("\n", $lines) . "\n";
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($jsonMode) {
|
||||||
|
echo json_encode($detected, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
||||||
|
} else {
|
||||||
|
foreach ($detected as $k => $v) {
|
||||||
|
echo "{$k}={$v}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Detection engine
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
private function detectAll(string $root, string $repoName): array
|
||||||
|
{
|
||||||
|
$platform = $this->detectPlatform($root);
|
||||||
|
|
||||||
|
$fields = [
|
||||||
|
'platform' => $platform,
|
||||||
|
'name' => '',
|
||||||
|
'description' => '',
|
||||||
|
'version' => '',
|
||||||
|
'element_name' => '',
|
||||||
|
'package_type' => '',
|
||||||
|
'language' => '',
|
||||||
|
'entry_point' => '',
|
||||||
|
'license_spdx' => '',
|
||||||
|
'display_name' => '',
|
||||||
|
'target_version' => '',
|
||||||
|
'php_minimum' => '',
|
||||||
|
];
|
||||||
|
|
||||||
|
switch ($platform) {
|
||||||
|
case 'joomla':
|
||||||
|
$this->detectJoomla($root, $repoName, $fields);
|
||||||
|
break;
|
||||||
|
case 'dolibarr':
|
||||||
|
$this->detectDolibarr($root, $repoName, $fields);
|
||||||
|
break;
|
||||||
|
case 'go':
|
||||||
|
$this->detectGo($root, $repoName, $fields);
|
||||||
|
break;
|
||||||
|
case 'mcp':
|
||||||
|
$this->detectNode($root, $repoName, $fields);
|
||||||
|
break;
|
||||||
|
case 'node':
|
||||||
|
$this->detectNode($root, $repoName, $fields);
|
||||||
|
$fields['platform'] = 'node';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$this->detectGeneric($root, $repoName, $fields);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallbacks
|
||||||
|
if ($fields['name'] === '') {
|
||||||
|
$fields['name'] = $repoName ?: basename($root);
|
||||||
|
}
|
||||||
|
if ($fields['entry_point'] === '') {
|
||||||
|
$fields['entry_point'] = $this->detectEntryPoint($root);
|
||||||
|
}
|
||||||
|
if ($fields['license_spdx'] === '') {
|
||||||
|
$fields['license_spdx'] = $this->detectLicense($root);
|
||||||
|
}
|
||||||
|
// description: only from platform-specific source, never guessed
|
||||||
|
|
||||||
|
// Strip empty values
|
||||||
|
return array_filter($fields, fn($v) => $v !== '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Platform detection ──────────────────────────────────────────
|
||||||
|
|
||||||
|
private function detectPlatform(string $root): string
|
||||||
|
{
|
||||||
|
// Joomla: look for pkg_*.xml or extension XML in source dirs
|
||||||
|
$joomlaXmls = array_merge(
|
||||||
|
SourceResolver::globSource($root, 'pkg_*.xml'),
|
||||||
|
glob("{$root}/pkg_*.xml") ?: []
|
||||||
|
);
|
||||||
|
if (!empty($joomlaXmls)) {
|
||||||
|
return 'joomla';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check source dirs for any Joomla extension XML
|
||||||
|
foreach (SourceResolver::globSource($root, '*.xml') as $xmlFile) {
|
||||||
|
$content = file_get_contents($xmlFile);
|
||||||
|
if (strpos($content, '<extension') !== false) {
|
||||||
|
return 'joomla';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dolibarr: mod*.class.php with DolibarrModules
|
||||||
|
$modFiles = array_merge(
|
||||||
|
SourceResolver::globSource($root, 'core/modules/mod*.class.php'),
|
||||||
|
glob("{$root}/core/modules/mod*.class.php") ?: []
|
||||||
|
);
|
||||||
|
foreach ($modFiles as $file) {
|
||||||
|
if (strpos(file_get_contents($file), 'DolibarrModules') !== false) {
|
||||||
|
return 'dolibarr';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go
|
||||||
|
if (file_exists("{$root}/go.mod")) {
|
||||||
|
return 'go';
|
||||||
|
}
|
||||||
|
|
||||||
|
// MCP: package.json with mcp-related content
|
||||||
|
if (file_exists("{$root}/package.json")) {
|
||||||
|
$pkg = json_decode(file_get_contents("{$root}/package.json"), true) ?? [];
|
||||||
|
$deps = array_merge(
|
||||||
|
array_keys($pkg['dependencies'] ?? []),
|
||||||
|
array_keys($pkg['devDependencies'] ?? [])
|
||||||
|
);
|
||||||
|
foreach ($deps as $dep) {
|
||||||
|
if (strpos($dep, '@modelcontextprotocol/') === 0 || $dep === '@anthropic/mcp-sdk') {
|
||||||
|
return 'mcp';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'node';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Python
|
||||||
|
if (file_exists("{$root}/pyproject.toml") || file_exists("{$root}/setup.py")) {
|
||||||
|
return 'python';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'generic';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Joomla ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function detectJoomla(string $root, string $repoName, array &$fields): void
|
||||||
|
{
|
||||||
|
$fields['language'] = 'PHP';
|
||||||
|
|
||||||
|
// Find the primary extension manifest XML
|
||||||
|
$extManifest = $this->findJoomlaManifest($root);
|
||||||
|
if ($extManifest === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$xml = file_get_contents($extManifest);
|
||||||
|
|
||||||
|
// Type
|
||||||
|
$extType = '';
|
||||||
|
if (preg_match('/type="([^"]*)"/', $xml, $m)) {
|
||||||
|
$extType = $m[1];
|
||||||
|
}
|
||||||
|
$fields['package_type'] = $extType;
|
||||||
|
|
||||||
|
// Element name
|
||||||
|
$element = '';
|
||||||
|
if (preg_match('/<element>([^<]+)<\/element>/', $xml, $m)) {
|
||||||
|
$element = $m[1];
|
||||||
|
}
|
||||||
|
if ($element === '' && preg_match('/module="([^"]*)"/', $xml, $m)) {
|
||||||
|
$element = $m[1];
|
||||||
|
}
|
||||||
|
if ($element === '' && preg_match('/plugin="([^"]*)"/', $xml, $m)) {
|
||||||
|
$element = $m[1];
|
||||||
|
}
|
||||||
|
if ($extType === 'package' && preg_match('/<packagename>([^<]+)<\/packagename>/', $xml, $m)) {
|
||||||
|
$element = $m[1];
|
||||||
|
}
|
||||||
|
if ($element === '') {
|
||||||
|
$element = strtolower(basename($extManifest, '.xml'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure element has type prefix (API stores full element_name like pkg_mokosuiteclient)
|
||||||
|
$prefixMap = [
|
||||||
|
'package' => 'pkg_', 'component' => 'com_', 'module' => 'mod_',
|
||||||
|
'template' => 'tpl_', 'library' => 'lib_', 'file' => 'file_',
|
||||||
|
];
|
||||||
|
if (isset($prefixMap[$extType])) {
|
||||||
|
$prefix = $prefixMap[$extType];
|
||||||
|
// Only add prefix if not already present (check all known prefixes)
|
||||||
|
$hasPrefix = false;
|
||||||
|
foreach ($prefixMap as $p) {
|
||||||
|
if (strpos($element, $p) === 0) { $hasPrefix = true; break; }
|
||||||
|
}
|
||||||
|
if (strpos($element, 'plg_') === 0) { $hasPrefix = true; }
|
||||||
|
if (!$hasPrefix) {
|
||||||
|
$element = $prefix . $element;
|
||||||
|
}
|
||||||
|
} elseif ($extType === 'plugin') {
|
||||||
|
$folder = '';
|
||||||
|
if (preg_match('/group="([^"]*)"/', $xml, $gm)) {
|
||||||
|
$folder = $gm[1];
|
||||||
|
}
|
||||||
|
if ($folder !== '' && strpos($element, 'plg_') !== 0) {
|
||||||
|
$element = "plg_{$folder}_" . $element;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$fields['element_name'] = $element;
|
||||||
|
|
||||||
|
// Name
|
||||||
|
if (preg_match('/<name>([^<]+)<\/name>/', $xml, $m)) {
|
||||||
|
$fields['name'] = trim($m[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version
|
||||||
|
if (preg_match('/<version>([^<]+)<\/version>/', $xml, $m)) {
|
||||||
|
$fields['version'] = trim($m[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description
|
||||||
|
if (preg_match('/<description>([^<]+)<\/description>/', $xml, $m)) {
|
||||||
|
$desc = trim($m[1]);
|
||||||
|
// Skip language string keys like COM_MOKOSUITE_DESCRIPTION
|
||||||
|
if (strpos($desc, '_') === false || strlen($desc) > 60) {
|
||||||
|
$fields['description'] = $desc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display name for update feeds
|
||||||
|
if (!empty($fields['name'])) {
|
||||||
|
$name = $fields['name'];
|
||||||
|
// If name already has "Type - " prefix, use as-is
|
||||||
|
if (preg_match('/^(Package|Component|Module|Plugin|Template|Library)\s*-\s*/i', $name)) {
|
||||||
|
$fields['display_name'] = $name;
|
||||||
|
} elseif (!empty($extType)) {
|
||||||
|
$fields['display_name'] = ucfirst($extType) . ' - ' . $name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Target Joomla version
|
||||||
|
if (preg_match('/<targetplatform\s[^>]*version="([^"]+)"/', $xml, $m)) {
|
||||||
|
$fields['target_version'] = trim($m[1]);
|
||||||
|
} else {
|
||||||
|
// Default for Joomla 5/6
|
||||||
|
$fields['target_version'] = '(5|6)\..*';
|
||||||
|
}
|
||||||
|
|
||||||
|
// PHP minimum
|
||||||
|
if (preg_match('/<php_minimum>([^<]+)<\/php_minimum>/', $xml, $m)) {
|
||||||
|
$fields['php_minimum'] = trim($m[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// License
|
||||||
|
if (preg_match('/<license>([^<]+)<\/license>/', $xml, $m)) {
|
||||||
|
$fields['license_spdx'] = $this->normalizeLicense(trim($m[1]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function findJoomlaManifest(string $root): ?string
|
||||||
|
{
|
||||||
|
// Priority: pkg_*.xml (package manifest)
|
||||||
|
$pkgXmls = array_merge(
|
||||||
|
SourceResolver::globSource($root, 'pkg_*.xml'),
|
||||||
|
glob("{$root}/pkg_*.xml") ?: []
|
||||||
|
);
|
||||||
|
if (!empty($pkgXmls)) {
|
||||||
|
return $pkgXmls[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any extension XML in source dir
|
||||||
|
foreach (SourceResolver::globSource($root, '*.xml') as $file) {
|
||||||
|
$content = file_get_contents($file);
|
||||||
|
if (strpos($content, '<extension') !== false) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Root level
|
||||||
|
foreach (glob("{$root}/*.xml") ?: [] as $file) {
|
||||||
|
$content = file_get_contents($file);
|
||||||
|
if (strpos($content, '<extension') !== false) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Dolibarr ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function detectDolibarr(string $root, string $repoName, array &$fields): void
|
||||||
|
{
|
||||||
|
$fields['language'] = 'PHP';
|
||||||
|
$fields['package_type'] = 'dolibarr-module';
|
||||||
|
|
||||||
|
$modFile = $this->findDolibarrModule($root);
|
||||||
|
if ($modFile === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = file_get_contents($modFile);
|
||||||
|
|
||||||
|
// Element name from class file
|
||||||
|
$modBasename = basename($modFile, '.class.php');
|
||||||
|
$fields['element_name'] = strtolower(preg_replace('/^mod/', '', $modBasename));
|
||||||
|
|
||||||
|
// Name
|
||||||
|
if (preg_match('/\$this->name\s*=\s*[\'"]([^\'"]+)[\'"]/', $content, $m)) {
|
||||||
|
$fields['name'] = $m[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version
|
||||||
|
if (preg_match('/\$this->version\s*=\s*[\'"]([^\'"]+)[\'"]/', $content, $m)) {
|
||||||
|
$fields['version'] = $m[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description
|
||||||
|
if (preg_match('/\$this->description\s*=\s*[\'"]([^\'"]+)[\'"]/', $content, $m)) {
|
||||||
|
$desc = $m[1];
|
||||||
|
if (strpos($desc, '$') === false) {
|
||||||
|
$fields['description'] = $desc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// License
|
||||||
|
if (preg_match('/SPDX-License-Identifier:\s*(\S+)/', $content, $m)) {
|
||||||
|
$fields['license_spdx'] = $m[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function findDolibarrModule(string $root): ?string
|
||||||
|
{
|
||||||
|
$candidates = array_merge(
|
||||||
|
SourceResolver::globSource($root, 'core/modules/mod*.class.php'),
|
||||||
|
glob("{$root}/core/modules/mod*.class.php") ?: []
|
||||||
|
);
|
||||||
|
foreach ($candidates as $file) {
|
||||||
|
if (strpos(file_get_contents($file), 'DolibarrModules') !== false) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Go ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function detectGo(string $root, string $repoName, array &$fields): void
|
||||||
|
{
|
||||||
|
$fields['language'] = 'Go';
|
||||||
|
$fields['package_type'] = 'application';
|
||||||
|
$fields['entry_point'] = './';
|
||||||
|
|
||||||
|
$goMod = "{$root}/go.mod";
|
||||||
|
if (!file_exists($goMod)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = file_get_contents($goMod);
|
||||||
|
|
||||||
|
// Module path → name
|
||||||
|
if (preg_match('/^module\s+(\S+)/m', $content, $m)) {
|
||||||
|
$modulePath = $m[1];
|
||||||
|
$parts = explode('/', $modulePath);
|
||||||
|
$fields['name'] = end($parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go version
|
||||||
|
if (preg_match('/^go\s+(\S+)/m', $content, $m)) {
|
||||||
|
// This is Go language version, not the project version
|
||||||
|
// Project version comes from git tags or source files
|
||||||
|
}
|
||||||
|
|
||||||
|
// License
|
||||||
|
$fields['license_spdx'] = $this->detectLicense($root);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Node / MCP ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function detectNode(string $root, string $repoName, array &$fields): void
|
||||||
|
{
|
||||||
|
$pkgFile = "{$root}/package.json";
|
||||||
|
if (!file_exists($pkgFile)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pkg = json_decode(file_get_contents($pkgFile), true) ?? [];
|
||||||
|
|
||||||
|
$fields['name'] = $pkg['name'] ?? '';
|
||||||
|
// Strip npm scope
|
||||||
|
if (strpos($fields['name'], '/') !== false) {
|
||||||
|
$fields['name'] = explode('/', $fields['name'])[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
$fields['version'] = $pkg['version'] ?? '';
|
||||||
|
$fields['description'] = $pkg['description'] ?? '';
|
||||||
|
$fields['license_spdx'] = $pkg['license'] ?? '';
|
||||||
|
|
||||||
|
// Language detection
|
||||||
|
if (file_exists("{$root}/tsconfig.json")) {
|
||||||
|
$fields['language'] = 'TypeScript';
|
||||||
|
} else {
|
||||||
|
$fields['language'] = 'JavaScript';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Package type
|
||||||
|
$deps = array_merge(
|
||||||
|
array_keys($pkg['dependencies'] ?? []),
|
||||||
|
array_keys($pkg['devDependencies'] ?? [])
|
||||||
|
);
|
||||||
|
$isMcp = false;
|
||||||
|
foreach ($deps as $dep) {
|
||||||
|
if (strpos($dep, '@modelcontextprotocol/') === 0 || $dep === '@anthropic/mcp-sdk') {
|
||||||
|
$isMcp = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$fields['package_type'] = $isMcp ? 'mcp-server' : 'application';
|
||||||
|
|
||||||
|
// Entry point
|
||||||
|
if (file_exists("{$root}/dist")) {
|
||||||
|
$fields['entry_point'] = 'dist/';
|
||||||
|
} elseif (file_exists("{$root}/src")) {
|
||||||
|
$fields['entry_point'] = 'src/';
|
||||||
|
} else {
|
||||||
|
$fields['entry_point'] = './';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Generic ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function detectGeneric(string $root, string $repoName, array &$fields): void
|
||||||
|
{
|
||||||
|
$fields['package_type'] = 'generic';
|
||||||
|
|
||||||
|
// Try to detect language from file extensions
|
||||||
|
$fields['language'] = $this->detectLanguageFromFiles($root);
|
||||||
|
$fields['license_spdx'] = $this->detectLicense($root);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Shared detection helpers
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
private function detectEntryPoint(string $root): string
|
||||||
|
{
|
||||||
|
$abs = SourceResolver::resolveAbsolute($root);
|
||||||
|
if ($abs !== null) {
|
||||||
|
return basename($abs) . '/';
|
||||||
|
}
|
||||||
|
if (is_dir("{$root}/dist")) return 'dist/';
|
||||||
|
if (is_dir("{$root}/src")) return 'src/';
|
||||||
|
return './';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function detectLicense(string $root): string
|
||||||
|
{
|
||||||
|
// Check LICENSE file
|
||||||
|
foreach (['LICENSE', 'LICENSE.md', 'LICENSE.txt', 'COPYING'] as $name) {
|
||||||
|
$file = "{$root}/{$name}";
|
||||||
|
if (!file_exists($file)) continue;
|
||||||
|
$content = file_get_contents($file);
|
||||||
|
|
||||||
|
// SPDX header
|
||||||
|
if (preg_match('/SPDX-License-Identifier:\s*(\S+)/', $content, $m)) {
|
||||||
|
return $m[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common license patterns
|
||||||
|
if (strpos($content, 'GNU GENERAL PUBLIC LICENSE') !== false) {
|
||||||
|
if (strpos($content, 'Version 3') !== false) return 'GPL-3.0-or-later';
|
||||||
|
if (strpos($content, 'Version 2') !== false) return 'GPL-2.0-or-later';
|
||||||
|
}
|
||||||
|
if (strpos($content, 'MIT License') !== false) return 'MIT';
|
||||||
|
if (strpos($content, 'Apache License') !== false && strpos($content, 'Version 2.0') !== false) return 'Apache-2.0';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private function detectLanguageFromFiles(string $root): string
|
||||||
|
{
|
||||||
|
$counts = ['PHP' => 0, 'Go' => 0, 'TypeScript' => 0, 'JavaScript' => 0, 'Python' => 0, 'Shell' => 0];
|
||||||
|
|
||||||
|
$extensions = [
|
||||||
|
'php' => 'PHP', 'go' => 'Go', 'ts' => 'TypeScript',
|
||||||
|
'js' => 'JavaScript', 'py' => 'Python', 'sh' => 'Shell',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Quick scan: only check top two levels
|
||||||
|
foreach (glob("{$root}/*") ?: [] as $item) {
|
||||||
|
$ext = pathinfo($item, PATHINFO_EXTENSION);
|
||||||
|
if (isset($extensions[$ext])) {
|
||||||
|
$counts[$extensions[$ext]]++;
|
||||||
|
}
|
||||||
|
if (is_dir($item) && basename($item)[0] !== '.') {
|
||||||
|
foreach (glob("{$item}/*") ?: [] as $subItem) {
|
||||||
|
$ext = pathinfo($subItem, PATHINFO_EXTENSION);
|
||||||
|
if (isset($extensions[$ext])) {
|
||||||
|
$counts[$extensions[$ext]]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
arsort($counts);
|
||||||
|
$top = key($counts);
|
||||||
|
return $counts[$top] > 0 ? $top : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeLicense(string $license): string
|
||||||
|
{
|
||||||
|
$lower = strtolower($license);
|
||||||
|
$isGpl = strpos($lower, 'gpl') !== false || strpos($lower, 'general public license') !== false;
|
||||||
|
if ($isGpl && strpos($lower, '3') !== false) return 'GPL-3.0-or-later';
|
||||||
|
if ($isGpl && strpos($lower, '2') !== false) return 'GPL-2.0-or-later';
|
||||||
|
if ($lower === 'mit' || strpos($lower, 'mit license') !== false) return 'MIT';
|
||||||
|
if (strpos($lower, 'apache') !== false) return 'Apache-2.0';
|
||||||
|
return $license;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function detectRepoName(string $root): string
|
||||||
|
{
|
||||||
|
$gitConfig = "{$root}/.git/config";
|
||||||
|
if (!file_exists($gitConfig)) {
|
||||||
|
return basename($root);
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = file_get_contents($gitConfig);
|
||||||
|
if (preg_match('/url\s*=\s*.*\/([^\/\s]+?)(?:\.git)?\s*$/m', $content, $m)) {
|
||||||
|
return $m[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return basename($root);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// API interaction
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
private function fetchManifest(string $apiBase, string $org, string $repo, string $token): ?array
|
||||||
|
{
|
||||||
|
$url = "{$apiBase}/repos/{$org}/{$repo}/manifest";
|
||||||
|
$ctx = stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'header' => "Authorization: token {$token}\r\nAccept: application/json\r\n",
|
||||||
|
'timeout' => 10,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$body = @file_get_contents($url, false, $ctx);
|
||||||
|
if ($body === false) return null;
|
||||||
|
|
||||||
|
return json_decode($body, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function computeDiff(array $current, array $detected): array
|
||||||
|
{
|
||||||
|
// Map detected keys to API keys (underscores match)
|
||||||
|
$changes = [];
|
||||||
|
|
||||||
|
foreach ($detected as $key => $value) {
|
||||||
|
$apiKey = $key;
|
||||||
|
$currentVal = $current[$apiKey] ?? '';
|
||||||
|
|
||||||
|
// Only flag as changed if detected value is non-empty and differs
|
||||||
|
if ($value !== '' && $value !== $currentVal) {
|
||||||
|
// Don't overwrite a non-empty API value with a detected value
|
||||||
|
// unless the API value is actually empty
|
||||||
|
if ($currentVal === '' || $this->shouldOverride($key, $currentVal, $value)) {
|
||||||
|
$changes[$key] = [
|
||||||
|
'current' => $currentVal,
|
||||||
|
'detected' => $value,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function shouldOverride(string $field, string $current, string $detected): bool
|
||||||
|
{
|
||||||
|
// Version: detected from source is authoritative
|
||||||
|
if ($field === 'version') return true;
|
||||||
|
|
||||||
|
// These fields: source files are authoritative
|
||||||
|
if (in_array($field, ['element_name', 'package_type', 'language', 'entry_point'], true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other fields, only fill empty — don't overwrite manual edits
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function pushManifest(string $apiBase, string $org, string $repo, string $token, array $current, array $update): bool
|
||||||
|
{
|
||||||
|
$merged = array_merge($current, $update);
|
||||||
|
$url = "{$apiBase}/repos/{$org}/{$repo}/manifest";
|
||||||
|
$payload = json_encode($merged);
|
||||||
|
|
||||||
|
$ctx = stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'method' => 'PUT',
|
||||||
|
'header' => "Authorization: token {$token}\r\nContent-Type: application/json\r\nAccept: application/json\r\n",
|
||||||
|
'content' => $payload,
|
||||||
|
'timeout' => 10,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$body = @file_get_contents($url, false, $ctx);
|
||||||
|
return $body !== false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$app = new ManifestDetectCli();
|
||||||
|
exit($app->execute());
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* FILE INFORMATION
|
||||||
|
* DEFGROUP: mokoplatform.CLI
|
||||||
|
* INGROUP: mokoplatform
|
||||||
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
|
* PATH: /cli/manifest_element.php
|
||||||
|
* BRIEF: Extract element name, type, type prefix, and ZIP name from manifest
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
|
class ManifestElementCli extends CliFramework
|
||||||
|
{
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this->setDescription('Extract element name, type, type prefix, and ZIP name from manifest');
|
||||||
|
$this->addArgument('--path', 'Repository root', '.');
|
||||||
|
$this->addArgument('--version', 'Version string', null);
|
||||||
|
$this->addArgument('--stability', 'Stability level', 'stable');
|
||||||
|
$this->addArgument('--repo', 'Repository name', '');
|
||||||
|
$this->addArgument('--github-output', 'Export results to $GITHUB_OUTPUT', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function run(): int
|
||||||
|
{
|
||||||
|
$path = $this->getArgument('--path');
|
||||||
|
$version = $this->getArgument('--version');
|
||||||
|
$stability = $this->getArgument('--stability');
|
||||||
|
$repoName = $this->getArgument('--repo');
|
||||||
|
$githubOutput = (bool) $this->getArgument('--github-output');
|
||||||
|
$root = realpath($path) ?: $path;
|
||||||
|
$platform = 'generic';
|
||||||
|
$manifestXml = "{$root}/.mokogitea/manifest.xml";
|
||||||
|
if (file_exists($manifestXml)) {
|
||||||
|
$content = file_get_contents($manifestXml);
|
||||||
|
if (preg_match('/<platform>([^<]+)<\/platform>/', $content, $pm)) {
|
||||||
|
$platform = trim($pm[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$extManifest = null;
|
||||||
|
$manifestFiles = array_merge(SourceResolver::globSource($root, 'pkg_*.xml'), SourceResolver::globSource($root, '*.xml'), glob("{$root}/*.xml") ?: []);
|
||||||
|
foreach ($manifestFiles as $file) {
|
||||||
|
$c = file_get_contents($file);
|
||||||
|
if (strpos($c, '<extension') !== false) {
|
||||||
|
$extManifest = $file;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$modFile = null;
|
||||||
|
$modFiles = array_merge(
|
||||||
|
SourceResolver::globSource($root, 'core/modules/mod*.class.php'),
|
||||||
|
glob("{$root}/core/modules/mod*.class.php") ?: []
|
||||||
|
);
|
||||||
|
foreach ($modFiles as $file) {
|
||||||
|
$c = file_get_contents($file);
|
||||||
|
if (strpos($c, 'extends DolibarrModules') !== false) {
|
||||||
|
$modFile = $file;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$extElement = '';
|
||||||
|
$extType = '';
|
||||||
|
$extFolder = '';
|
||||||
|
$extName = '';
|
||||||
|
switch (true) {
|
||||||
|
case in_array($platform, ['joomla', 'waas-component'], true) && $extManifest !== null:
|
||||||
|
$xml = file_get_contents($extManifest);
|
||||||
|
if (preg_match('/type="([^"]*)"/', $xml, $tm)) {
|
||||||
|
$extType = $tm[1];
|
||||||
|
}
|
||||||
|
if (preg_match('/group="([^"]*)"/', $xml, $gm)) {
|
||||||
|
$extFolder = $gm[1];
|
||||||
|
}
|
||||||
|
if (preg_match('/<element>([^<]+)<\/element>/', $xml, $em)) {
|
||||||
|
$extElement = $em[1];
|
||||||
|
}
|
||||||
|
if (empty($extElement) && preg_match('/module="([^"]*)"/', $xml, $mm)) {
|
||||||
|
$extElement = $mm[1];
|
||||||
|
}
|
||||||
|
if (empty($extElement) && preg_match('/plugin="([^"]*)"/', $xml, $pm2)) {
|
||||||
|
$extElement = $pm2[1];
|
||||||
|
}
|
||||||
|
if ($extType === 'package' && preg_match('/<packagename>([^<]+)<\/packagename>/', $xml, $pn)) {
|
||||||
|
$extElement = $pn[1];
|
||||||
|
}
|
||||||
|
if (empty($extElement)) {
|
||||||
|
$extElement = strtolower(basename($extManifest, '.xml'));
|
||||||
|
if (in_array($extElement, ['templatedetails', 'manifest'], true)) {
|
||||||
|
$extElement = strtolower(str_replace([' ', '-'], '', $repoName ?: basename($root)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (preg_match('/<name>([^<]+)<\/name>/', $xml, $nm)) {
|
||||||
|
$extName = trim($nm[1]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case in_array($platform, ['dolibarr', 'crm-module'], true) && $modFile !== null:
|
||||||
|
$extType = 'dolibarr-module';
|
||||||
|
$modBasename = basename($modFile, '.class.php');
|
||||||
|
$extElement = strtolower(preg_replace('/^mod/', '', $modBasename));
|
||||||
|
$modContent = file_get_contents($modFile);
|
||||||
|
if (preg_match('/\$this->name\s*=\s*[\'"]([^\'"]+)[\'"]/', $modContent, $nm)) {
|
||||||
|
$extName = $nm[1];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$extElement = strtolower(str_replace([' ', '-'], '', $repoName ?: basename($root)));
|
||||||
|
$extType = 'generic';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$extElement = preg_replace('/^(pkg_|com_|mod_|plg_[a-z]+_|tpl_|lib_)/', '', $extElement);
|
||||||
|
$typePrefix = '';
|
||||||
|
switch ($extType) {
|
||||||
|
case 'plugin':
|
||||||
|
$typePrefix = "plg_{$extFolder}_";
|
||||||
|
break;
|
||||||
|
case 'module':
|
||||||
|
$typePrefix = 'mod_';
|
||||||
|
break;
|
||||||
|
case 'component':
|
||||||
|
$typePrefix = 'com_';
|
||||||
|
break;
|
||||||
|
case 'template':
|
||||||
|
$typePrefix = 'tpl_';
|
||||||
|
break;
|
||||||
|
case 'library':
|
||||||
|
$typePrefix = 'lib_';
|
||||||
|
break;
|
||||||
|
case 'package':
|
||||||
|
$typePrefix = 'pkg_';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$suffixMap = [
|
||||||
|
'development' => '-dev',
|
||||||
|
'dev' => '-dev',
|
||||||
|
'alpha' => '-alpha',
|
||||||
|
'beta' => '-beta',
|
||||||
|
'rc' => '-rc',
|
||||||
|
'release-candidate' => '-rc',
|
||||||
|
'stable' => '',
|
||||||
|
];
|
||||||
|
$suffix = $suffixMap[$stability] ?? '';
|
||||||
|
$zipName = '';
|
||||||
|
if ($version !== null) {
|
||||||
|
$zipName = "{$typePrefix}{$extElement}-{$version}{$suffix}.zip";
|
||||||
|
}
|
||||||
|
if (empty($extName)) {
|
||||||
|
$extName = $repoName ?: basename($root);
|
||||||
|
}
|
||||||
|
$outputs = [
|
||||||
|
'platform' => $platform,
|
||||||
|
'ext_element' => $extElement,
|
||||||
|
'ext_type' => $extType,
|
||||||
|
'ext_folder' => $extFolder,
|
||||||
|
'ext_name' => $extName,
|
||||||
|
'type_prefix' => $typePrefix,
|
||||||
|
'zip_name' => $zipName,
|
||||||
|
];
|
||||||
|
if ($githubOutput) {
|
||||||
|
$ghOutput = getenv('GITHUB_OUTPUT');
|
||||||
|
$lines = [];
|
||||||
|
foreach ($outputs as $key => $value) {
|
||||||
|
$lines[] = "{$key}={$value}";
|
||||||
|
}
|
||||||
|
if ($ghOutput) {
|
||||||
|
file_put_contents($ghOutput, implode("\n", $lines) . "\n", FILE_APPEND);
|
||||||
|
} else {
|
||||||
|
foreach ($outputs as $key => $value) {
|
||||||
|
echo "::set-output name={$key}::{$value}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
foreach ($outputs as $key => $value) {
|
||||||
|
echo "{$key}={$value}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$app = new ManifestElementCli();
|
||||||
|
exit($app->execute());
|
||||||
@@ -0,0 +1,564 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* FILE INFORMATION
|
||||||
|
* DEFGROUP: mokoplatform.CLI
|
||||||
|
* INGROUP: mokoplatform
|
||||||
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
|
* PATH: /cli/manifest_integrity.php
|
||||||
|
* VERSION: 09.26.02
|
||||||
|
* BRIEF: Cross-check manifest API fields against repo contents across the org
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
|
class ManifestIntegrityCli extends CliFramework
|
||||||
|
{
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this->setDescription('Cross-check manifest fields against repo contents across the org');
|
||||||
|
$this->addArgument('--path', 'Single repo path (local mode)', '');
|
||||||
|
$this->addArgument('--org', 'Gitea org (bulk mode)', 'MokoConsulting');
|
||||||
|
$this->addArgument('--repo', 'Single repo name (remote mode)', '');
|
||||||
|
$this->addArgument('--token', 'Gitea API token (or GITEA_TOKEN env)', '');
|
||||||
|
$this->addArgument('--api-base', 'Gitea API base URL', 'https://git.mokoconsulting.tech/api/v1');
|
||||||
|
$this->addArgument('--fix', 'Push fixes for detected drift', false);
|
||||||
|
$this->addArgument('--json', 'Output as JSON', false);
|
||||||
|
$this->addArgument('--quiet', 'Only show repos with issues', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function run(): int
|
||||||
|
{
|
||||||
|
$path = $this->getArgument('--path');
|
||||||
|
$org = $this->getArgument('--org');
|
||||||
|
$repoName = $this->getArgument('--repo');
|
||||||
|
$token = $this->getArgument('--token') ?: getenv('GITEA_TOKEN') ?: '';
|
||||||
|
$apiBase = rtrim($this->getArgument('--api-base'), '/');
|
||||||
|
$fixMode = (bool) $this->getArgument('--fix');
|
||||||
|
$jsonMode = (bool) $this->getArgument('--json');
|
||||||
|
$quiet = (bool) $this->getArgument('--quiet');
|
||||||
|
|
||||||
|
if ($token === '') {
|
||||||
|
$this->log('ERROR', 'API token required (use --token or GITEA_TOKEN env)');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Mode selection ──────────────────────────────────────────
|
||||||
|
if ($path !== '') {
|
||||||
|
// Local mode: detect from source + compare to API
|
||||||
|
return $this->checkLocal($path, $org, $repoName, $token, $apiBase, $fixMode, $jsonMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($repoName !== '') {
|
||||||
|
// Single remote repo
|
||||||
|
return $this->checkRemoteRepo($org, $repoName, $token, $apiBase, $fixMode, $jsonMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bulk mode: all repos in org
|
||||||
|
return $this->checkOrg($org, $token, $apiBase, $fixMode, $jsonMode, $quiet);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Local mode — detect from source, compare to API
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
private function checkLocal(string $path, string $org, string $repoName, string $token, string $apiBase, bool $fix, bool $json): int
|
||||||
|
{
|
||||||
|
$root = realpath($path) ?: $path;
|
||||||
|
if (!is_dir($root)) {
|
||||||
|
$this->log('ERROR', "Path does not exist: {$path}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($repoName === '') {
|
||||||
|
$repoName = $this->detectRepoName($root);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run manifest_detect logic
|
||||||
|
$detected = $this->runDetect($root, $repoName);
|
||||||
|
$current = $this->fetchManifest($apiBase, $org, $repoName, $token);
|
||||||
|
|
||||||
|
if ($current === null) {
|
||||||
|
$this->log('ERROR', "Failed to fetch manifest for {$org}/{$repoName}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$issues = $this->validate($current, $detected, $repoName);
|
||||||
|
|
||||||
|
if ($json) {
|
||||||
|
echo json_encode(['repo' => $repoName, 'issues' => $issues], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
||||||
|
} else {
|
||||||
|
$this->printIssues($repoName, $issues);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($fix && !empty($issues)) {
|
||||||
|
return $this->applyFixes($apiBase, $org, $repoName, $token, $current, $issues);
|
||||||
|
}
|
||||||
|
|
||||||
|
return empty($issues) ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Remote single repo mode — fetch source files via API
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
private function checkRemoteRepo(string $org, string $repoName, string $token, string $apiBase, bool $fix, bool $json): int
|
||||||
|
{
|
||||||
|
$current = $this->fetchManifest($apiBase, $org, $repoName, $token);
|
||||||
|
if ($current === null) {
|
||||||
|
$this->log('ERROR', "Failed to fetch manifest for {$org}/{$repoName}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$issues = $this->validateManifestOnly($current, $repoName);
|
||||||
|
|
||||||
|
if ($json) {
|
||||||
|
echo json_encode(['repo' => $repoName, 'issues' => $issues], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
||||||
|
} else {
|
||||||
|
$this->printIssues($repoName, $issues);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($fix && !empty($issues)) {
|
||||||
|
return $this->applyFixes($apiBase, $org, $repoName, $token, $current, $issues);
|
||||||
|
}
|
||||||
|
|
||||||
|
return empty($issues) ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Bulk org mode — check all repos
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
private function checkOrg(string $org, string $token, string $apiBase, bool $fix, bool $json, bool $quiet): int
|
||||||
|
{
|
||||||
|
$repos = $this->fetchOrgRepos($apiBase, $org, $token);
|
||||||
|
if ($repos === null) {
|
||||||
|
$this->log('ERROR', "Failed to fetch repos for org {$org}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log('INFO', "Manifest Integrity Check — {$org} (" . count($repos) . " repos)");
|
||||||
|
|
||||||
|
$allResults = [];
|
||||||
|
$totalIssues = 0;
|
||||||
|
$reposWithIssues = 0;
|
||||||
|
|
||||||
|
foreach ($repos as $repo) {
|
||||||
|
$name = $repo['name'];
|
||||||
|
$manifest = $this->fetchManifest($apiBase, $org, $name, $token);
|
||||||
|
|
||||||
|
if ($manifest === null) {
|
||||||
|
if (!$quiet) {
|
||||||
|
$this->log('WARN', "{$name}: no manifest");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$issues = $this->validateManifestOnly($manifest, $name);
|
||||||
|
|
||||||
|
if (!empty($issues)) {
|
||||||
|
$reposWithIssues++;
|
||||||
|
$totalIssues += count($issues);
|
||||||
|
|
||||||
|
if ($json) {
|
||||||
|
$allResults[] = ['repo' => $name, 'issues' => $issues];
|
||||||
|
} else {
|
||||||
|
$this->printIssues($name, $issues);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($fix) {
|
||||||
|
$this->applyFixes($apiBase, $org, $name, $token, $manifest, $issues);
|
||||||
|
}
|
||||||
|
} elseif (!$quiet && !$json) {
|
||||||
|
$this->log('OK', "{$name}: clean");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($json) {
|
||||||
|
echo json_encode($allResults, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
||||||
|
} else {
|
||||||
|
echo "\n";
|
||||||
|
$level = $reposWithIssues > 0 ? 'WARN' : 'OK';
|
||||||
|
$this->log($level, sprintf(
|
||||||
|
'Summary: %d repos checked, %d with issues (%d total issues)',
|
||||||
|
count($repos),
|
||||||
|
$reposWithIssues,
|
||||||
|
$totalIssues
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $reposWithIssues > 0 ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Validation rules
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full validation: compare API manifest against locally-detected fields.
|
||||||
|
*/
|
||||||
|
private function validate(array $current, array $detected, string $repoName): array
|
||||||
|
{
|
||||||
|
$issues = [];
|
||||||
|
|
||||||
|
// Required fields that should never be empty
|
||||||
|
$required = ['platform', 'name', 'version', 'package_type', 'language', 'entry_point'];
|
||||||
|
foreach ($required as $field) {
|
||||||
|
if (empty($current[$field])) {
|
||||||
|
$fix = $detected[$field] ?? null;
|
||||||
|
$issues[] = [
|
||||||
|
'field' => $field,
|
||||||
|
'severity' => 'error',
|
||||||
|
'message' => 'Missing required field',
|
||||||
|
'current' => '',
|
||||||
|
'fix' => $fix,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drift detection: detected value differs from API
|
||||||
|
foreach ($detected as $field => $detectedValue) {
|
||||||
|
$currentValue = $current[$field] ?? '';
|
||||||
|
if ($detectedValue !== '' && $currentValue !== '' && $detectedValue !== $currentValue) {
|
||||||
|
// Version drift is expected on dev branches (suffix)
|
||||||
|
if ($field === 'version' && strpos($detectedValue, $currentValue) === 0) {
|
||||||
|
continue; // e.g., detected "02.34.50-dev" vs API "02.34.50"
|
||||||
|
}
|
||||||
|
if ($field === 'version' && strpos($currentValue, $detectedValue) === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$issues[] = [
|
||||||
|
'field' => $field,
|
||||||
|
'severity' => 'warn',
|
||||||
|
'message' => 'Drift: source differs from manifest',
|
||||||
|
'current' => $currentValue,
|
||||||
|
'fix' => $detectedValue,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Platform-specific structure validation
|
||||||
|
$platform = $current['platform'] ?? '';
|
||||||
|
$issues = array_merge($issues, $this->validatePlatformStructure($platform, $current, $repoName));
|
||||||
|
|
||||||
|
return $issues;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API-only validation: check manifest fields for completeness and consistency
|
||||||
|
* without access to source files.
|
||||||
|
*/
|
||||||
|
private function validateManifestOnly(array $manifest, string $repoName): array
|
||||||
|
{
|
||||||
|
$issues = [];
|
||||||
|
|
||||||
|
// Required fields
|
||||||
|
$required = ['platform', 'name', 'version', 'language'];
|
||||||
|
foreach ($required as $field) {
|
||||||
|
if (empty($manifest[$field])) {
|
||||||
|
$issues[] = [
|
||||||
|
'field' => $field,
|
||||||
|
'severity' => 'error',
|
||||||
|
'message' => 'Missing required field',
|
||||||
|
'current' => '',
|
||||||
|
'fix' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recommended fields
|
||||||
|
$recommended = ['package_type', 'entry_point', 'license_spdx', 'description'];
|
||||||
|
foreach ($recommended as $field) {
|
||||||
|
if (empty($manifest[$field])) {
|
||||||
|
$issues[] = [
|
||||||
|
'field' => $field,
|
||||||
|
'severity' => 'info',
|
||||||
|
'message' => 'Recommended field is empty',
|
||||||
|
'current' => '',
|
||||||
|
'fix' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Platform-specific checks
|
||||||
|
$platform = $manifest['platform'] ?? '';
|
||||||
|
$issues = array_merge($issues, $this->validatePlatformStructure($platform, $manifest, $repoName));
|
||||||
|
|
||||||
|
return $issues;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Platform-specific validation rules.
|
||||||
|
*/
|
||||||
|
private function validatePlatformStructure(string $platform, array $manifest, string $repoName): array
|
||||||
|
{
|
||||||
|
$issues = [];
|
||||||
|
|
||||||
|
switch ($platform) {
|
||||||
|
case 'joomla':
|
||||||
|
case 'waas-component':
|
||||||
|
// Joomla repos must have element_name
|
||||||
|
if (empty($manifest['element_name'])) {
|
||||||
|
$issues[] = [
|
||||||
|
'field' => 'element_name',
|
||||||
|
'severity' => 'error',
|
||||||
|
'message' => 'Joomla repos require element_name',
|
||||||
|
'current' => '',
|
||||||
|
'fix' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// Language should be PHP
|
||||||
|
if (!empty($manifest['language']) && $manifest['language'] !== 'PHP') {
|
||||||
|
$issues[] = [
|
||||||
|
'field' => 'language',
|
||||||
|
'severity' => 'warn',
|
||||||
|
'message' => 'Joomla repos should have language=PHP',
|
||||||
|
'current' => $manifest['language'],
|
||||||
|
'fix' => 'PHP',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'dolibarr':
|
||||||
|
case 'crm-module':
|
||||||
|
if (!empty($manifest['language']) && $manifest['language'] !== 'PHP') {
|
||||||
|
$issues[] = [
|
||||||
|
'field' => 'language',
|
||||||
|
'severity' => 'warn',
|
||||||
|
'message' => 'Dolibarr repos should have language=PHP',
|
||||||
|
'current' => $manifest['language'],
|
||||||
|
'fix' => 'PHP',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'go':
|
||||||
|
if (!empty($manifest['language']) && $manifest['language'] !== 'Go') {
|
||||||
|
$issues[] = [
|
||||||
|
'field' => 'language',
|
||||||
|
'severity' => 'warn',
|
||||||
|
'message' => 'Go repos should have language=Go',
|
||||||
|
'current' => $manifest['language'],
|
||||||
|
'fix' => 'Go',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'mcp':
|
||||||
|
if (!empty($manifest['language']) && !in_array($manifest['language'], ['TypeScript', 'JavaScript'], true)) {
|
||||||
|
$issues[] = [
|
||||||
|
'field' => 'language',
|
||||||
|
'severity' => 'warn',
|
||||||
|
'message' => 'MCP repos should have language=TypeScript or JavaScript',
|
||||||
|
'current' => $manifest['language'],
|
||||||
|
'fix' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version format check: should be XX.YY.ZZ
|
||||||
|
$version = $manifest['version'] ?? '';
|
||||||
|
if ($version !== '' && !preg_match('/^\d{2}\.\d{2}\.\d{2}/', $version)) {
|
||||||
|
// Allow semver for node/go repos
|
||||||
|
if (!in_array($platform, ['mcp', 'node', 'go'], true)) {
|
||||||
|
$issues[] = [
|
||||||
|
'field' => 'version',
|
||||||
|
'severity' => 'info',
|
||||||
|
'message' => 'Version does not match XX.YY.ZZ format',
|
||||||
|
'current' => $version,
|
||||||
|
'fix' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $issues;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Output
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
private function printIssues(string $repoName, array $issues): void
|
||||||
|
{
|
||||||
|
if (empty($issues)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$errors = count(array_filter($issues, fn($i) => $i['severity'] === 'error'));
|
||||||
|
$warns = count(array_filter($issues, fn($i) => $i['severity'] === 'warn'));
|
||||||
|
$infos = count($issues) - $errors - $warns;
|
||||||
|
|
||||||
|
echo "\n";
|
||||||
|
$summary = [];
|
||||||
|
if ($errors > 0) $summary[] = "{$errors} error(s)";
|
||||||
|
if ($warns > 0) $summary[] = "{$warns} warning(s)";
|
||||||
|
if ($infos > 0) $summary[] = "{$infos} info";
|
||||||
|
$this->log($errors > 0 ? 'ERROR' : 'WARN', "{$repoName} — " . implode(', ', $summary));
|
||||||
|
|
||||||
|
foreach ($issues as $issue) {
|
||||||
|
$icon = match ($issue['severity']) {
|
||||||
|
'error' => 'ERROR',
|
||||||
|
'warn' => 'WARN',
|
||||||
|
default => 'INFO',
|
||||||
|
};
|
||||||
|
$msg = sprintf(' %-18s %s', $issue['field'], $issue['message']);
|
||||||
|
if ($issue['current'] !== '') {
|
||||||
|
$msg .= " (current: {$issue['current']})";
|
||||||
|
}
|
||||||
|
if ($issue['fix'] !== null) {
|
||||||
|
$msg .= " → fix: {$issue['fix']}";
|
||||||
|
}
|
||||||
|
$this->log($icon, $msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Fix application
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
private function applyFixes(string $apiBase, string $org, string $repo, string $token, array $current, array $issues): int
|
||||||
|
{
|
||||||
|
$fixes = [];
|
||||||
|
foreach ($issues as $issue) {
|
||||||
|
if ($issue['fix'] !== null && $issue['fix'] !== '') {
|
||||||
|
$fixes[$issue['field']] = $issue['fix'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($fixes)) {
|
||||||
|
$this->log('INFO', "{$repo}: no auto-fixable issues");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$merged = array_merge($current, $fixes);
|
||||||
|
$url = "{$apiBase}/repos/{$org}/{$repo}/manifest";
|
||||||
|
$payload = json_encode($merged);
|
||||||
|
|
||||||
|
$ctx = stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'method' => 'PUT',
|
||||||
|
'header' => "Authorization: token {$token}\r\nContent-Type: application/json\r\nAccept: application/json\r\n",
|
||||||
|
'content' => $payload,
|
||||||
|
'timeout' => 10,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$body = @file_get_contents($url, false, $ctx);
|
||||||
|
if ($body === false) {
|
||||||
|
$this->log('ERROR', "{$repo}: failed to push fixes");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log('OK', "{$repo}: fixed " . implode(', ', array_keys($fixes)));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// API helpers
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
private function fetchManifest(string $apiBase, string $org, string $repo, string $token): ?array
|
||||||
|
{
|
||||||
|
$url = "{$apiBase}/repos/{$org}/{$repo}/manifest";
|
||||||
|
$ctx = stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'header' => "Authorization: token {$token}\r\nAccept: application/json\r\n",
|
||||||
|
'timeout' => 10,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$body = @file_get_contents($url, false, $ctx);
|
||||||
|
if ($body === false) return null;
|
||||||
|
|
||||||
|
$data = json_decode($body, true);
|
||||||
|
return is_array($data) ? $data : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function fetchOrgRepos(string $apiBase, string $org, string $token): ?array
|
||||||
|
{
|
||||||
|
$allRepos = [];
|
||||||
|
$page = 1;
|
||||||
|
$limit = 50;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
$url = "{$apiBase}/orgs/{$org}/repos?page={$page}&limit={$limit}";
|
||||||
|
$ctx = stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'header' => "Authorization: token {$token}\r\nAccept: application/json\r\n",
|
||||||
|
'timeout' => 15,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$body = @file_get_contents($url, false, $ctx);
|
||||||
|
if ($body === false) return null;
|
||||||
|
|
||||||
|
$repos = json_decode($body, true);
|
||||||
|
if (!is_array($repos) || empty($repos)) break;
|
||||||
|
|
||||||
|
$allRepos = array_merge($allRepos, $repos);
|
||||||
|
|
||||||
|
if (count($repos) < $limit) break;
|
||||||
|
$page++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out archived and empty repos
|
||||||
|
return array_filter($allRepos, fn($r) => !($r['archived'] ?? false) && !($r['empty'] ?? false));
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Detection (delegates to manifest_detect logic)
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
private function runDetect(string $root, string $repoName): array
|
||||||
|
{
|
||||||
|
$script = __DIR__ . '/manifest_detect.php';
|
||||||
|
$redirect = PHP_OS_FAMILY === 'Windows' ? '2>NUL' : '2>/dev/null';
|
||||||
|
$cmd = sprintf(
|
||||||
|
'php %s --path %s --repo %s --json --quiet %s',
|
||||||
|
escapeshellarg($script),
|
||||||
|
escapeshellarg($root),
|
||||||
|
escapeshellarg($repoName),
|
||||||
|
$redirect
|
||||||
|
);
|
||||||
|
|
||||||
|
$output = shell_exec($cmd) ?? '';
|
||||||
|
|
||||||
|
// Extract JSON object from output (skip banner/log lines)
|
||||||
|
if (preg_match('/\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/s', $output, $m)) {
|
||||||
|
$data = json_decode($m[0], true);
|
||||||
|
if (is_array($data)) {
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function detectRepoName(string $root): string
|
||||||
|
{
|
||||||
|
$gitConfig = "{$root}/.git/config";
|
||||||
|
if (!file_exists($gitConfig)) {
|
||||||
|
return basename($root);
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = file_get_contents($gitConfig);
|
||||||
|
if (preg_match('/url\s*=\s*.*\/([^\/\s]+?)(?:\.git)?\s*$/m', $content, $m)) {
|
||||||
|
return $m[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return basename($root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$app = new ManifestIntegrityCli();
|
||||||
|
exit($app->execute());
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* FILE INFORMATION
|
||||||
|
* DEFGROUP: mokoplatform.CLI
|
||||||
|
* INGROUP: mokoplatform
|
||||||
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
|
* PATH: /cli/manifest_licensing.php
|
||||||
|
* VERSION: 09.26.02
|
||||||
|
* BRIEF: Ensure licensing tags (updateservers, dlid) in Joomla extension manifests
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the <licensing> block from .mokogitea/manifest.xml and ensures that the
|
||||||
|
* Joomla extension manifest contains the correct <updateservers> and <dlid> tags.
|
||||||
|
*
|
||||||
|
* manifest.xml licensing block example:
|
||||||
|
*
|
||||||
|
* <licensing>
|
||||||
|
* <enabled>true</enabled>
|
||||||
|
* <dlid>true</dlid>
|
||||||
|
* <update-server>https://git.mokoconsulting.tech/{org}/{repo}/updates.xml</update-server>
|
||||||
|
* <update-server-name>MyExtension Updates</update-server-name>
|
||||||
|
* </licensing>
|
||||||
|
*
|
||||||
|
* Supports {org} and {repo} placeholders in update-server URL, resolved from
|
||||||
|
* the manifest's <identity> block or git remote.
|
||||||
|
*/
|
||||||
|
class ManifestLicensingCli extends CliFramework
|
||||||
|
{
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this->setDescription('Ensure licensing tags (updateservers, dlid) in Joomla extension manifests');
|
||||||
|
$this->addArgument('--path', 'Repository root path', '.');
|
||||||
|
$this->addArgument('--fix', 'Apply fixes (default: dry-run check only)', false);
|
||||||
|
$this->addArgument('--github-output', 'Write results to $GITHUB_OUTPUT', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function run(): int
|
||||||
|
{
|
||||||
|
$root = realpath($this->getArgument('--path')) ?: $this->getArgument('--path');
|
||||||
|
$fix = (bool) $this->getArgument('--fix');
|
||||||
|
$ghOutput = (bool) $this->getArgument('--github-output');
|
||||||
|
|
||||||
|
// ── 1. Read manifest.xml ──────────────────────────────────────────
|
||||||
|
$manifestFile = "{$root}/.mokogitea/manifest.xml";
|
||||||
|
|
||||||
|
if (!file_exists($manifestFile)) {
|
||||||
|
$this->log('WARN', "No manifest.xml found at {$manifestFile}");
|
||||||
|
$this->outputResult($ghOutput, 'skipped', 'No manifest.xml');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$xml = @simplexml_load_file($manifestFile);
|
||||||
|
|
||||||
|
if ($xml === false) {
|
||||||
|
$this->log('ERROR', "Failed to parse {$manifestFile}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 2. Check if licensing is enabled ──────────────────────────────
|
||||||
|
if (!isset($xml->licensing) || (string) ($xml->licensing->enabled ?? '') !== 'true') {
|
||||||
|
$this->log('INFO', 'Licensing not enabled in manifest.xml — skipping');
|
||||||
|
$this->outputResult($ghOutput, 'skipped', 'Licensing not enabled');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$licensingNode = $xml->licensing;
|
||||||
|
$dlidEnabled = ((string) ($licensingNode->dlid ?? 'true')) === 'true';
|
||||||
|
$updateServerUrl = (string) ($licensingNode->{'update-server'} ?? '');
|
||||||
|
$updateServerName = (string) ($licensingNode->{'update-server-name'} ?? '');
|
||||||
|
|
||||||
|
// ── 3. Resolve placeholders ───────────────────────────────────────
|
||||||
|
$org = (string) ($xml->identity->org ?? '');
|
||||||
|
$repo = (string) ($xml->identity->name ?? '');
|
||||||
|
|
||||||
|
// Fallback to git remote if manifest doesn't have org/name
|
||||||
|
if (empty($org) || empty($repo)) {
|
||||||
|
$remote = trim((string) @shell_exec("cd " . escapeshellarg($root) . " && git remote get-url origin 2>/dev/null"));
|
||||||
|
|
||||||
|
if (preg_match('#[/:]([^/]+)/([^/.]+?)(?:\.git)?$#', $remote, $m)) {
|
||||||
|
if (empty($org)) {
|
||||||
|
$org = $m[1];
|
||||||
|
}
|
||||||
|
if (empty($repo)) {
|
||||||
|
$repo = $m[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default update server URL if not specified
|
||||||
|
if (empty($updateServerUrl) && !empty($org) && !empty($repo)) {
|
||||||
|
$updateServerUrl = "https://git.mokoconsulting.tech/{$org}/{$repo}/updates.xml";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve {org} and {repo} placeholders
|
||||||
|
$updateServerUrl = str_replace(['{org}', '{repo}'], [$org, $repo], $updateServerUrl);
|
||||||
|
|
||||||
|
// Default server name from display-name or repo name
|
||||||
|
if (empty($updateServerName)) {
|
||||||
|
$displayName = (string) ($xml->identity->{'display-name'} ?? $repo);
|
||||||
|
$updateServerName = $displayName . ' Updates';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($updateServerUrl)) {
|
||||||
|
$this->log('ERROR', 'Cannot determine update server URL — set <update-server> in manifest.xml or ensure org/repo are available');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log('INFO', "Licensing enabled — org={$org}, repo={$repo}");
|
||||||
|
$this->log('INFO', "Update server: {$updateServerUrl}");
|
||||||
|
$this->log('INFO', "DLID required: " . ($dlidEnabled ? 'yes' : 'no'));
|
||||||
|
|
||||||
|
// ── 4. Find Joomla extension manifests ────────────────────────────
|
||||||
|
$xmlFiles = array_merge(
|
||||||
|
SourceResolver::globSource($root, '*.xml'),
|
||||||
|
SourceResolver::globSource($root, 'packages/*/*.xml'),
|
||||||
|
glob("{$root}/*.xml") ?: []
|
||||||
|
);
|
||||||
|
|
||||||
|
$packageManifest = null;
|
||||||
|
|
||||||
|
foreach ($xmlFiles as $file) {
|
||||||
|
$content = file_get_contents($file);
|
||||||
|
|
||||||
|
if (!str_contains($content, '<extension')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the package manifest (type="package") or the main extension manifest
|
||||||
|
if (str_contains($content, 'type="package"')) {
|
||||||
|
$packageManifest = $file;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: first extension manifest found
|
||||||
|
if ($packageManifest === null) {
|
||||||
|
$packageManifest = $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($packageManifest === null) {
|
||||||
|
$this->log('WARN', 'No Joomla extension manifest found');
|
||||||
|
$this->outputResult($ghOutput, 'skipped', 'No extension manifest');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$relPath = str_replace($root . '/', '', str_replace('\\', '/', $packageManifest));
|
||||||
|
$this->log('INFO', "Package manifest: {$relPath}");
|
||||||
|
|
||||||
|
// ── 5. Check and fix the manifest ─────────────────────────────────
|
||||||
|
$content = file_get_contents($packageManifest);
|
||||||
|
$original = $content;
|
||||||
|
$changes = [];
|
||||||
|
|
||||||
|
// --- 5a. Ensure <updateservers> block with correct URL ---
|
||||||
|
if (preg_match('#<updateservers>\s*</updateservers>#s', $content)) {
|
||||||
|
// Empty updateservers block — inject the server
|
||||||
|
$replacement = "<updateservers>\n"
|
||||||
|
. " <server type=\"extension\" name=\"{$updateServerName}\">{$updateServerUrl}</server>\n"
|
||||||
|
. " </updateservers>";
|
||||||
|
$content = preg_replace('#<updateservers>\s*</updateservers>#s', $replacement, $content);
|
||||||
|
$changes[] = 'Added update server URL to empty <updateservers>';
|
||||||
|
} elseif (!str_contains($content, '<updateservers>')) {
|
||||||
|
// No updateservers at all — add before </extension>
|
||||||
|
$serverBlock = "\n <updateservers>\n"
|
||||||
|
. " <server type=\"extension\" name=\"{$updateServerName}\">{$updateServerUrl}</server>\n"
|
||||||
|
. " </updateservers>\n";
|
||||||
|
$content = str_replace('</extension>', $serverBlock . '</extension>', $content);
|
||||||
|
$changes[] = 'Added <updateservers> block';
|
||||||
|
} else {
|
||||||
|
// updateservers exists — verify URL is correct
|
||||||
|
if (preg_match('#<server[^>]*>([^<]+)</server>#', $content, $m)) {
|
||||||
|
if ($m[1] !== $updateServerUrl) {
|
||||||
|
$content = preg_replace(
|
||||||
|
'#(<server[^>]*>)[^<]+(</server>)#',
|
||||||
|
"\${1}{$updateServerUrl}\${2}",
|
||||||
|
$content
|
||||||
|
);
|
||||||
|
$changes[] = "Updated server URL: {$m[1]} → {$updateServerUrl}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 5b. Ensure <dlid> tag if required ---
|
||||||
|
if ($dlidEnabled) {
|
||||||
|
if (!str_contains($content, '<dlid')) {
|
||||||
|
// Add before <updateservers> if present, otherwise before </extension>
|
||||||
|
$dlidTag = ' <dlid prefix="dlid=" suffix=""/>' . "\n";
|
||||||
|
|
||||||
|
if (str_contains($content, '<updateservers>')) {
|
||||||
|
$content = str_replace('<updateservers>', $dlidTag . "\n <updateservers>", $content);
|
||||||
|
} else {
|
||||||
|
$content = str_replace('</extension>', $dlidTag . '</extension>', $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
$changes[] = 'Added <dlid> tag';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 5c. Ensure <blockChildUninstall> for packages ---
|
||||||
|
if (str_contains($content, 'type="package"') && !str_contains($content, '<blockChildUninstall>')) {
|
||||||
|
$blockTag = ' <blockChildUninstall>true</blockChildUninstall>' . "\n";
|
||||||
|
|
||||||
|
if (str_contains($content, '<dlid')) {
|
||||||
|
// Add after <dlid>
|
||||||
|
$content = preg_replace(
|
||||||
|
'#(<dlid[^/]*/>\s*\n)#',
|
||||||
|
"\${1}{$blockTag}",
|
||||||
|
$content
|
||||||
|
);
|
||||||
|
} elseif (str_contains($content, '<updateservers>')) {
|
||||||
|
$content = str_replace('<updateservers>', $blockTag . "\n <updateservers>", $content);
|
||||||
|
} else {
|
||||||
|
$content = str_replace('</extension>', $blockTag . '</extension>', $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
$changes[] = 'Added <blockChildUninstall>true</blockChildUninstall>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 6. Report and apply ───────────────────────────────────────────
|
||||||
|
if (empty($changes)) {
|
||||||
|
$this->log('INFO', 'All licensing tags are correct — no changes needed');
|
||||||
|
$this->outputResult($ghOutput, 'ok', 'No changes needed');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($changes as $change) {
|
||||||
|
$this->log($fix ? 'INFO' : 'WARN', ($fix ? 'Fixed: ' : 'Needs fix: ') . $change);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($fix) {
|
||||||
|
file_put_contents($packageManifest, $content);
|
||||||
|
$this->log('INFO', "Wrote {$relPath} with " . count($changes) . " change(s)");
|
||||||
|
$this->outputResult($ghOutput, 'fixed', implode('; ', $changes));
|
||||||
|
} else {
|
||||||
|
$this->log('WARN', 'Run with --fix to apply changes');
|
||||||
|
$this->outputResult($ghOutput, 'needs-fix', implode('; ', $changes));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write result to $GITHUB_OUTPUT if requested.
|
||||||
|
*/
|
||||||
|
private function outputResult(bool $ghOutput, string $status, string $detail): void
|
||||||
|
{
|
||||||
|
if (!$ghOutput) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$outputFile = getenv('GITHUB_OUTPUT');
|
||||||
|
|
||||||
|
if ($outputFile === false || $outputFile === '') {
|
||||||
|
echo "licensing_status={$status}\n";
|
||||||
|
echo "licensing_detail={$detail}\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fh = fopen($outputFile, 'a');
|
||||||
|
fwrite($fh, "licensing_status={$status}\n");
|
||||||
|
fwrite($fh, "licensing_detail={$detail}\n");
|
||||||
|
fclose($fh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$app = new ManifestLicensingCli();
|
||||||
|
exit($app->execute());
|
||||||
@@ -0,0 +1,317 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* FILE INFORMATION
|
||||||
|
* DEFGROUP: mokoplatform.CLI
|
||||||
|
* INGROUP: mokoplatform
|
||||||
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
|
* PATH: /cli/metadata_read.php
|
||||||
|
* VERSION: 09.26.02
|
||||||
|
* BRIEF: Read and set metadata fields in .mokogitea/metadata.xml (or manifest.xml)
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
|
/** Field name → XPath mapping into the metadata XML */
|
||||||
|
const FIELD_MAP = [
|
||||||
|
// identity
|
||||||
|
'name' => 'identity/name',
|
||||||
|
'display-name' => 'identity/display-name',
|
||||||
|
'org' => 'identity/org',
|
||||||
|
'description' => 'identity/description',
|
||||||
|
'license' => 'identity/license',
|
||||||
|
'version' => 'identity/version',
|
||||||
|
// governance
|
||||||
|
'platform' => 'governance/platform',
|
||||||
|
'standards-version' => 'governance/standards-version',
|
||||||
|
'standards-source' => 'governance/standards-source',
|
||||||
|
// build
|
||||||
|
'language' => 'build/language',
|
||||||
|
'package-type' => 'build/package-type',
|
||||||
|
'entry-point' => 'build/entry-point',
|
||||||
|
// deploy
|
||||||
|
'source-dir' => 'deploy/source-dir',
|
||||||
|
'remote-subdir' => 'deploy/remote-subdir',
|
||||||
|
'excludes' => 'deploy/excludes',
|
||||||
|
'dev-host' => 'deploy/dev-host',
|
||||||
|
'demo-host' => 'deploy/demo-host',
|
||||||
|
];
|
||||||
|
|
||||||
|
class MetadataReadCli extends CliFramework
|
||||||
|
{
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this->setDescription('Read or set metadata fields in .mokogitea/metadata.xml');
|
||||||
|
$this->addArgument('--path', 'Repository root path', '.');
|
||||||
|
$this->addArgument('--field', 'Single field name to read', '');
|
||||||
|
$this->addArgument('--set', 'Set field value (field=value), repeatable', '');
|
||||||
|
$this->addArgument('--all', 'Print all fields as KEY=VALUE lines', false);
|
||||||
|
$this->addArgument('--github-output', 'Append all fields to $GITHUB_OUTPUT', false);
|
||||||
|
$this->addArgument('--json', 'Output all fields as JSON', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function run(): int
|
||||||
|
{
|
||||||
|
$path = $this->getArgument('--path');
|
||||||
|
$field = $this->getArgument('--field');
|
||||||
|
$setValue = $this->getArgument('--set');
|
||||||
|
$showAll = $this->getArgument('--all');
|
||||||
|
$ghOutput = $this->getArgument('--github-output');
|
||||||
|
$jsonMode = $this->getArgument('--json');
|
||||||
|
|
||||||
|
$root = realpath($path) ?: $path;
|
||||||
|
|
||||||
|
// -- Locate metadata file --
|
||||||
|
$metadataFile = $this->findMetadataFile($root);
|
||||||
|
|
||||||
|
if ($metadataFile === null) {
|
||||||
|
$this->log('ERROR', "No metadata file found in {$root}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Auto-migrate manifest.xml → metadata.xml --
|
||||||
|
$metadataFile = $this->migrateIfNeeded($metadataFile, $root);
|
||||||
|
|
||||||
|
// -- Set mode --
|
||||||
|
if ($setValue !== '') {
|
||||||
|
return $this->handleSet($metadataFile, $setValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Read mode --
|
||||||
|
$xml = @simplexml_load_file($metadataFile);
|
||||||
|
|
||||||
|
if ($xml === false) {
|
||||||
|
// Fallback: legacy YAML format (.mokoplatform)
|
||||||
|
$fields = $this->parseLegacy($metadataFile);
|
||||||
|
} else {
|
||||||
|
$fields = $this->parseXml($xml, $metadataFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
$fields = array_filter($fields, fn($v) => $v !== '');
|
||||||
|
|
||||||
|
return $this->outputFields($fields, $field, $showAll, $ghOutput, $jsonMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function findMetadataFile(string $root): ?string
|
||||||
|
{
|
||||||
|
$candidates = [
|
||||||
|
"{$root}/.mokogitea/metadata.xml",
|
||||||
|
"{$root}/.mokogitea/manifest.xml",
|
||||||
|
"{$root}/.mokogitea/.manifest.xml",
|
||||||
|
"{$root}/.mokogitea/.mokoplatform",
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($candidates as $candidate) {
|
||||||
|
if (file_exists($candidate)) {
|
||||||
|
return $candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function migrateIfNeeded(string $metadataFile, string $root): string
|
||||||
|
{
|
||||||
|
$newPath = "{$root}/.mokogitea/metadata.xml";
|
||||||
|
|
||||||
|
// Already at the new location
|
||||||
|
if ($metadataFile === $newPath) {
|
||||||
|
return $metadataFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy file found — migrate
|
||||||
|
if (str_ends_with($metadataFile, '.mokoplatform')) {
|
||||||
|
// YAML legacy — can't auto-migrate, just warn
|
||||||
|
$this->log('WARN', "Legacy .mokoplatform format detected — migrate to metadata.xml manually");
|
||||||
|
return $metadataFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
// manifest.xml or .manifest.xml → metadata.xml
|
||||||
|
copy($metadataFile, $newPath);
|
||||||
|
unlink($metadataFile);
|
||||||
|
$this->log('INFO', "Migrated " . basename($metadataFile) . " → metadata.xml");
|
||||||
|
return $newPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseXml(\SimpleXMLElement $xml, string $filePath): array
|
||||||
|
{
|
||||||
|
$fields = [];
|
||||||
|
foreach (FIELD_MAP as $name => $xpath) {
|
||||||
|
$parts = explode('/', $xpath);
|
||||||
|
$node = $xml;
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
$node = $node->{$part} ?? null;
|
||||||
|
if ($node === null) break;
|
||||||
|
}
|
||||||
|
if ($name === 'license' && $node !== null) {
|
||||||
|
// Also extract spdx attribute
|
||||||
|
$fields['license'] = (string)$node;
|
||||||
|
$fields['license-spdx'] = (string)($node['spdx'] ?? '');
|
||||||
|
} else {
|
||||||
|
$fields[$name] = $node !== null ? (string)$node : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$fields['metadata-file'] = $filePath;
|
||||||
|
return $fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseLegacy(string $filePath): array
|
||||||
|
{
|
||||||
|
$content = file_get_contents($filePath);
|
||||||
|
$fields = [];
|
||||||
|
if (preg_match('/^platform:\s*(.+)/m', $content, $m)) {
|
||||||
|
$fields['platform'] = trim($m[1], " \t\n\r\"'");
|
||||||
|
}
|
||||||
|
if (preg_match('/^standards_version:\s*(.+)/m', $content, $m)) {
|
||||||
|
$fields['standards-version'] = trim($m[1], " \t\n\r\"'");
|
||||||
|
}
|
||||||
|
if (preg_match('/^governed_repo:\s*(.+)/m', $content, $m)) {
|
||||||
|
$fields['name'] = trim($m[1], " \t\n\r\"'");
|
||||||
|
}
|
||||||
|
return $fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handleSet(string $metadataFile, string $setValue): int
|
||||||
|
{
|
||||||
|
// Parse field=value pairs (comma-separated or from repeated --set)
|
||||||
|
$pairs = [];
|
||||||
|
foreach (explode(',', $setValue) as $pair) {
|
||||||
|
$pair = trim($pair);
|
||||||
|
if ($pair === '') continue;
|
||||||
|
$eq = strpos($pair, '=');
|
||||||
|
if ($eq === false) {
|
||||||
|
$this->log('ERROR', "Invalid set format: '{$pair}' — expected field=value");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
$key = trim(substr($pair, 0, $eq));
|
||||||
|
$val = trim(substr($pair, $eq + 1));
|
||||||
|
$pairs[$key] = $val;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($pairs)) {
|
||||||
|
$this->log('ERROR', 'No field=value pairs provided');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate all fields exist in FIELD_MAP
|
||||||
|
foreach ($pairs as $key => $val) {
|
||||||
|
if (!isset(FIELD_MAP[$key])) {
|
||||||
|
$this->log('ERROR', "Unknown field: '{$key}'");
|
||||||
|
$this->log('INFO', 'Valid fields: ' . implode(', ', array_keys(FIELD_MAP)));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy files are read-only
|
||||||
|
if (str_ends_with($metadataFile, '.mokoplatform')) {
|
||||||
|
$this->log('ERROR', 'Cannot set fields on legacy .mokoplatform format — migrate to metadata.xml first');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load XML
|
||||||
|
$xml = @simplexml_load_file($metadataFile);
|
||||||
|
if ($xml === false) {
|
||||||
|
$this->log('ERROR', "Failed to parse XML: {$metadataFile}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set each field
|
||||||
|
foreach ($pairs as $key => $val) {
|
||||||
|
$xpath = FIELD_MAP[$key];
|
||||||
|
$parts = explode('/', $xpath);
|
||||||
|
$section = $parts[0];
|
||||||
|
$element = $parts[1];
|
||||||
|
|
||||||
|
if (!isset($xml->{$section})) {
|
||||||
|
$this->log('ERROR', "Section <{$section}> not found in XML — cannot set '{$key}'");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($xml->{$section}->{$element})) {
|
||||||
|
$this->log('ERROR', "Element <{$element}> not found in <{$section}> — cannot set '{$key}'");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$old = (string)$xml->{$section}->{$element};
|
||||||
|
$xml->{$section}->{$element} = $val;
|
||||||
|
$this->log('INFO', "Set {$key}: '{$old}' → '{$val}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write back with preserved formatting
|
||||||
|
$dom = new \DOMDocument('1.0', 'UTF-8');
|
||||||
|
$dom->preserveWhiteSpace = false;
|
||||||
|
$dom->formatOutput = true;
|
||||||
|
$dom->loadXML($xml->asXML());
|
||||||
|
$dom->save($metadataFile);
|
||||||
|
|
||||||
|
$this->log('INFO', "Updated {$metadataFile}");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function outputFields(array $fields, string $field, $showAll, $ghOutput, $jsonMode): int
|
||||||
|
{
|
||||||
|
if ($ghOutput) {
|
||||||
|
$mode = 'github-output';
|
||||||
|
} elseif ($showAll) {
|
||||||
|
$mode = 'all';
|
||||||
|
} elseif ($jsonMode) {
|
||||||
|
$mode = 'json';
|
||||||
|
} else {
|
||||||
|
$mode = 'field';
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($mode) {
|
||||||
|
case 'field':
|
||||||
|
if ($field === '') {
|
||||||
|
$this->log('ERROR', "Usage: metadata_read.php --path <dir> --field <name>");
|
||||||
|
$this->log('ERROR', " metadata_read.php --path <dir> --all");
|
||||||
|
$this->log('ERROR', " metadata_read.php --path <dir> --json");
|
||||||
|
$this->log('ERROR', " metadata_read.php --path <dir> --set field=value");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
echo ($fields[$field] ?? '') . "\n";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'all':
|
||||||
|
foreach ($fields as $k => $v) {
|
||||||
|
echo "{$k}={$v}\n";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'json':
|
||||||
|
echo json_encode($fields, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'github-output':
|
||||||
|
$outputFile = getenv('GITHUB_OUTPUT');
|
||||||
|
if ($outputFile === false || $outputFile === '') {
|
||||||
|
$this->log('ERROR', 'GITHUB_OUTPUT not set — printing to stdout instead');
|
||||||
|
foreach ($fields as $k => $v) {
|
||||||
|
$envKey = str_replace('-', '_', $k);
|
||||||
|
echo "{$envKey}={$v}\n";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$fh = fopen($outputFile, 'a');
|
||||||
|
foreach ($fields as $k => $v) {
|
||||||
|
$envKey = str_replace('-', '_', $k);
|
||||||
|
fwrite($fh, "{$envKey}={$v}\n");
|
||||||
|
}
|
||||||
|
fclose($fh);
|
||||||
|
$this->log('INFO', "Wrote " . count($fields) . " fields to GITHUB_OUTPUT");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$app = new MetadataReadCli();
|
||||||
|
exit($app->execute());
|
||||||
@@ -19,7 +19,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\{CliFramework, SourceResolver};
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class PackageBuildCli extends CliFramework
|
class PackageBuildCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* INGROUP: mokoplatform
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/platform_detect.php
|
* PATH: /cli/platform_detect.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.26.02
|
||||||
* BRIEF: Auto-detect repository platform type and optionally update manifest
|
* BRIEF: Auto-detect repository platform type and optionally update manifest
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
class PlatformDetectCli extends CliFramework
|
class PlatformDetectCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -82,7 +82,7 @@ class PlatformDetectCli extends CliFramework
|
|||||||
$giteaUrl,
|
$giteaUrl,
|
||||||
$token,
|
$token,
|
||||||
'PATCH',
|
'PATCH',
|
||||||
"/api/v1/repos/{$owner}/{$repo}/metadata",
|
"/api/v1/repos/{$owner}/{$repo}/manifest",
|
||||||
json_encode(['platform' => $platform])
|
json_encode(['platform' => $platform])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
class ReleaseCli extends CliFramework
|
class ReleaseCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
class ReleaseBodyUpdateCli extends CliFramework
|
class ReleaseBodyUpdateCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
+9
-314
@@ -6,336 +6,31 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: mokocli.CLI
|
* DEFGROUP: mokoplatform.CLI
|
||||||
* INGROUP: mokocli
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/release_cascade.php
|
* PATH: /cli/release_cascade.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.26.02
|
||||||
* BRIEF: Cascade release zip to all lower stability channels
|
* BRIEF: DEPRECATED — cascade behavior removed. Each release stream is independent.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
class ReleaseCascadeCli extends CliFramework
|
class ReleaseCascadeCli extends CliFramework
|
||||||
{
|
{
|
||||||
/** Channel hierarchy: highest stability first. */
|
|
||||||
private const CHANNELS = ['stable', 'release-candidate', 'beta', 'alpha', 'development'];
|
|
||||||
|
|
||||||
/** Map stability input names to canonical tag names. */
|
|
||||||
private const TAG_MAP = [
|
|
||||||
'stable' => 'stable',
|
|
||||||
'release-candidate' => 'release-candidate',
|
|
||||||
'rc' => 'release-candidate',
|
|
||||||
'beta' => 'beta',
|
|
||||||
'alpha' => 'alpha',
|
|
||||||
'development' => 'development',
|
|
||||||
'dev' => 'development',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this->setDescription('Cascade release zip to all lower stability channels');
|
$this->setDescription('DEPRECATED — cascade behavior removed');
|
||||||
$this->addArgument('--stability', 'Source stability channel (required)', '');
|
|
||||||
$this->addArgument('--token', 'Gitea API token (required)', '');
|
|
||||||
$this->addArgument('--api-base', 'Gitea API base URL for the repo (required)', '');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function run(): int
|
protected function run(): int
|
||||||
{
|
{
|
||||||
$stability = strtolower($this->getArgument('--stability'));
|
$this->log('INFO', 'No-op (cascade behavior removed — each stream is independent)');
|
||||||
$token = $this->getArgument('--token');
|
return 0;
|
||||||
$apiBase = rtrim($this->getArgument('--api-base'), '/');
|
|
||||||
|
|
||||||
if ($token === '') {
|
|
||||||
$envToken = getenv('MOKOGITEA_TOKEN');
|
|
||||||
if ($envToken === false || $envToken === '') {
|
|
||||||
$envToken = getenv('GITEA_TOKEN');
|
|
||||||
}
|
|
||||||
if ($envToken !== false && $envToken !== '') {
|
|
||||||
$token = $envToken;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($stability === '' || $token === '' || $apiBase === '') {
|
|
||||||
$this->log('ERROR', 'Usage: release_cascade.php --stability CHANNEL --token TOKEN --api-base URL');
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
$sourceTag = self::TAG_MAP[$stability] ?? null;
|
|
||||||
if ($sourceTag === null) {
|
|
||||||
$this->log('ERROR', "Unknown stability: {$stability}");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find lower channels to cascade to
|
|
||||||
$lowerChannels = $this->getLowerChannels($sourceTag);
|
|
||||||
if (count($lowerChannels) === 0) {
|
|
||||||
$this->log('INFO', "No lower channels for '{$stability}' — nothing to cascade.");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->log('INFO', "Cascading from '{$sourceTag}' to: " . implode(', ', $lowerChannels));
|
|
||||||
|
|
||||||
if ($this->dryRun) {
|
|
||||||
$this->log('INFO', '[DRY RUN] No changes will be made.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Get source release
|
|
||||||
$sourceRelease = $this->giteaApi("{$apiBase}/releases/tags/{$sourceTag}", $token);
|
|
||||||
if ($sourceRelease === null) {
|
|
||||||
$this->log('WARN', "No release found at tag '{$sourceTag}' — nothing to cascade.");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$sourceVersion = $sourceRelease['name'] ?? $sourceTag;
|
|
||||||
$sourceBody = $sourceRelease['body'] ?? '';
|
|
||||||
$sourceAssets = $sourceRelease['assets'] ?? [];
|
|
||||||
|
|
||||||
// Find zip assets (exclude .sha256 sidecars)
|
|
||||||
$zipAssets = array_filter($sourceAssets, function (array $asset): bool {
|
|
||||||
$name = strtolower($asset['name'] ?? '');
|
|
||||||
return str_ends_with($name, '.zip') && !str_ends_with($name, '.sha256');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Also grab sha256 sidecars
|
|
||||||
$sha256Assets = array_filter($sourceAssets, function (array $asset): bool {
|
|
||||||
return str_ends_with(strtolower($asset['name'] ?? ''), '.zip.sha256');
|
|
||||||
});
|
|
||||||
|
|
||||||
if (count($zipAssets) === 0) {
|
|
||||||
$this->log('WARN', "Source release '{$sourceTag}' has no zip assets — nothing to cascade.");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->log('INFO', "Source: {$sourceVersion} — " . count($zipAssets) . " zip(s)");
|
|
||||||
echo "\n";
|
|
||||||
|
|
||||||
// 2. Download source assets to temp files
|
|
||||||
$downloads = [];
|
|
||||||
foreach (array_merge($zipAssets, $sha256Assets) as $asset) {
|
|
||||||
$url = $asset['browser_download_url'] ?? '';
|
|
||||||
if ($url === '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$tmpFile = tempnam(sys_get_temp_dir(), 'cascade_');
|
|
||||||
if ($this->downloadFile($url, $token, $tmpFile)) {
|
|
||||||
$downloads[] = ['name' => $asset['name'], 'path' => $tmpFile];
|
|
||||||
$this->log('INFO', "Downloaded: {$asset['name']}");
|
|
||||||
} else {
|
|
||||||
$this->log('ERROR', "Failed to download: {$asset['name']}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (count($downloads) === 0) {
|
|
||||||
$this->log('ERROR', 'Could not download any source assets.');
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Cascade to each lower channel
|
|
||||||
$errors = 0;
|
|
||||||
foreach ($lowerChannels as $targetTag) {
|
|
||||||
echo "\n";
|
|
||||||
$result = $this->cascadeToChannel(
|
|
||||||
$apiBase, $token, $targetTag,
|
|
||||||
$sourceVersion, $sourceBody, $downloads
|
|
||||||
);
|
|
||||||
if (!$result) {
|
|
||||||
$errors++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Cleanup temp files
|
|
||||||
foreach ($downloads as $dl) {
|
|
||||||
@unlink($dl['path']);
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "\n";
|
|
||||||
$this->log('INFO', "Cascade complete. " . (count($lowerChannels) - $errors)
|
|
||||||
. "/" . count($lowerChannels) . " channels updated.");
|
|
||||||
|
|
||||||
return $errors > 0 ? 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cascade assets to a single target channel.
|
|
||||||
*/
|
|
||||||
private function cascadeToChannel(
|
|
||||||
string $apiBase,
|
|
||||||
string $token,
|
|
||||||
string $targetTag,
|
|
||||||
string $sourceVersion,
|
|
||||||
string $sourceBody,
|
|
||||||
array $downloads
|
|
||||||
): bool {
|
|
||||||
$this->log('INFO', "→ {$targetTag}");
|
|
||||||
|
|
||||||
if ($this->dryRun) {
|
|
||||||
$this->log('INFO', " [DRY RUN] Would cascade to {$targetTag}");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find existing release at target tag
|
|
||||||
$existing = $this->giteaApi("{$apiBase}/releases/tags/{$targetTag}", $token);
|
|
||||||
|
|
||||||
if ($existing !== null && !empty($existing['id'])) {
|
|
||||||
$releaseId = (int) $existing['id'];
|
|
||||||
|
|
||||||
// Delete existing assets
|
|
||||||
$existingAssets = $existing['assets'] ?? [];
|
|
||||||
foreach ($existingAssets as $asset) {
|
|
||||||
$assetId = $asset['id'] ?? 0;
|
|
||||||
if ($assetId > 0) {
|
|
||||||
$this->giteaApi(
|
|
||||||
"{$apiBase}/releases/{$releaseId}/assets/{$assetId}",
|
|
||||||
$token, 'DELETE'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update release metadata
|
|
||||||
$updatePayload = json_encode([
|
|
||||||
'name' => $sourceVersion,
|
|
||||||
'body' => $sourceBody,
|
|
||||||
]);
|
|
||||||
$this->giteaApi(
|
|
||||||
"{$apiBase}/releases/{$releaseId}",
|
|
||||||
$token, 'PATCH', $updatePayload
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->log('INFO', " Updated release metadata (id: {$releaseId})");
|
|
||||||
} else {
|
|
||||||
// Create new release at target tag
|
|
||||||
// Use the source release's target commitish so the tag points to the same commit
|
|
||||||
$createPayload = json_encode([
|
|
||||||
'tag_name' => $targetTag,
|
|
||||||
'target_commitish' => 'main',
|
|
||||||
'name' => $sourceVersion,
|
|
||||||
'body' => $sourceBody,
|
|
||||||
'prerelease' => ($targetTag !== 'stable'),
|
|
||||||
]);
|
|
||||||
$newRelease = $this->giteaApi("{$apiBase}/releases", $token, 'POST', $createPayload);
|
|
||||||
if ($newRelease === null || empty($newRelease['id'])) {
|
|
||||||
$this->log('ERROR', " Failed to create release at tag '{$targetTag}'");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$releaseId = (int) $newRelease['id'];
|
|
||||||
$this->log('INFO', " Created release (id: {$releaseId})");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upload assets
|
|
||||||
foreach ($downloads as $dl) {
|
|
||||||
$uploadUrl = "{$apiBase}/releases/{$releaseId}/assets?name=" . rawurlencode($dl['name']);
|
|
||||||
$success = $this->uploadAsset($uploadUrl, $token, $dl['path'], $dl['name']);
|
|
||||||
if ($success) {
|
|
||||||
$this->log('INFO', " Uploaded: {$dl['name']}");
|
|
||||||
} else {
|
|
||||||
$this->log('ERROR', " Failed to upload: {$dl['name']}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all channels below the given source channel.
|
|
||||||
*/
|
|
||||||
private function getLowerChannels(string $sourceTag): array
|
|
||||||
{
|
|
||||||
$idx = array_search($sourceTag, self::CHANNELS, true);
|
|
||||||
if ($idx === false) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return array_slice(self::CHANNELS, $idx + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Download a file via HTTP.
|
|
||||||
*/
|
|
||||||
private function downloadFile(string $url, string $token, string $destPath): bool
|
|
||||||
{
|
|
||||||
$ch = curl_init($url);
|
|
||||||
if ($ch === false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$fp = fopen($destPath, 'wb');
|
|
||||||
if ($fp === false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
curl_setopt_array($ch, [
|
|
||||||
CURLOPT_FOLLOWLOCATION => true,
|
|
||||||
CURLOPT_FILE => $fp,
|
|
||||||
CURLOPT_HTTPHEADER => ["Authorization: token {$token}"],
|
|
||||||
CURLOPT_TIMEOUT => 120,
|
|
||||||
]);
|
|
||||||
curl_exec($ch);
|
|
||||||
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
||||||
curl_close($ch);
|
|
||||||
fclose($fp);
|
|
||||||
return $code >= 200 && $code < 300;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Upload a file as a release asset via multipart form.
|
|
||||||
*/
|
|
||||||
private function uploadAsset(string $url, string $token, string $filePath, string $fileName): bool
|
|
||||||
{
|
|
||||||
$ch = curl_init($url);
|
|
||||||
if ($ch === false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$cfile = new CURLFile($filePath, 'application/octet-stream', $fileName);
|
|
||||||
curl_setopt_array($ch, [
|
|
||||||
CURLOPT_RETURNTRANSFER => true,
|
|
||||||
CURLOPT_POST => true,
|
|
||||||
CURLOPT_POSTFIELDS => ['attachment' => $cfile],
|
|
||||||
CURLOPT_HTTPHEADER => ["Authorization: token {$token}"],
|
|
||||||
CURLOPT_TIMEOUT => 120,
|
|
||||||
]);
|
|
||||||
$response = curl_exec($ch);
|
|
||||||
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
||||||
curl_close($ch);
|
|
||||||
return $code >= 200 && $code < 300;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make an HTTP request to the Gitea API.
|
|
||||||
*/
|
|
||||||
private function giteaApi(
|
|
||||||
string $url,
|
|
||||||
string $token,
|
|
||||||
string $method = 'GET',
|
|
||||||
?string $body = null
|
|
||||||
): ?array {
|
|
||||||
$ch = curl_init($url);
|
|
||||||
if ($ch === false) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
curl_setopt_array($ch, [
|
|
||||||
CURLOPT_RETURNTRANSFER => true,
|
|
||||||
CURLOPT_HTTPHEADER => [
|
|
||||||
"Authorization: token {$token}",
|
|
||||||
'Content-Type: application/json',
|
|
||||||
],
|
|
||||||
CURLOPT_TIMEOUT => 30,
|
|
||||||
CURLOPT_CUSTOMREQUEST => $method,
|
|
||||||
]);
|
|
||||||
if ($body !== null) {
|
|
||||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
|
||||||
}
|
|
||||||
$response = curl_exec($ch);
|
|
||||||
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
||||||
curl_close($ch);
|
|
||||||
|
|
||||||
if ($httpCode < 200 || $httpCode >= 300 || empty($response) || !is_string($response)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$decoded = json_decode($response, true);
|
|
||||||
return is_array($decoded) ? $decoded : null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\{CliFramework, SourceResolver};
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class ReleaseCreateCli extends CliFramework
|
class ReleaseCreateCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
class ReleaseManageCli extends CliFramework
|
class ReleaseManageCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
class ReleaseMirrorCli extends CliFramework
|
class ReleaseMirrorCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
class ReleaseNotesCli extends CliFramework
|
class ReleaseNotesCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\{CliFramework, SourceResolver};
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class ReleasePackageCli extends CliFramework
|
class ReleasePackageCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\{CliFramework, SourceResolver};
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class ReleasePromoteCli extends CliFramework
|
class ReleasePromoteCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* INGROUP: mokoplatform
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/release_publish.php
|
* PATH: /cli/release_publish.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.26.02
|
||||||
* BRIEF: Publish a release and create copies for all lesser stability streams.
|
* BRIEF: Publish a release and create copies for all lesser stability streams.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
class ReleasePublishCli extends CliFramework
|
class ReleasePublishCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\{CliFramework, SourceResolver};
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class ReleaseValidateCli extends CliFramework
|
class ReleaseValidateCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
class ReleaseVerifyCli extends CliFramework
|
class ReleaseVerifyCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* INGROUP: mokoplatform
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/scaffold_client.php
|
* PATH: /cli/scaffold_client.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.26.02
|
||||||
* BRIEF: Scaffold a new client-waas repo from Template-Client-WaaS with pre-configured settings
|
* BRIEF: Scaffold a new client-waas repo from Template-Client-WaaS with pre-configured settings
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
class ScaffoldClientCli extends CliFramework
|
class ScaffoldClientCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ declare(strict_types=1);
|
|||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
use MokoCli\Config;
|
use MokoEnterprise\Config;
|
||||||
use MokoCli\PlatformAdapterFactory;
|
use MokoEnterprise\PlatformAdapterFactory;
|
||||||
|
|
||||||
class SyncRulesetsCli extends CliFramework
|
class SyncRulesetsCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\{CliFramework, SourceResolver};
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class ThemeLintCli extends CliFramework
|
class ThemeLintCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\{CliFramework, SourceResolver};
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class UpdatesXmlBuildCli extends CliFramework
|
class UpdatesXmlBuildCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* INGROUP: mokoplatform
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/updates_xml_sync.php
|
* PATH: /cli/updates_xml_sync.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.26.02
|
||||||
* BRIEF: Sync updates.xml to target branches via Gitea API
|
* BRIEF: Sync updates.xml to target branches via Gitea API
|
||||||
* NOTE: Called by pre-release and auto-release workflows after updates.xml
|
* NOTE: Called by pre-release and auto-release workflows after updates.xml
|
||||||
* is modified on the current branch. Pushes the file to other branches
|
* is modified on the current branch. Pushes the file to other branches
|
||||||
@@ -21,7 +21,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
class UpdatesXmlSyncCli extends CliFramework
|
class UpdatesXmlSyncCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* INGROUP: mokoplatform
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/version_auto_bump.php
|
* PATH: /cli/version_auto_bump.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.26.02
|
||||||
* BRIEF: Auto patch-bump, set stability suffix, and commit — single CLI replacing inline workflow bash
|
* BRIEF: Auto patch-bump, set stability suffix, and commit — single CLI replacing inline workflow bash
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
class VersionAutoBumpCli extends CliFramework
|
class VersionAutoBumpCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
+2
-89
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\{CliFramework, SourceResolver};
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class VersionBumpCli extends CliFramework
|
class VersionBumpCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -27,7 +27,6 @@ class VersionBumpCli extends CliFramework
|
|||||||
$this->addArgument('--path', 'Repository root', '.');
|
$this->addArgument('--path', 'Repository root', '.');
|
||||||
$this->addArgument('--minor', 'Bump minor version', false);
|
$this->addArgument('--minor', 'Bump minor version', false);
|
||||||
$this->addArgument('--major', 'Bump major version', false);
|
$this->addArgument('--major', 'Bump major version', false);
|
||||||
$this->addArgument('--min-version', 'Minimum base version (ensures bump is above this)', '');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function run(): int
|
protected function run(): int
|
||||||
@@ -117,28 +116,6 @@ class VersionBumpCli extends CliFramework
|
|||||||
$baseVersion = $v;
|
$baseVersion = $v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check --min-version: ensures dev never falls behind stable
|
|
||||||
$minVersion = $this->getArgument('--min-version');
|
|
||||||
if (!empty($minVersion)) {
|
|
||||||
$minVersion = preg_replace('/-(?:dev|alpha|beta|rc)$/', '', $minVersion);
|
|
||||||
if (preg_match('/^\d{2}\.\d{2}\.\d{2}$/', $minVersion)) {
|
|
||||||
if ($baseVersion === null || version_compare($minVersion, $baseVersion, '>')) {
|
|
||||||
$this->log('INFO', "Using --min-version {$minVersion} (higher than manifest {$baseVersion})");
|
|
||||||
$baseVersion = $minVersion;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-detect: scan git tags for higher versions from other channels
|
|
||||||
if ($baseVersion !== null) {
|
|
||||||
$gitTagVersion = $this->getHighestGitTagVersion($root);
|
|
||||||
if ($gitTagVersion !== null && version_compare($gitTagVersion, $baseVersion, '>')) {
|
|
||||||
$this->log('INFO', "Git tag version {$gitTagVersion} is higher than manifest {$baseVersion} — using as base");
|
|
||||||
$baseVersion = $gitTagVersion;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($baseVersion === null) {
|
if ($baseVersion === null) {
|
||||||
$this->log('ERROR', "No version found in manifest.xml, README.md, or Joomla XML");
|
$this->log('ERROR', "No version found in manifest.xml, README.md, or Joomla XML");
|
||||||
return 1;
|
return 1;
|
||||||
@@ -255,25 +232,9 @@ class VersionBumpCli extends CliFramework
|
|||||||
$pkgContent
|
$pkgContent
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if ($updatedPkg !== $pkgContent && $updatedPkg !== null) {
|
if ($updatedPkg !== $pkgContent) {
|
||||||
file_put_contents($packageJsonFile, $updatedPkg);
|
file_put_contents($packageJsonFile, $updatedPkg);
|
||||||
fwrite(STDERR, "Updated package.json\n");
|
fwrite(STDERR, "Updated package.json\n");
|
||||||
} elseif (preg_match('/("version"\s*:\s*")(\d+)\.(\d+)\.(\d+)(")/m', $pkgContent, $semM)) {
|
|
||||||
// Semver fallback: bump standard x.y.z version when XX.YY.ZZ pattern didn't match
|
|
||||||
$sMajor = (int)$semM[2];
|
|
||||||
$sMinor = (int)$semM[3];
|
|
||||||
$sPatch = (int)$semM[4];
|
|
||||||
switch ($type) {
|
|
||||||
case 'major': $sMajor++; $sMinor = 0; $sPatch = 0; break;
|
|
||||||
case 'minor': $sMinor++; $sPatch = 0; break;
|
|
||||||
default: $sPatch++; break;
|
|
||||||
}
|
|
||||||
$semNew = "{$sMajor}.{$sMinor}.{$sPatch}";
|
|
||||||
$semUpdated = preg_replace('/("version"\s*:\s*")\d+\.\d+\.\d+(")/m', '${1}' . $semNew . '${2}', $pkgContent);
|
|
||||||
if ($semUpdated !== $pkgContent) {
|
|
||||||
file_put_contents($packageJsonFile, $semUpdated);
|
|
||||||
fwrite(STDERR, "Updated package.json (semver: {$semM[2]}.{$semM[3]}.{$semM[4]} -> $semNew)\n");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$pyprojectFile = "{$root}/pyproject.toml";
|
$pyprojectFile = "{$root}/pyproject.toml";
|
||||||
@@ -366,54 +327,6 @@ class VersionBumpCli extends CliFramework
|
|||||||
echo "{$old} -> {$newFull}\n";
|
echo "{$old} -> {$newFull}\n";
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Scan git release tags for the highest version across all channels.
|
|
||||||
*
|
|
||||||
* Checks release names like "MokoSuiteClient (VERSION: 09.32.01)" in
|
|
||||||
* git tags (stable, release-candidate, development, etc.) to find the
|
|
||||||
* highest version that has been released on any channel.
|
|
||||||
*/
|
|
||||||
private function getHighestGitTagVersion(string $root): ?string
|
|
||||||
{
|
|
||||||
$highest = null;
|
|
||||||
|
|
||||||
// Method 1: Parse version from git tag annotations / release commit messages
|
|
||||||
$output = [];
|
|
||||||
exec("cd " . escapeshellarg($root) . " && git log --all --oneline --grep='chore(version)' --grep='chore(release)' --format='%s' -20 2>/dev/null", $output);
|
|
||||||
|
|
||||||
foreach ($output as $line) {
|
|
||||||
if (preg_match('/(\d{2}\.\d{2}\.\d{2})/', $line, $m)) {
|
|
||||||
$v = preg_replace('/-(?:dev|alpha|beta|rc)$/', '', $m[1]);
|
|
||||||
if ($highest === null || version_compare($v, $highest, '>')) {
|
|
||||||
$highest = $v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method 2: Check version in remote branches' manifest files
|
|
||||||
$branches = ['origin/main', 'origin/rc', 'origin/dev'];
|
|
||||||
$manifestPaths = ['source/pkg_*.xml', 'pkg_*.xml'];
|
|
||||||
|
|
||||||
foreach ($branches as $branch) {
|
|
||||||
foreach ($manifestPaths as $pattern) {
|
|
||||||
$files = [];
|
|
||||||
exec("cd " . escapeshellarg($root) . " && git ls-tree --name-only {$branch} -- '{$pattern}' 2>/dev/null", $files);
|
|
||||||
|
|
||||||
foreach ($files as $file) {
|
|
||||||
$content = shell_exec("cd " . escapeshellarg($root) . " && git show {$branch}:{$file} 2>/dev/null");
|
|
||||||
if ($content && preg_match('#<version>(\d{2}\.\d{2}\.\d{2})(?:-(?:dev|alpha|beta|rc))?</version>#', $content, $m)) {
|
|
||||||
$v = $m[1];
|
|
||||||
if ($highest === null || version_compare($v, $highest, '>')) {
|
|
||||||
$highest = $v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $highest;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$app = new VersionBumpCli();
|
$app = new VersionBumpCli();
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\{CliFramework, SourceResolver};
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class VersionBumpRemoteCli extends CliFramework
|
class VersionBumpRemoteCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* INGROUP: mokoplatform
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/version_check.php
|
* PATH: /cli/version_check.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.26.02
|
||||||
* BRIEF: Validate version consistency across README, manifests, and sub-packages
|
* BRIEF: Validate version consistency across README, manifests, and sub-packages
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\{CliFramework, SourceResolver};
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class VersionCheckCli extends CliFramework
|
class VersionCheckCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\{CliFramework, SourceResolver};
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class VersionReadCli extends CliFramework
|
class VersionReadCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
class VersionResetDevCli extends CliFramework
|
class VersionResetDevCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\{CliFramework, SourceResolver};
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
||||||
|
|
||||||
class VersionSetPlatformCli extends CliFramework
|
class VersionSetPlatformCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
+2
-2
@@ -10,7 +10,7 @@
|
|||||||
* INGROUP: mokoplatform
|
* INGROUP: mokoplatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /cli/wiki_sync.php
|
* PATH: /cli/wiki_sync.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.26.02
|
||||||
* BRIEF: Sync select wiki pages from mokoplatform to all template repos
|
* BRIEF: Sync select wiki pages from mokoplatform to all template repos
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
class WikiSyncCli extends CliFramework
|
class WikiSyncCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
+2
-23
@@ -10,7 +10,7 @@
|
|||||||
* INGROUP: moko-platform
|
* INGROUP: moko-platform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
* PATH: /cli/workflow_sync.php
|
* PATH: /cli/workflow_sync.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.26.02
|
||||||
* BRIEF: Sync workflows from Generic → platform templates → live repos based on manifest.platform
|
* BRIEF: Sync workflows from Generic → platform templates → live repos based on manifest.platform
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
class WorkflowSyncCli extends CliFramework
|
class WorkflowSyncCli extends CliFramework
|
||||||
{
|
{
|
||||||
@@ -34,14 +34,6 @@ class WorkflowSyncCli extends CliFramework
|
|||||||
private const DEFAULT_TEMPLATE = 'Template-Generic';
|
private const DEFAULT_TEMPLATE = 'Template-Generic';
|
||||||
private const GENERIC_TEMPLATE = 'Template-Generic';
|
private const GENERIC_TEMPLATE = 'Template-Generic';
|
||||||
|
|
||||||
/**
|
|
||||||
* Workflows to exclude per platform during sync.
|
|
||||||
* Key = platform name (matching PLATFORM_TEMPLATES keys), Value = array of workflow filenames to skip.
|
|
||||||
*/
|
|
||||||
private const PLATFORM_EXCLUDES = [
|
|
||||||
'joomla' => ['deploy-manual.yml'],
|
|
||||||
];
|
|
||||||
|
|
||||||
private int $updated = 0;
|
private int $updated = 0;
|
||||||
private int $created = 0;
|
private int $created = 0;
|
||||||
private int $skipped = 0;
|
private int $skipped = 0;
|
||||||
@@ -173,13 +165,6 @@ class WorkflowSyncCli extends CliFramework
|
|||||||
foreach ($platformTemplates as $templateRepo) {
|
foreach ($platformTemplates as $templateRepo) {
|
||||||
foreach ($genericWorkflows as $workflow) {
|
foreach ($genericWorkflows as $workflow) {
|
||||||
$filename = $workflow['name'];
|
$filename = $workflow['name'];
|
||||||
// Skip platform-excluded workflows
|
|
||||||
$templatePlatform = array_search($templateRepo, self::PLATFORM_TEMPLATES, true);
|
|
||||||
if ($templatePlatform !== false && in_array($filename, self::PLATFORM_EXCLUDES[$templatePlatform] ?? [], true)) {
|
|
||||||
fprintf(STDERR, "%-45s | %s\n", "{$templateRepo}/{$filename}", 'EXCLUDED (platform)');
|
|
||||||
$this->skipped++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$destPath = '.mokogitea/workflows/' . $filename;
|
$destPath = '.mokogitea/workflows/' . $filename;
|
||||||
$label = "{$templateRepo}/{$filename}";
|
$label = "{$templateRepo}/{$filename}";
|
||||||
|
|
||||||
@@ -275,12 +260,6 @@ class WorkflowSyncCli extends CliFramework
|
|||||||
|
|
||||||
foreach ($workflows as $workflow) {
|
foreach ($workflows as $workflow) {
|
||||||
$filename = $workflow['name'];
|
$filename = $workflow['name'];
|
||||||
// Skip platform-excluded workflows
|
|
||||||
if (in_array($filename, self::PLATFORM_EXCLUDES[$platform] ?? [], true)) {
|
|
||||||
fprintf(STDERR, "%-45s | %s\n", $label, 'EXCLUDED (platform)');
|
|
||||||
$this->skipped++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$destPath = '.mokogitea/workflows/' . $filename;
|
$destPath = '.mokogitea/workflows/' . $filename;
|
||||||
$label = "{$repoFullName}/{$filename}";
|
$label = "{$repoFullName}/{$filename}";
|
||||||
|
|
||||||
|
|||||||
+6
-6
@@ -43,14 +43,15 @@
|
|||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"MokoCli\\": ["source/", "lib/Enterprise/"],
|
"MokoStandards\\": "src/",
|
||||||
"MokoCli\\Plugins\\Joomla\\": "lib/plugins/Joomla/"
|
"MokoEnterprise\\": "lib/Enterprise/",
|
||||||
|
"MokoStandards\\Plugins\\Joomla\\": "lib/plugins/Joomla/"
|
||||||
},
|
},
|
||||||
"classmap": [
|
"classmap": [
|
||||||
"lib/Enterprise/CliFramework.php"
|
"lib/Enterprise/CliFramework.php"
|
||||||
],
|
],
|
||||||
"files": [
|
"files": [
|
||||||
"source/functions.php"
|
"src/functions.php"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"archive": {
|
"archive": {
|
||||||
@@ -63,7 +64,7 @@
|
|||||||
},
|
},
|
||||||
"autoload-dev": {
|
"autoload-dev": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"MokoCli\\Tests\\": "tests/"
|
"MokoStandards\\Tests\\": "tests/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bin": [
|
"bin": [
|
||||||
@@ -92,8 +93,7 @@
|
|||||||
"check:headers": "php bin/moko check:headers -- --path .",
|
"check:headers": "php bin/moko check:headers -- --path .",
|
||||||
"check:secrets": "php bin/moko check:secrets -- --path .",
|
"check:secrets": "php bin/moko check:secrets -- --path .",
|
||||||
"check:enterprise": "php bin/moko check:enterprise -- --path .",
|
"check:enterprise": "php bin/moko check:enterprise -- --path .",
|
||||||
"drift": "php bin/moko drift",
|
"drift": "php bin/moko drift"
|
||||||
"security:advisories": "php bin/moko security:advisories"
|
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"sort-packages": true,
|
"sort-packages": true,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* INGROUP: MokoPlatform
|
* INGROUP: MokoPlatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /deploy/backup-before-deploy.php
|
* PATH: /deploy/backup-before-deploy.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.26.02
|
||||||
* BRIEF: Snapshot Joomla directories before deployment for rollback capability
|
* BRIEF: Snapshot Joomla directories before deployment for rollback capability
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
class BackupBeforeDeployCli extends CliFramework
|
class BackupBeforeDeployCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* INGROUP: MokoPlatform
|
* INGROUP: MokoPlatform
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
* PATH: /deploy/deploy-dolibarr.php
|
* PATH: /deploy/deploy-dolibarr.php
|
||||||
* VERSION: 09.32.01
|
* VERSION: 09.26.02
|
||||||
* BRIEF: Deploy Dolibarr module files to a remote server via SFTP/rsync
|
* BRIEF: Deploy Dolibarr module files to a remote server via SFTP/rsync
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||||
|
|
||||||
use MokoCli\CliFramework;
|
use MokoEnterprise\CliFramework;
|
||||||
|
|
||||||
class DeployDolibarrCli extends CliFramework
|
class DeployDolibarrCli extends CliFramework
|
||||||
{
|
{
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user