diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml index cb078c61..12bbf0b4 100644 --- a/.mokogitea/workflows/auto-bump.yml +++ b/.mokogitea/workflows/auto-bump.yml @@ -1,66 +1,66 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: mokocli.Release -# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli -# PATH: /.mokogitea/workflows/auto-bump.yml -# VERSION: 09.02.00 -# BRIEF: Auto patch-bump version on every push to dev (skips merge commits) - -name: "Universal: Auto Version Bump" - -on: - push: - branches: - - dev - - rc - - 'feature/**' - - 'patch/**' - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - -permissions: - contents: write - -jobs: - bump: - name: Version Bump - runs-on: release - if: >- - !contains(github.event.head_commit.message, '[skip ci]') && - !contains(github.event.head_commit.message, '[skip bump]') && - !startsWith(github.event.head_commit.message, 'Merge pull request') - - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.MOKOGITEA_TOKEN }} - fetch-depth: 1 - - - name: Setup mokocli tools - run: | - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - if [ -d "/opt/mokocli/cli" ]; then - echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV" - else - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \ - /tmp/mokocli - cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet - echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV" - fi - - - name: Bump version - run: | - php ${MOKO_CLI}/version_auto_bump.php \ - --path . --branch "${GITHUB_REF_NAME}" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: mokocli.Release +# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli +# PATH: /.mokogitea/workflows/auto-bump.yml +# VERSION: 09.02.00 +# BRIEF: Auto patch-bump version on every push to dev (skips merge commits) + +name: "Universal: Auto Version Bump" + +on: + push: + branches: + - dev + - rc + - 'feature/**' + - 'patch/**' + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + +permissions: + contents: write + +jobs: + bump: + name: Version Bump + runs-on: release + if: >- + !contains(github.event.head_commit.message, '[skip ci]') && + !contains(github.event.head_commit.message, '[skip bump]') && + !startsWith(github.event.head_commit.message, 'Merge pull request') + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.MOKOGITEA_TOKEN }} + fetch-depth: 1 + + - name: Setup mokocli tools + run: | + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + if [ -d "/opt/mokocli/cli" ]; then + echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV" + else + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \ + /tmp/mokocli + cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet + echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV" + fi + + - name: Bump version + run: | + php ${MOKO_CLI}/version_auto_bump.php \ + --path . --branch "${GITHUB_REF_NAME}" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index 18b67dee..5865324d 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -27,7 +27,7 @@ name: "Universal: Build & Release" on: pull_request: - types: [opened, closed] + types: [opened, synchronize, closed] branches: - main paths-ignore: @@ -52,7 +52,7 @@ on: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} @@ -66,6 +66,7 @@ jobs: runs-on: release if: >- (github.event.action == 'opened' && github.event.pull_request.merged != true) || + (github.event.action == 'synchronize' && github.event.pull_request.merged != true) || (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') steps: @@ -101,7 +102,7 @@ jobs: php ${MOKO_CLI}/branch_rename.php \ --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \ + --api-base "${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \ --pr "${{ github.event.pull_request.number }}" - name: Checkout rc and configure git @@ -120,7 +121,7 @@ jobs: - name: Update RC release notes from CHANGELOG.md run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" # Extract [Unreleased] section from changelog @@ -268,7 +269,7 @@ jobs: !startsWith(steps.platform.outputs.platform, 'joomla') run: | VERSION="${{ steps.version.outputs.version }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" SEMVER_TAG="v${VERSION}" @@ -293,7 +294,7 @@ jobs: - name: Update release notes and promote changelog run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" # Get the stable release info (version and ID) @@ -362,7 +363,7 @@ jobs: VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" RELEASE_TAG="${{ steps.version.outputs.release_tag }}" GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" php ${MOKO_CLI}/release_mirror.php \ --version "$VERSION" --tag "$RELEASE_TAG" \ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ @@ -391,7 +392,7 @@ jobs: if: steps.version.outputs.skip != 'true' continue-on-error: true run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" # Delete rc branch (ephemeral — created by promote-rc) @@ -415,7 +416,7 @@ jobs: if: steps.version.outputs.skip != 'true' continue-on-error: true run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" BRANCH_NAME="version/${VERSION}" @@ -436,7 +437,7 @@ jobs: if: steps.version.outputs.skip != 'true' continue-on-error: true run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" php ${MOKO_CLI}/version_reset_dev.php \ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \ --branch dev --path . 2>&1 || true @@ -462,5 +463,5 @@ jobs: echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY + echo "| Release | [View](${MOKOGITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY fi diff --git a/.mokogitea/workflows/ci-generic.yml b/.mokogitea/workflows/ci-generic.yml index 18ae768c..92d26853 100644 --- a/.mokogitea/workflows/ci-generic.yml +++ b/.mokogitea/workflows/ci-generic.yml @@ -13,6 +13,12 @@ name: "Generic: Project CI" on: + pull_request: + branches: + - main + - dev + - dev/** + - rc/** workflow_dispatch: permissions: diff --git a/.mokogitea/workflows/ci-issue-reporter.yml b/.mokogitea/workflows/ci-issue-reporter.yml index 06ab8a70..7ad19c8a 100644 --- a/.mokogitea/workflows/ci-issue-reporter.yml +++ b/.mokogitea/workflows/ci-issue-reporter.yml @@ -50,8 +50,8 @@ jobs: - name: Clone MokoCLI env: MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} run: | + MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}" git clone --depth 1 --filter=blob:none --sparse "${MOKOGITEA_URL}/MokoConsulting/MokoCLI.git" /tmp/mokocli cd /tmp/mokocli && git sparse-checkout set cli/ci_issue_reporter.sh @@ -59,14 +59,10 @@ jobs: env: MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - INPUT_GATE: ${{ inputs.gate }} - INPUT_DETAILS: ${{ inputs.details }} - INPUT_SEVERITY: ${{ inputs.severity }} - INPUT_WORKFLOW: ${{ inputs.workflow }} run: | chmod +x /tmp/mokocli/cli/ci_issue_reporter.sh /tmp/mokocli/cli/ci_issue_reporter.sh \ - --gate "$INPUT_GATE" \ - --details "$INPUT_DETAILS" \ - --severity "$INPUT_SEVERITY" \ - --workflow "$INPUT_WORKFLOW" + --gate "${{ inputs.gate }}" \ + --details "${{ inputs.details }}" \ + --severity "${{ inputs.severity }}" \ + --workflow "${{ inputs.workflow }}" diff --git a/.mokogitea/workflows/cleanup.yml b/.mokogitea/workflows/cleanup.yml index 3a818567..00238627 100644 --- a/.mokogitea/workflows/cleanup.yml +++ b/.mokogitea/workflows/cleanup.yml @@ -21,7 +21,7 @@ permissions: contents: write env: - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} jobs: cleanup: @@ -33,17 +33,17 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 - token: ${{ secrets.GA_TOKEN }} + token: ${{ secrets.MOKOGITEA_TOKEN }} - name: Delete merged branches env: - GA_TOKEN: ${{ secrets.GA_TOKEN }} + MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} run: | echo "=== Merged Branch Cleanup ===" - API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" + API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}" # List branches via API - BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \ + BRANCHES=$(curl -sS -H "Authorization: token ${MOKOGITEA_TOKEN}" \ "${API}/branches?limit=50" | jq -r '.[].name') DELETED=0 @@ -56,7 +56,7 @@ jobs: # Check if branch is merged into main if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then echo " Deleting merged branch: ${BRANCH}" - curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \ + curl -sS -X DELETE -H "Authorization: token ${MOKOGITEA_TOKEN}" \ "${API}/branches/${BRANCH}" 2>/dev/null || true DELETED=$((DELETED + 1)) fi @@ -66,20 +66,20 @@ jobs: - name: Clean old workflow runs env: - GA_TOKEN: ${{ secrets.GA_TOKEN }} + MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} run: | echo "=== Workflow Run Cleanup ===" - API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" + API="${MOKOGITEA_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) # Get old completed runs - RUNS=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \ + RUNS=$(curl -sS -H "Authorization: token ${MOKOGITEA_TOKEN}" \ "${API}/actions/runs?status=completed&limit=50" | \ jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null) DELETED=0 for RUN_ID in $RUNS; do - curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \ + curl -sS -X DELETE -H "Authorization: token ${MOKOGITEA_TOKEN}" \ "${API}/actions/runs/${RUN_ID}" 2>/dev/null || true DELETED=$((DELETED + 1)) done diff --git a/.mokogitea/workflows/deploy-manual.yml b/.mokogitea/workflows/deploy-manual.yml new file mode 100644 index 00000000..1af323c4 --- /dev/null +++ b/.mokogitea/workflows/deploy-manual.yml @@ -0,0 +1,126 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Deploy +# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API +# PATH: /templates/workflows/joomla/deploy-manual.yml.template +# VERSION: 04.07.00 +# BRIEF: Manual SFTP deploy to dev server for Joomla repos + +name: "Universal: Deploy to Dev (Manual)" + +on: + workflow_dispatch: + inputs: + clear_remote: + description: 'Delete all remote files before uploading' + required: false + default: 'false' + type: boolean + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +permissions: + contents: read + +jobs: + deploy: + name: SFTP Deploy to Dev + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP + run: | + php -v && composer --version + + - name: Setup MokoStandards tools + env: + MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }} + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }} + MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }} + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}' + run: | + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \ + /tmp/mokostandards-api 2>/dev/null || true + if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then + cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true + fi + + - name: Check FTP configuration + id: check + env: + HOST: ${{ vars.DEV_FTP_HOST }} + PATH_VAR: ${{ vars.DEV_FTP_PATH }} + PORT: ${{ vars.DEV_FTP_PORT }} + run: | + if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then + echo "DEV_FTP_HOST or DEV_FTP_PATH not configured -- cannot deploy" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "skip=false" >> "$GITHUB_OUTPUT" + echo "host=$HOST" >> "$GITHUB_OUTPUT" + + REMOTE="${PATH_VAR%/}" + echo "remote=$REMOTE" >> "$GITHUB_OUTPUT" + + [ -z "$PORT" ] && PORT="22" + echo "port=$PORT" >> "$GITHUB_OUTPUT" + + - name: Deploy via SFTP + if: steps.check.outputs.skip != 'true' + env: + SFTP_KEY: ${{ secrets.DEV_FTP_KEY }} + SFTP_PASS: ${{ secrets.DEV_FTP_PASSWORD }} + SFTP_USER: ${{ vars.DEV_FTP_USERNAME }} + run: | + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + [ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ -- nothing to deploy"; exit 0; } + + printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \ + "${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \ + > /tmp/sftp-config.json + + if [ -n "$SFTP_KEY" ]; then + echo "$SFTP_KEY" > /tmp/deploy_key + chmod 600 /tmp/deploy_key + printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json + else + printf ',"password":"%s"}' "$SFTP_PASS" >> /tmp/sftp-config.json + fi + + DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json) + [ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote) + + PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true) + if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then + php /tmp/mokostandards-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}" + else + php /tmp/mokostandards-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}" + fi + + rm -f /tmp/deploy_key /tmp/sftp-config.json + + - name: Summary + if: always() + run: | + if [ "${{ steps.check.outputs.skip }}" = "true" ]; then + echo "### Deploy Skipped -- FTP not configured" >> $GITHUB_STEP_SUMMARY + else + echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Host | \`${{ steps.check.outputs.host }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Remote | \`${{ steps.check.outputs.remote }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Clear | ${{ inputs.clear_remote }} |" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index efcbca7d..11958bdc 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 01.11.03 +# VERSION: 01.00.00 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" @@ -19,7 +19,7 @@ permissions: issues: write env: - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} jobs: create-branch: @@ -28,8 +28,8 @@ jobs: steps: - name: Create branch and comment run: | - TOKEN="${{ secrets.GA_TOKEN }}" - API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}" ISSUE_NUM="${{ github.event.issue.number }}" ISSUE_TITLE="${{ github.event.issue.title }}" @@ -58,7 +58,7 @@ jobs: echo "Created branch: ${BRANCH}" # Comment on issue with branch link - REPO_URL="${GITEA_URL}/${{ github.repository }}" + REPO_URL="${MOKOGITEA_URL}/${{ github.repository }}" BODY="Branch created: [\`${BRANCH}\`](${REPO_URL}/src/branch/${BRANCH})\n\n\`\`\`bash\ngit fetch origin\ngit checkout ${BRANCH}\n\`\`\`" curl -sf -X POST \ diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index d34108ce..c834bf5f 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -1,534 +1,521 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.CI -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform -# PATH: /templates/workflows/universal/pr-check.yml.template -# VERSION: 09.23.00 -# BRIEF: PR gate — branch policy + code validation before merge - -name: "Universal: PR Check" - -on: - pull_request: - types: [opened, synchronize, reopened, edited] - -permissions: - contents: read - pull-requests: write - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - # ── Branch Policy ────────────────────────────────────────────────────── - branch-policy: - name: Branch Policy - runs-on: ubuntu-latest - steps: - - name: Check branch merge target - run: | - HEAD="${{ github.head_ref }}" - BASE="${{ github.base_ref }}" - - echo "PR: ${HEAD} → ${BASE}" - - ALLOWED=true - REASON="" - - case "$HEAD" in - feature/*|feat/*) - if [ "$BASE" != "dev" ]; then - ALLOWED=false - REASON="Feature branches must target 'dev', not '${BASE}'" - fi - ;; - fix/*|bugfix/*) - if [ "$BASE" != "dev" ]; then - ALLOWED=false - REASON="Fix branches must target 'dev', not '${BASE}'" - fi - ;; - patch/*) - if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then - ALLOWED=false - REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'" - fi - ;; - hotfix/*) - if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then - ALLOWED=false - REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'" - fi - ;; - rc) - if [ "$BASE" != "main" ]; then - ALLOWED=false - REASON="RC branch can only merge into 'main', not '${BASE}'" - fi - ;; - dev) - if [ "$BASE" != "main" ]; then - ALLOWED=false - REASON="Dev branch can only merge into 'main', not '${BASE}'" - fi - ;; - esac - - if [ "$ALLOWED" = false ]; then - echo "::error::${REASON}" - echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "${REASON}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY - echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY - echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY - echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY - echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY - echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - echo "Branch policy: OK (${HEAD} → ${BASE})" - echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY - - # ── 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 ──────────────────────────────────────────────────── - validate: - name: Validate PR - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Check for merge conflict markers - run: | - CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true) - if [ -n "$CONFLICTS" ]; then - echo "::error::Merge conflict markers found in source files" - echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - exit 1 - fi - echo "No conflict markers found" - - - name: Detect platform - id: platform - run: | - # Read platform from XML manifest ( tag) or plain text fallback - PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1) - [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]') - [ -z "$PLATFORM" ] && PLATFORM="generic" - echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" - - - name: Setup PHP - if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' - run: | - if ! command -v php &> /dev/null; then - sudo apt-get update -qq - sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1 - fi - - - name: PHP syntax check - if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' - run: | - ERRORS=0 - while IFS= read -r -d '' file; do - if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then - ERRORS=$((ERRORS + 1)) - fi - done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0) - echo "PHP lint: ${ERRORS} error(s)" - [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; } - - - name: Joomla JEXEC guard check - if: steps.platform.outputs.platform == 'joomla' - run: | - ERRORS=0 - while IFS= read -r -d '' file; do - # Skip vendor, node_modules, and index.html stub files - case "$file" in ./vendor/*|./node_modules/*) continue ;; esac - # Check first 10 lines for JEXEC or JPATH guard - if ! head -20 "$file" | grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]"; then - echo "::error file=${file}::Missing JEXEC guard: ${file}" - ERRORS=$((ERRORS + 1)) - fi - done < <(find . -name "*.php" -path "*/src/*" -not -path "./.git/*" -not -path "./vendor/*" -print0) - if [ "$ERRORS" -gt 0 ]; then - echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard" - echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY - echo "${ERRORS} file(s) in src/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY - exit 1 - fi - echo "JEXEC guard: OK" - - - name: Joomla directory listing protection - if: steps.platform.outputs.platform == 'joomla' - run: | - MISSING=0 - SOURCE_DIR="src" - [ ! -d "$SOURCE_DIR" ] && exit 0 - while IFS= read -r dir; do - if [ ! -f "${dir}/index.html" ]; then - echo "::warning::Missing index.html in ${dir} (directory listing protection)" - MISSING=$((MISSING + 1)) - fi - done < <(find "$SOURCE_DIR" -type d -not -path "./.git/*" -not -path "*/vendor/*" -not -path "*/node_modules/*") - if [ "$MISSING" -gt 0 ]; then - echo "## Directory Protection" >> $GITHUB_STEP_SUMMARY - echo "${MISSING} director(ies) missing index.html" >> $GITHUB_STEP_SUMMARY - fi - echo "Directory protection: ${MISSING} missing (advisory)" - - - name: Joomla script file and asset checks - if: steps.platform.outputs.platform == 'joomla' - run: | - ERRORS=0 - MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) - [ -z "$MANIFEST" ] && exit 0 - MANIFEST_DIR=$(dirname "$MANIFEST") - - # Check scriptfile exists if declared - SCRIPTFILE=$(sed -n 's/.*\([^<]*\)<\/scriptfile>.*/\1/p' "$MANIFEST" 2>/dev/null) - if [ -n "$SCRIPTFILE" ]; then - if [ ! -f "${MANIFEST_DIR}/${SCRIPTFILE}" ]; then - echo "::error::Manifest declares ${SCRIPTFILE} but file not found at ${MANIFEST_DIR}/${SCRIPTFILE}" - ERRORS=$((ERRORS + 1)) - else - echo "Script file: ${MANIFEST_DIR}/${SCRIPTFILE} (OK)" - fi - fi - - # Require joomla.asset.json and validate it - ASSET_JSON=$(find "$MANIFEST_DIR" -name "joomla.asset.json" -not -path "./.git/*" 2>/dev/null | head -1) - if [ -z "$ASSET_JSON" ]; then - echo "::error::joomla.asset.json not found — Joomla asset system is required" - ERRORS=$((ERRORS + 1)) - else - if command -v php &> /dev/null; then - php -r "json_decode(file_get_contents('$ASSET_JSON')); if(json_last_error()!==JSON_ERROR_NONE){echo json_last_error_msg();exit(1);}" 2>&1 || { - echo "::error::joomla.asset.json is not valid JSON" - ERRORS=$((ERRORS + 1)) - } - fi - echo "joomla.asset.json: valid" - fi - - # Validate all XML files in src/ are well-formed - XML_ERRORS=0 - if command -v php &> /dev/null; then - while IFS= read -r -d '' xmlfile; do - if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$xmlfile'); if(!\$x){foreach(libxml_get_errors() as \$e) echo trim(\$e->message) . ' in $xmlfile'; exit(1);}" 2>&1; then - XML_ERRORS=$((XML_ERRORS + 1)) - fi - done < <(find "$MANIFEST_DIR" -name "*.xml" -not -path "./.git/*" -print0) - fi - if [ "$XML_ERRORS" -gt 0 ]; then - echo "::error::${XML_ERRORS} XML file(s) are malformed" - ERRORS=$((ERRORS + 1)) - else - echo "XML well-formedness: OK" - fi - - [ "$ERRORS" -gt 0 ] && exit 1 - echo "Joomla asset checks: OK" - - - name: Validate platform manifest - run: | - PLATFORM="${{ steps.platform.outputs.platform }}" - case "$PLATFORM" in - joomla) - MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) - if [ -z "$MANIFEST" ]; then - echo "::warning::No Joomla manifest found (WaaS site)" - exit 0 - fi - echo "Manifest: ${MANIFEST}" - if command -v php &> /dev/null; then - php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; } - fi - for ELEMENT in name version description; do - grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; } - done - # Block legacy raw/branch update server URLs on MokoGitea - RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true) - if [ -n "$RAW_URLS" ]; then - echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the Gitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)" - echo "$RAW_URLS" - exit 1 - fi - echo "Joomla manifest valid" - ;; - dolibarr) - MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1) - if [ -z "$MOD_FILE" ]; then - echo "::error::No mod*.class.php found" - exit 1 - fi - echo "Dolibarr module: ${MOD_FILE}" - ;; - *) - echo "Generic platform — no manifest validation" - ;; - esac - - - name: Check update stream format - run: | - PLATFORM="${{ steps.platform.outputs.platform }}" - case "$PLATFORM" in - joomla) - if [ -f "updates.xml" ]; then - if command -v php &> /dev/null; then - php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; } - fi - echo "updates.xml valid" - fi - ;; - dolibarr) - [ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt" - ;; - esac - - - name: Validate Joomla language files - if: steps.platform.outputs.platform == 'joomla' - run: | - ERRORS=0 - WARNINGS=0 - - # Require both en-GB and en-US language directories - LANG_ROOT=$(find . -path "*/language" -type d -not -path "./.git/*" 2>/dev/null | head -1) - if [ -z "$LANG_ROOT" ]; then - echo "No language/ directory found — skipping" - exit 0 - fi - - if [ ! -d "$LANG_ROOT/en-GB" ]; then - echo "::error::Missing en-GB language directory (${LANG_ROOT}/en-GB)" - ERRORS=$((ERRORS + 1)) - fi - if [ ! -d "$LANG_ROOT/en-US" ]; then - echo "::error::Missing en-US language directory (${LANG_ROOT}/en-US)" - ERRORS=$((ERRORS + 1)) - fi - - # Check that en-GB and en-US have matching .ini files - if [ -d "$LANG_ROOT/en-GB" ] && [ -d "$LANG_ROOT/en-US" ]; then - for GB_INI in "$LANG_ROOT/en-GB"/*.ini; do - [ ! -f "$GB_INI" ] && continue - US_INI="$LANG_ROOT/en-US/$(basename "$GB_INI")" - if [ ! -f "$US_INI" ]; then - echo "::error::$(basename "$GB_INI") exists in en-GB but missing from en-US" - ERRORS=$((ERRORS + 1)) - fi - done - for US_INI in "$LANG_ROOT/en-US"/*.ini; do - [ ! -f "$US_INI" ] && continue - GB_INI="$LANG_ROOT/en-GB/$(basename "$US_INI")" - if [ ! -f "$GB_INI" ]; then - echo "::error::$(basename "$US_INI") exists in en-US but missing from en-GB" - ERRORS=$((ERRORS + 1)) - fi - done - fi - - # Find all .ini language files - INI_FILES=$(find . -path "*/language/*/*.ini" -not -path "./.git/*" 2>/dev/null) - if [ -z "$INI_FILES" ]; then - echo "No .ini language files found" - [ "$ERRORS" -gt 0 ] && exit 1 - exit 0 - fi - - echo "Found $(echo "$INI_FILES" | wc -l) language file(s)" - - for FILE in $INI_FILES; do - FNAME=$(basename "$FILE") - LINENUM=0 - SEEN_KEYS="" - - while IFS= read -r line || [ -n "$line" ]; do - LINENUM=$((LINENUM + 1)) - - # Skip empty lines and comments - [ -z "$line" ] && continue - echo "$line" | grep -qE '^\s*;' && continue - echo "$line" | grep -qE '^\s*$' && continue - - # Must match KEY="VALUE" format - if ! echo "$line" | grep -qE '^[A-Z_][A-Z0-9_]*=".*"$'; then - echo "::error file=${FILE},line=${LINENUM}::Malformed line: ${line}" - ERRORS=$((ERRORS + 1)) - continue - fi - - # Extract key and check for duplicates - KEY=$(echo "$line" | sed 's/=.*//') - if echo "$SEEN_KEYS" | grep -qx "$KEY"; then - echo "::error file=${FILE},line=${LINENUM}::Duplicate key: ${KEY}" - ERRORS=$((ERRORS + 1)) - fi - SEEN_KEYS="${SEEN_KEYS} - ${KEY}" - done < "$FILE" - - echo " ${FILE}: checked ${LINENUM} lines" - done - - # Cross-check en-GB vs en-US key consistency - GB_DIR=$(find . -path "*/language/en-GB" -type d -not -path "./.git/*" 2>/dev/null | head -1) - US_DIR=$(find . -path "*/language/en-US" -type d -not -path "./.git/*" 2>/dev/null | head -1) - - if [ -n "$GB_DIR" ] && [ -n "$US_DIR" ]; then - for GB_FILE in "$GB_DIR"/*.ini; do - [ ! -f "$GB_FILE" ] && continue - FNAME=$(basename "$GB_FILE") - US_FILE="$US_DIR/$FNAME" - [ ! -f "$US_FILE" ] && continue - - GB_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$GB_FILE" 2>/dev/null | sort) - US_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$US_FILE" 2>/dev/null | sort) - - # Keys in en-GB but not en-US - MISSING_US=$(comm -23 <(echo "$GB_KEYS") <(echo "$US_KEYS")) - if [ -n "$MISSING_US" ]; then - echo "::warning::Keys in en-GB/$FNAME but missing from en-US/$FNAME:" - echo "$MISSING_US" | while read -r k; do echo " - $k"; done - WARNINGS=$((WARNINGS + 1)) - fi - - # Keys in en-US but not en-GB - MISSING_GB=$(comm -13 <(echo "$GB_KEYS") <(echo "$US_KEYS")) - if [ -n "$MISSING_GB" ]; then - echo "::warning::Keys in en-US/$FNAME but missing from en-GB/$FNAME:" - echo "$MISSING_GB" | while read -r k; do echo " - $k"; done - WARNINGS=$((WARNINGS + 1)) - fi - done - fi - - { - echo "### Language File Validation" - echo "| Metric | Count |" - echo "|---|---|" - echo "| Files checked | $(echo "$INI_FILES" | wc -l) |" - echo "| Errors | ${ERRORS} |" - echo "| Warnings | ${WARNINGS} |" - } >> $GITHUB_STEP_SUMMARY - - if [ "$ERRORS" -gt 0 ]; then - echo "::error::Language validation failed with ${ERRORS} error(s)" - exit 1 - fi - echo "Language files: OK (${WARNINGS} warning(s))" - - - name: Check changelog has unreleased entry - run: | - if [ ! -f "CHANGELOG.md" ]; then - echo "::warning::No CHANGELOG.md found" - exit 0 - fi - # Check for content under [Unreleased] section - if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then - echo "::error::CHANGELOG.md missing [Unreleased] section" - exit 1 - fi - # Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased - UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true) - if [ "$UNRELEASED_CONTENT" -eq 0 ]; then - echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes." - echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY - echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY - echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]" - - - name: Verify package source - run: | - SOURCE_DIR="src" - [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" - if [ ! -d "$SOURCE_DIR" ]; then - echo "::warning::No src/ or htdocs/ directory" - exit 0 - fi - FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l) - echo "Source: ${FILE_COUNT} files" - [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; } - - # ── Pre-Release RC Build ───────────────────────────────────────────────── - pre-release: - name: Build RC Package - runs-on: ubuntu-latest - needs: [branch-policy, validate] - - steps: - - name: Trigger RC pre-release - env: - GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - REPO: ${{ github.repository }} - BRANCH: ${{ github.head_ref }} - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - run: | - curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}" - echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY - echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY - - # ── Issue Reporter ────────────────────────────────────────────────────── - report-issues: - name: Report Issues - runs-on: ubuntu-latest - needs: [branch-policy, validate] - if: >- - always() && - needs.validate.result == 'failure' - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - sparse-checkout: automation/ci-issue-reporter.sh - sparse-checkout-cone-mode: false - - - name: "File issue for PR validation failure" - env: - GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - run: | - chmod +x automation/ci-issue-reporter.sh - ./automation/ci-issue-reporter.sh \ - --gate "PR Validation" \ - --workflow "PR Check" \ - --severity error \ - --details "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed." +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.CI +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform +# PATH: /templates/workflows/universal/pr-check.yml.template +# VERSION: 09.23.00 +# BRIEF: PR gate — branch policy + code validation before merge + +name: "Universal: PR Check" + +on: + pull_request: + types: [opened, synchronize, reopened, edited] + +permissions: + contents: read + pull-requests: write + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + # ── Branch Policy ────────────────────────────────────────────────────── + branch-policy: + name: Branch Policy + runs-on: ubuntu-latest + steps: + - name: Check branch merge target + run: | + HEAD="${{ github.head_ref }}" + BASE="${{ github.base_ref }}" + + echo "PR: ${HEAD} → ${BASE}" + + ALLOWED=true + REASON="" + + case "$HEAD" in + feature/*|feat/*) + if [ "$BASE" != "dev" ]; then + ALLOWED=false + REASON="Feature branches must target 'dev', not '${BASE}'" + fi + ;; + fix/*|bugfix/*) + if [ "$BASE" != "dev" ]; then + ALLOWED=false + REASON="Fix branches must target 'dev', not '${BASE}'" + fi + ;; + patch/*) + if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then + ALLOWED=false + REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'" + fi + ;; + hotfix/*) + if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'" + fi + ;; + rc) + if [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="RC branch can only merge into 'main', not '${BASE}'" + fi + ;; + dev) + if [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Dev branch can only merge into 'main', not '${BASE}'" + fi + ;; + esac + + if [ "$ALLOWED" = false ]; then + echo "::error::${REASON}" + echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "${REASON}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY + echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY + echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY + echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY + echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY + echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + echo "Branch policy: OK (${HEAD} → ${BASE})" + echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY + + # ── 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 ──────────────────────────────────────────────────── + validate: + name: Validate PR + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check for merge conflict markers + run: | + CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true) + if [ -n "$CONFLICTS" ]; then + echo "::error::Merge conflict markers found in source files" + echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "No conflict markers found" + + - name: Detect platform + id: platform + run: | + # Read platform from XML manifest ( tag) or plain text fallback + PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1) + [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]') + [ -z "$PLATFORM" ] && PLATFORM="generic" + echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" + + - name: Setup PHP + if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' + run: | + if ! command -v php &> /dev/null; then + sudo apt-get update -qq + sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1 + fi + + - name: PHP syntax check + if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' + run: | + ERRORS=0 + while IFS= read -r -d '' file; do + if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then + ERRORS=$((ERRORS + 1)) + fi + done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0) + echo "PHP lint: ${ERRORS} error(s)" + [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; } + + - name: Joomla JEXEC guard check + if: steps.platform.outputs.platform == 'joomla' + run: | + ERRORS=0 + while IFS= read -r -d '' file; do + # Skip vendor, node_modules, and index.html stub files + case "$file" in ./vendor/*|./node_modules/*) continue ;; esac + # Check first 10 lines for JEXEC or JPATH guard + if ! head -20 "$file" | grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]"; then + echo "::error file=${file}::Missing JEXEC guard: ${file}" + ERRORS=$((ERRORS + 1)) + fi + done < <(find . -name "*.php" -path "*/src/*" -not -path "./.git/*" -not -path "./vendor/*" -print0) + if [ "$ERRORS" -gt 0 ]; then + echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard" + echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY + echo "${ERRORS} file(s) in src/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "JEXEC guard: OK" + + - name: Joomla directory listing protection + if: steps.platform.outputs.platform == 'joomla' + run: | + MISSING=0 + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && exit 0 + while IFS= read -r dir; do + if [ ! -f "${dir}/index.html" ]; then + echo "::warning::Missing index.html in ${dir} (directory listing protection)" + MISSING=$((MISSING + 1)) + fi + done < <(find "$SOURCE_DIR" -type d -not -path "./.git/*" -not -path "*/vendor/*" -not -path "*/node_modules/*") + if [ "$MISSING" -gt 0 ]; then + echo "## Directory Protection" >> $GITHUB_STEP_SUMMARY + echo "${MISSING} director(ies) missing index.html" >> $GITHUB_STEP_SUMMARY + fi + echo "Directory protection: ${MISSING} missing (advisory)" + + - name: Joomla script file and asset checks + if: steps.platform.outputs.platform == 'joomla' + run: | + ERRORS=0 + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) + [ -z "$MANIFEST" ] && exit 0 + MANIFEST_DIR=$(dirname "$MANIFEST") + + # Check scriptfile exists if declared + SCRIPTFILE=$(sed -n 's/.*\([^<]*\)<\/scriptfile>.*/\1/p' "$MANIFEST" 2>/dev/null) + if [ -n "$SCRIPTFILE" ]; then + if [ ! -f "${MANIFEST_DIR}/${SCRIPTFILE}" ]; then + echo "::error::Manifest declares ${SCRIPTFILE} but file not found at ${MANIFEST_DIR}/${SCRIPTFILE}" + ERRORS=$((ERRORS + 1)) + else + echo "Script file: ${MANIFEST_DIR}/${SCRIPTFILE} (OK)" + fi + fi + + # Require joomla.asset.json and validate it + ASSET_JSON=$(find "$MANIFEST_DIR" -name "joomla.asset.json" -not -path "./.git/*" 2>/dev/null | head -1) + if [ -z "$ASSET_JSON" ]; then + echo "::error::joomla.asset.json not found — Joomla asset system is required" + ERRORS=$((ERRORS + 1)) + else + if command -v php &> /dev/null; then + php -r "json_decode(file_get_contents('$ASSET_JSON')); if(json_last_error()!==JSON_ERROR_NONE){echo json_last_error_msg();exit(1);}" 2>&1 || { + echo "::error::joomla.asset.json is not valid JSON" + ERRORS=$((ERRORS + 1)) + } + fi + echo "joomla.asset.json: valid" + fi + + # Validate all XML files in src/ are well-formed + XML_ERRORS=0 + if command -v php &> /dev/null; then + while IFS= read -r -d '' xmlfile; do + if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$xmlfile'); if(!\$x){foreach(libxml_get_errors() as \$e) echo trim(\$e->message) . ' in $xmlfile'; exit(1);}" 2>&1; then + XML_ERRORS=$((XML_ERRORS + 1)) + fi + done < <(find "$MANIFEST_DIR" -name "*.xml" -not -path "./.git/*" -print0) + fi + if [ "$XML_ERRORS" -gt 0 ]; then + echo "::error::${XML_ERRORS} XML file(s) are malformed" + ERRORS=$((ERRORS + 1)) + else + echo "XML well-formedness: OK" + fi + + [ "$ERRORS" -gt 0 ] && exit 1 + echo "Joomla asset checks: OK" + + - name: Validate platform manifest + run: | + PLATFORM="${{ steps.platform.outputs.platform }}" + case "$PLATFORM" in + joomla) + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) + if [ -z "$MANIFEST" ]; then + echo "::warning::No Joomla manifest found (WaaS site)" + exit 0 + fi + echo "Manifest: ${MANIFEST}" + if command -v php &> /dev/null; then + php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; } + fi + for ELEMENT in name version description; do + grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; } + done + # Block legacy raw/branch update server URLs on MokoGitea + RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true) + if [ -n "$RAW_URLS" ]; then + echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the Gitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)" + echo "$RAW_URLS" + exit 1 + fi + echo "Joomla manifest valid" + ;; + dolibarr) + MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1) + if [ -z "$MOD_FILE" ]; then + echo "::error::No mod*.class.php found" + exit 1 + fi + echo "Dolibarr module: ${MOD_FILE}" + ;; + *) + echo "Generic platform — no manifest validation" + ;; + esac + + - name: Check update stream format + run: | + PLATFORM="${{ steps.platform.outputs.platform }}" + case "$PLATFORM" in + joomla) + if [ -f "updates.xml" ]; then + if command -v php &> /dev/null; then + php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; } + fi + echo "updates.xml valid" + fi + ;; + dolibarr) + [ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt" + ;; + esac + + - name: Validate Joomla language files + if: steps.platform.outputs.platform == 'joomla' + run: | + ERRORS=0 + WARNINGS=0 + + # Require both en-GB and en-US language directories + LANG_ROOT=$(find . -path "*/language" -type d -not -path "./.git/*" 2>/dev/null | head -1) + if [ -z "$LANG_ROOT" ]; then + echo "No language/ directory found — skipping" + exit 0 + fi + + if [ ! -d "$LANG_ROOT/en-GB" ]; then + echo "::error::Missing en-GB language directory (${LANG_ROOT}/en-GB)" + ERRORS=$((ERRORS + 1)) + fi + if [ ! -d "$LANG_ROOT/en-US" ]; then + echo "::error::Missing en-US language directory (${LANG_ROOT}/en-US)" + ERRORS=$((ERRORS + 1)) + fi + + # Check that en-GB and en-US have matching .ini files + if [ -d "$LANG_ROOT/en-GB" ] && [ -d "$LANG_ROOT/en-US" ]; then + for GB_INI in "$LANG_ROOT/en-GB"/*.ini; do + [ ! -f "$GB_INI" ] && continue + US_INI="$LANG_ROOT/en-US/$(basename "$GB_INI")" + if [ ! -f "$US_INI" ]; then + echo "::error::$(basename "$GB_INI") exists in en-GB but missing from en-US" + ERRORS=$((ERRORS + 1)) + fi + done + for US_INI in "$LANG_ROOT/en-US"/*.ini; do + [ ! -f "$US_INI" ] && continue + GB_INI="$LANG_ROOT/en-GB/$(basename "$US_INI")" + if [ ! -f "$GB_INI" ]; then + echo "::error::$(basename "$US_INI") exists in en-US but missing from en-GB" + ERRORS=$((ERRORS + 1)) + fi + done + fi + + # Find all .ini language files + INI_FILES=$(find . -path "*/language/*/*.ini" -not -path "./.git/*" 2>/dev/null) + if [ -z "$INI_FILES" ]; then + echo "No .ini language files found" + [ "$ERRORS" -gt 0 ] && exit 1 + exit 0 + fi + + echo "Found $(echo "$INI_FILES" | wc -l) language file(s)" + + for FILE in $INI_FILES; do + FNAME=$(basename "$FILE") + LINENUM=0 + SEEN_KEYS="" + + while IFS= read -r line || [ -n "$line" ]; do + LINENUM=$((LINENUM + 1)) + + # Skip empty lines and comments + [ -z "$line" ] && continue + echo "$line" | grep -qE '^\s*;' && continue + echo "$line" | grep -qE '^\s*$' && continue + + # Must match KEY="VALUE" format + if ! echo "$line" | grep -qE '^[A-Z_][A-Z0-9_]*=".*"$'; then + echo "::error file=${FILE},line=${LINENUM}::Malformed line: ${line}" + ERRORS=$((ERRORS + 1)) + continue + fi + + # Extract key and check for duplicates + KEY=$(echo "$line" | sed 's/=.*//') + if echo "$SEEN_KEYS" | grep -qx "$KEY"; then + echo "::error file=${FILE},line=${LINENUM}::Duplicate key: ${KEY}" + ERRORS=$((ERRORS + 1)) + fi + SEEN_KEYS="${SEEN_KEYS} + ${KEY}" + done < "$FILE" + + echo " ${FILE}: checked ${LINENUM} lines" + done + + # Cross-check en-GB vs en-US key consistency + GB_DIR=$(find . -path "*/language/en-GB" -type d -not -path "./.git/*" 2>/dev/null | head -1) + US_DIR=$(find . -path "*/language/en-US" -type d -not -path "./.git/*" 2>/dev/null | head -1) + + if [ -n "$GB_DIR" ] && [ -n "$US_DIR" ]; then + for GB_FILE in "$GB_DIR"/*.ini; do + [ ! -f "$GB_FILE" ] && continue + FNAME=$(basename "$GB_FILE") + US_FILE="$US_DIR/$FNAME" + [ ! -f "$US_FILE" ] && continue + + GB_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$GB_FILE" 2>/dev/null | sort) + US_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$US_FILE" 2>/dev/null | sort) + + # Keys in en-GB but not en-US + MISSING_US=$(comm -23 <(echo "$GB_KEYS") <(echo "$US_KEYS")) + if [ -n "$MISSING_US" ]; then + echo "::warning::Keys in en-GB/$FNAME but missing from en-US/$FNAME:" + echo "$MISSING_US" | while read -r k; do echo " - $k"; done + WARNINGS=$((WARNINGS + 1)) + fi + + # Keys in en-US but not en-GB + MISSING_GB=$(comm -13 <(echo "$GB_KEYS") <(echo "$US_KEYS")) + if [ -n "$MISSING_GB" ]; then + echo "::warning::Keys in en-US/$FNAME but missing from en-GB/$FNAME:" + echo "$MISSING_GB" | while read -r k; do echo " - $k"; done + WARNINGS=$((WARNINGS + 1)) + fi + done + fi + + { + echo "### Language File Validation" + echo "| Metric | Count |" + echo "|---|---|" + echo "| Files checked | $(echo "$INI_FILES" | wc -l) |" + echo "| Errors | ${ERRORS} |" + echo "| Warnings | ${WARNINGS} |" + } >> $GITHUB_STEP_SUMMARY + + if [ "$ERRORS" -gt 0 ]; then + echo "::error::Language validation failed with ${ERRORS} error(s)" + exit 1 + fi + echo "Language files: OK (${WARNINGS} warning(s))" + + - name: Check changelog has unreleased entry + run: | + if [ ! -f "CHANGELOG.md" ]; then + echo "::warning::No CHANGELOG.md found" + exit 0 + fi + # Check for content under [Unreleased] section + if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then + echo "::error::CHANGELOG.md missing [Unreleased] section" + exit 1 + fi + # Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased + UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true) + if [ "$UNRELEASED_CONTENT" -eq 0 ]; then + echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes." + echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY + echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY + echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]" + + - name: Verify package source + run: | + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + if [ ! -d "$SOURCE_DIR" ]; then + echo "::warning::No src/ or htdocs/ directory" + exit 0 + fi + FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l) + echo "Source: ${FILE_COUNT} files" + [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; } + + # ── Pre-Release RC Build ───────────────────────────────────────────────── + pre-release: + name: Build RC Package + runs-on: ubuntu-latest + needs: [branch-policy, validate] + + steps: + - name: Trigger RC pre-release + env: + MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + REPO: ${{ github.repository }} + BRANCH: ${{ github.head_ref }} + MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + run: | + curl -s -X POST "${MOKOGITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${MOKOGITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}" + echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY + echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY + + # ── Issue Reporter ────────────────────────────────────────────────────── + report-issues: + name: Report Issues + needs: [branch-policy, validate] + if: >- + always() && + needs.validate.result == 'failure' + uses: ./.mokogitea/workflows/ci-issue-reporter.yml + with: + gate: "PR Validation" + workflow: "PR Check" + severity: error + details: "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed." + secrets: inherit diff --git a/.mokogitea/workflows/pr-metadata-check.yml b/.mokogitea/workflows/pr-metadata-check.yml index 68b75896..b4c9cbd4 100644 --- a/.mokogitea/workflows/pr-metadata-check.yml +++ b/.mokogitea/workflows/pr-metadata-check.yml @@ -20,7 +20,7 @@ permissions: contents: read env: - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} @@ -55,14 +55,14 @@ jobs: - name: Validate metadata against Joomla manifest env: - GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} run: | php ${MOKO_CLI}/joomla_metadata_validate.php \ --path . \ - --token "${GITEA_TOKEN}" \ + --token "${MOKOGITEA_TOKEN}" \ --org "${GITEA_ORG}" \ --repo "${GITEA_REPO}" \ - --api-base "${GITEA_URL}/api/v1" \ + --api-base "${MOKOGITEA_URL}/api/v1" \ --ci if [ $? -ne 0 ]; then diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index 4fd80eb3..efb3d1b4 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -7,7 +7,7 @@ # INGROUP: mokocli.Release # REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # PATH: /templates/workflows/universal/pre-release.yml.template -# VERSION: 05.01.00 +# VERSION: 05.02.00 # BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches name: "Universal: Pre-Release" @@ -59,6 +59,11 @@ jobs: fetch-depth: 0 token: ${{ secrets.MOKOGITEA_TOKEN }} ref: ${{ github.ref_name }} + submodules: recursive + + - name: Update submodules to main + run: | + git submodule foreach --quiet 'git checkout main && git pull --quiet origin main' 2>/dev/null || true - name: Setup mokocli tools env: diff --git a/.mokogitea/workflows/rc-revert.yml b/.mokogitea/workflows/rc-revert.yml index 5e61de81..82715938 100644 --- a/.mokogitea/workflows/rc-revert.yml +++ b/.mokogitea/workflows/rc-revert.yml @@ -29,12 +29,20 @@ jobs: steps: - name: Rename branch + env: + BRANCH: ${{ github.event.pull_request.head.ref }} + REPO: ${{ github.repository }} + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} run: | - BRANCH="${{ github.event.pull_request.head.ref }}" + set -euo pipefail + # BRANCH is attacker-controlled (PR head ref). Strict allowlist before ANY use. + if ! printf '%s' "$BRANCH" | grep -Eq '^rc/[A-Za-z0-9._/-]+$'; then + echo "::error::Refusing unsafe branch name: $BRANCH"; exit 1 + fi SUFFIX="${BRANCH#rc/}" DEV_BRANCH="dev/${SUFFIX}" - API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + API="${GITEA_URL}/api/v1/repos/${REPO}/branches" # Create dev/ branch from rc/ branch STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \ @@ -42,25 +50,22 @@ jobs: -H "Content-Type: application/json" \ -d "{\"new_branch_name\": \"${DEV_BRANCH}\", \"old_branch_name\": \"${BRANCH}\"}" \ "${API}" 2>/dev/null || true) - if [ "$STATUS" = "201" ]; then - echo "Created branch: ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY + echo "Created branch: ${DEV_BRANCH}" >> "$GITHUB_STEP_SUMMARY" else - echo "::error::Failed to create ${DEV_BRANCH} from ${BRANCH} (HTTP ${STATUS})" - exit 1 + echo "::error::Failed to create ${DEV_BRANCH} from ${BRANCH} (HTTP ${STATUS})"; exit 1 fi - # Delete rc/ branch - ENCODED=$(php -r "echo rawurlencode('${BRANCH}');") + # Read BRANCH from the environment inside PHP (getenv, no string interpolation -> no PHP injection) + ENCODED=$(php -r 'echo rawurlencode(getenv("BRANCH"));') STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \ -H "Authorization: token ${TOKEN}" \ "${API}/${ENCODED}" 2>/dev/null || true) - if [ "$STATUS" = "204" ]; then - echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY + echo "Deleted branch: ${BRANCH}" >> "$GITHUB_STEP_SUMMARY" else echo "::warning::Failed to delete ${BRANCH} (HTTP ${STATUS})" fi - echo "### RC Reverted" >> $GITHUB_STEP_SUMMARY - echo "${BRANCH} → ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY + echo "### RC Reverted" >> "$GITHUB_STEP_SUMMARY" + echo "${BRANCH} → ${DEV_BRANCH}" >> "$GITHUB_STEP_SUMMARY" diff --git a/.mokogitea/workflows/repo-health.yml b/.mokogitea/workflows/repo-health.yml index 154f77dd..092b60e5 100644 --- a/.mokogitea/workflows/repo-health.yml +++ b/.mokogitea/workflows/repo-health.yml @@ -1,712 +1,700 @@ -# ============================================================================ -# Copyright (C) 2025 Moko Consulting -# -# This file is part of a Moko Consulting project. -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: mokocli.Validation -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli -# PATH: /templates/workflows/joomla/repo_health.yml.template -# VERSION: 09.23.00 -# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts. -# ============================================================================ - -name: "Generic: Repo Health" - -defaults: - run: - shell: bash - -on: - workflow_dispatch: - inputs: - profile: - description: 'Validation profile: all, scripts, or repo' - required: true - default: all - type: choice - options: - - all - - scripts - - repo - pull_request: - branches: - - main - -permissions: - contents: read - -env: - # Scripts governance policy - SCRIPTS_REQUIRED_DIRS: - SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate - - # Repo health policy - REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.mokogitea/workflows/ - REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/ - REPO_DISALLOWED_DIRS: - REPO_DISALLOWED_FILES: TODO.md,todo.md - - # Extended checks toggles - EXTENDED_CHECKS: "true" - - # File / directory variables - DOCS_INDEX: docs/docs-index.md - SCRIPT_DIR: scripts - WORKFLOWS_DIR: .mokogitea/workflows - SHELLCHECK_PATTERN: '*.sh' - SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml' - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - access_check: - name: Access control - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: - contents: read - - outputs: - allowed: ${{ steps.perm.outputs.allowed }} - permission: ${{ steps.perm.outputs.permission }} - - steps: - - name: Check actor permission (admin only) - id: perm - env: - TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }} - REPO: ${{ github.repository }} - ACTOR: ${{ github.actor }} - run: | - set -euo pipefail - ALLOWED=false - PERMISSION=unknown - METHOD="" - - # Hardcoded authorized users — always allowed - case "$ACTOR" in - jmiller|gitea-actions[bot]) - ALLOWED=true - PERMISSION=admin - METHOD="hardcoded allowlist" - ;; - *) - # Detect platform and check permissions via API - API_BASE="${GITHUB_API_URL:-${GITEA_API_URL:-https://api.github.com}}" - RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}') - PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown") - if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "maintain" ] || [ "$PERMISSION" = "owner" ]; then - ALLOWED=true - fi - METHOD="collaborator API" - ;; - esac - - echo "permission=${PERMISSION}" >> "$GITHUB_OUTPUT" - echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT" - - { - echo "## Access Authorization" - echo "" - echo "| Field | Value |" - echo "|-------|-------|" - echo "| **Actor** | \`${ACTOR}\` |" - echo "| **Repository** | \`${REPO}\` |" - echo "| **Permission** | \`${PERMISSION}\` |" - echo "| **Method** | ${METHOD} |" - echo "| **Authorized** | ${ALLOWED} |" - echo "" - if [ "$ALLOWED" = "true" ]; then - echo "${ACTOR} authorized (${METHOD})" - else - echo "${ACTOR} is NOT authorized. Requires admin or maintain role." - fi - } >> "${GITHUB_STEP_SUMMARY}" - - - name: Deny execution when not permitted - if: ${{ steps.perm.outputs.allowed != 'true' }} - run: | - set -euo pipefail - printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}" - exit 1 - - scripts_governance: - name: Scripts governance - needs: access_check - if: ${{ needs.access_check.outputs.allowed == 'true' }} - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: read - - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - fetch-depth: 0 - - - name: Scripts folder checks - env: - PROFILE_RAW: ${{ github.event.inputs.profile }} - run: | - set -euo pipefail - - profile="${PROFILE_RAW:-all}" - case "${profile}" in - all|scripts|repo) ;; - *) - printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" - exit 1 - ;; - esac - - if [ "${profile}" = 'repo' ]; then - { - printf '%s\n' '### Scripts governance' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' 'Status: SKIPPED' - printf '%s\n' 'Reason: profile excludes scripts governance' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - exit 0 - fi - - if [ ! -d "${SCRIPT_DIR}" ]; then - { - printf '%s\n' '### Scripts governance' - printf '%s\n' 'Status: OK (advisory)' - printf '%s\n' 'scripts/ directory not present. No scripts governance enforced.' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - exit 0 - fi - - if [ -n "${SCRIPTS_REQUIRED_DIRS:-}" ]; then IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"; else required_dirs=(); fi - IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}" - - missing_dirs=() - unapproved_dirs=() - - for d in "${required_dirs[@]}"; do - req="${d%/}" - [ ! -d "${req}" ] && missing_dirs+=("${req}/") - done - - while IFS= read -r d; do - allowed=false - for a in "${allowed_dirs[@]}"; do - a_norm="${a%/}" - [ "${d%/}" = "${a_norm}" ] && allowed=true - done - [ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/") - done < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##') - - { - printf '%s\n' '### Scripts governance' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' '| Area | Status | Notes |' - printf '%s\n' '|---|---|---|' - - if [ "${#missing_dirs[@]}" -gt 0 ]; then - printf '%s\n' '| Required directories | Warning | Missing required subfolders |' - else - printf '%s\n' '| Required directories | OK | All required subfolders present |' - fi - - if [ "${#unapproved_dirs[@]}" -gt 0 ]; then - printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |' - else - printf '%s\n' '| Directory policy | OK | No unapproved directories |' - fi - - printf '%s\n' '| Enforcement mode | Advisory | scripts folder is optional |' - printf '\n' - - if [ "${#missing_dirs[@]}" -gt 0 ]; then - printf '%s\n' 'Missing required script directories:' - for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done - printf '\n' - else - printf '%s\n' 'Missing required script directories: none.' - printf '\n' - fi - - if [ "${#unapproved_dirs[@]}" -gt 0 ]; then - printf '%s\n' 'Unapproved script directories detected:' - for m in "${unapproved_dirs[@]}"; do printf '%s\n' "- ${m}"; done - printf '\n' - else - printf '%s\n' 'Unapproved script directories detected: none.' - printf '\n' - fi - - printf '%s\n' 'Scripts governance completed in advisory mode.' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - - repo_health: - name: Repository health - needs: access_check - if: ${{ needs.access_check.outputs.allowed == 'true' }} - runs-on: ubuntu-latest - timeout-minutes: 20 - permissions: - contents: read - - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - fetch-depth: 0 - - - name: Repository health checks - env: - PROFILE_RAW: ${{ github.event.inputs.profile }} - run: | - set -euo pipefail - - profile="${PROFILE_RAW:-all}" - case "${profile}" in - all|scripts|repo) ;; - *) - printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" - exit 1 - ;; - esac - - if [ "${profile}" = 'scripts' ]; then - { - printf '%s\n' '### Repository health' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' 'Status: SKIPPED' - printf '%s\n' 'Reason: profile excludes repository health' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - exit 0 - fi - - IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}" - IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}" - if [ -n "${REPO_DISALLOWED_DIRS:-}" ]; then IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"; else disallowed_dirs=(); fi - IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES:-}" - - missing_required=() - missing_optional=() - - # Source directory: src/ or htdocs/ (either is valid for extension repos) - SOURCE_DIR="" - if [ -d "src" ]; then - SOURCE_DIR="src" - elif [ -d "htdocs" ]; then - SOURCE_DIR="htdocs" - elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then - # Platform/tooling repos don't need src/ - SOURCE_DIR="" - else - missing_required+=("src/ or htdocs/ (source directory required)") - fi - - for item in "${required_artifacts[@]}"; do - if printf '%s' "${item}" | grep -q '/$'; then - d="${item%/}" - [ ! -d "${d}" ] && missing_required+=("${item}") - else - [ ! -f "${item}" ] && missing_required+=("${item}") - fi - done - - for f in "${optional_files[@]}"; do - if printf '%s' "${f}" | grep -q '/$'; then - d="${f%/}" - [ ! -d "${d}" ] && missing_optional+=("${f}") - else - [ ! -f "${f}" ] && missing_optional+=("${f}") - fi - done - - for d in "${disallowed_dirs[@]}"; do - d_norm="${d%/}" - [ -d "${d_norm}" ] && missing_required+=("${d_norm}/ (disallowed)") - done - - for f in "${disallowed_files[@]}"; do - [ -f "${f}" ] && missing_required+=("${f} (disallowed)") - done - - git fetch origin --prune - - dev_paths=() - dev_branches=() - - while IFS= read -r b; do - name="${b#origin/}" - if [ "${name}" = 'dev' ]; then - dev_branches+=("${name}") - else - dev_paths+=("${name}") - fi - done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//') - - if [ "${#dev_paths[@]}" -eq 0 ] && [ "${#dev_branches[@]}" -eq 0 ]; then - missing_required+=("dev or dev/* branch") - fi - - content_warnings=() - - if [ -f 'CHANGELOG.md' ] && ! grep -Eq '^# Changelog' CHANGELOG.md; then - content_warnings+=("CHANGELOG.md missing '# Changelog' header") - fi - - if [ -f 'CHANGELOG.md' ] && grep -Eq '^[# ]*Unreleased' CHANGELOG.md; then - content_warnings+=("CHANGELOG.md contains Unreleased section (review release readiness)") - fi - - if [ -f 'LICENSE' ] && ! grep -qiE 'GNU GENERAL PUBLIC LICENSE|GPL' LICENSE; then - content_warnings+=("LICENSE does not look like a GPL text") - fi - - if [ -f 'README.md' ] && ! grep -qiE 'moko|Moko' README.md; then - content_warnings+=("README.md missing expected brand keyword") - fi - - export PROFILE_RAW="${profile}" - export MISSING_REQUIRED="$(printf '%s\n' "${missing_required[@]:-}")" - export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")" - export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")" - - report_json=$(printf '{"profile":"%s","missing_required":%d,"missing_optional":%d,"content_warnings":%d}' "$profile" "${#missing_required[@]}" "${#missing_optional[@]}" "${#content_warnings[@]}") - - { - printf '%s\n' '### Repository health' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' '| Metric | Value |' - printf '%s\n' '|---|---|' - printf '%s\n' "| Missing required | ${#missing_required[@]} |" - printf '%s\n' "| Missing optional | ${#missing_optional[@]} |" - printf '%s\n' "| Content warnings | ${#content_warnings[@]} |" - printf '\n' - - printf '%s\n' '### Guardrails report (JSON)' - printf '%s\n' '```json' - printf '%s\n' "${report_json}" - printf '%s\n' '```' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - - if [ "${#missing_required[@]}" -gt 0 ]; then - { - printf '%s\n' '### Missing required repo artifacts' - for m in "${missing_required[@]}"; do printf '%s\n' "- ${m}"; done - printf '%s\n' 'ERROR: Guardrails failed. Missing required repository artifacts.' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - exit 1 - fi - - if [ "${#missing_optional[@]}" -gt 0 ]; then - { - printf '%s\n' '### Missing optional repo artifacts' - for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - if [ "${#content_warnings[@]}" -gt 0 ]; then - { - printf '%s\n' '### Repo content warnings' - for m in "${content_warnings[@]}"; do printf '%s\n' "- ${m}"; done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - # -- Joomla-specific checks -- - joomla_findings=() - - MANIFEST="$(find . -maxdepth 2 -name '*.xml' -exec grep -l '/dev/null | head -1 || true)" - if [ -z "${MANIFEST}" ]; then - joomla_findings+=("Joomla XML manifest not found (no *.xml with tag)") - else - if ! grep -qP '' "${MANIFEST}"; then - joomla_findings+=("XML manifest: tag missing") - fi - if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then - joomla_findings+=("XML manifest: type attribute missing or invalid") - fi - if ! grep -qP '' "${MANIFEST}"; then - joomla_findings+=("XML manifest: tag missing") - fi - if ! grep -qP '' "${MANIFEST}"; then - joomla_findings+=("XML manifest: tag missing") - fi - if ! grep -qP ' missing (required for Joomla 5+)") - fi - fi - - INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)" - if [ "${INI_COUNT}" -eq 0 ]; then - joomla_findings+=("No .ini language files found") - fi - - if [ ! -f 'updates.xml' ]; then - joomla_findings+=("updates.xml missing in root (required for Joomla update server)") - fi - - if [ -n "${SOURCE_DIR}" ]; then - INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site") - for dir in "${INDEX_DIRS[@]}"; do - if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then - joomla_findings+=("${dir}/index.html missing (directory listing protection)") - fi - done - fi - - if [ "${#joomla_findings[@]}" -gt 0 ]; then - { - printf '%s\n' '### Joomla extension checks' - printf '%s\n' '| Check | Status |' - printf '%s\n' '|---|---|' - for f in "${joomla_findings[@]}"; do - printf '%s\n' "| ${f} | Warning |" - done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - else - { - printf '%s\n' '### Joomla extension checks' - printf '%s\n' 'All Joomla-specific checks passed.' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - extended_enabled="${EXTENDED_CHECKS:-true}" - extended_findings=() - - if [ "${extended_enabled}" = 'true' ]; then - if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then - : - else - extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)") - fi - - if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then - bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)" - if [ -n "${bad_refs}" ]; then - extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt") - { - printf '%s\n' '### Workflow pinning advisory' - printf '%s\n' 'Found uses: entries pinned to main/master:' - printf '%s\n' '```' - printf '%s\n' "${bad_refs}" - printf '%s\n' '```' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - fi - - if [ -f "${DOCS_INDEX}" ]; then - missing_links="" - while IFS= read -r docline; do - for link in $(echo "$docline" | grep -oE '\]\([^)]+\)' | sed 's/\](//' | sed 's/)$//' || true); do - case "$link" in http://*|https://*|"#"*|mailto:*) continue ;; esac - linkpath="${link%%#*}" - linkpath="${linkpath%%\?*}" - [ -z "$linkpath" ] && continue - if [ "${linkpath:0:1}" = "/" ]; then - testpath="${linkpath#/}" - else - testpath="$(dirname "${DOCS_INDEX}")/${linkpath}" - fi - [ ! -e "$testpath" ] && missing_links="${missing_links}${testpath} " - done - done < "${DOCS_INDEX}" - if [ -n "${missing_links}" ]; then - extended_findings+=("docs/docs-index.md contains broken relative links") - { - printf '%s\n' '### Docs index link integrity' - printf '%s\n' 'Broken relative links:' - for bl in ${missing_links}; do - printf '%s\n' "- ${bl}" - done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - fi - - if [ -d "${SCRIPT_DIR}" ]; then - if ! command -v shellcheck >/dev/null 2>&1; then - sudo apt-get update -qq - sudo apt-get install -y shellcheck >/dev/null - fi - - sc_out='' - while IFS= read -r shf; do - [ -z "${shf}" ] && continue - out_one="$(shellcheck -S warning -x "${shf}" 2>/dev/null || true)" - if [ -n "${out_one}" ]; then - sc_out="${sc_out}${out_one}\n" - fi - done < <(find "${SCRIPT_DIR}" -type f -name "${SHELLCHECK_PATTERN}" 2>/dev/null | sort) - - if [ -n "${sc_out}" ]; then - extended_findings+=("ShellCheck warnings detected (advisory)") - sc_head="$(printf '%s' "${sc_out}" | head -n 200)" - { - printf '%s\n' '### ShellCheck (advisory)' - printf '%s\n' '```' - printf '%s\n' "${sc_head}" - printf '%s\n' '```' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - fi - - spdx_missing=() - IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}" - spdx_args=() - for g in "${spdx_globs[@]}"; do spdx_args+=("${g}"); done - - while IFS= read -r f; do - [ -z "${f}" ] && continue - if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then - spdx_missing+=("${f}") - fi - done < <(git ls-files "${spdx_args[@]}" 2>/dev/null || true) - - if [ "${#spdx_missing[@]}" -gt 0 ]; then - extended_findings+=("SPDX header missing in some tracked files (advisory)") - { - printf '%s\n' '### SPDX header advisory' - printf '%s\n' 'Files missing SPDX-License-Identifier (first 40 lines scan):' - for f in "${spdx_missing[@]}"; do printf '%s\n' "- ${f}"; done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - stale_cutoff_days=180 - stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 > days*86400) print $1}' | head -50)" - if [ -n "${stale_branches}" ]; then - extended_findings+=("Stale remote branches detected (advisory)") - { - printf '%s\n' '### Git hygiene advisory' - printf '%s\n' "Branches with last commit older than ${stale_cutoff_days} days (sample up to 50):" - while IFS= read -r b; do [ -n "${b}" ] && printf '%s\n' "- ${b}"; done <<< "${stale_branches}" - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - fi - - { - printf '%s\n' '### Guardrails coverage matrix' - printf '%s\n' '| Domain | Status | Notes |' - printf '%s\n' '|---|---|---|' - printf '%s\n' '| Access control | OK | Admin-only execution gate |' - printf '%s\n' '| Release policy | N/A | Releases handled by MokoGitea |' - printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |' - printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |' - printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |' - if [ "${extended_enabled}" = 'true' ]; then - if [ "${#extended_findings[@]}" -gt 0 ]; then - printf '%s\n' '| Extended checks | Warning | See extended findings below |' - else - printf '%s\n' '| Extended checks | OK | No findings |' - fi - else - printf '%s\n' '| Extended checks | SKIPPED | EXTENDED_CHECKS disabled |' - fi - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - - if [ "${extended_enabled}" = 'true' ] && [ "${#extended_findings[@]}" -gt 0 ]; then - { - printf '%s\n' '### Extended findings (advisory)' - for f in "${extended_findings[@]}"; do printf '%s\n' "- ${f}"; done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}" - - - site-health: - name: Site Health - runs-on: ubuntu-latest - if: github.event_name == 'workflow_dispatch' - steps: - - uses: actions/checkout@v4 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.3' - - - name: Uptime check - if: env.URLS != '' - run: | - echo "$URLS" > /tmp/urls.txt - php monitoring/uptime-probe.php --urls /tmp/urls.txt --timeout 15 || echo "::warning::Some sites are down" - rm -f /tmp/urls.txt - env: - URLS: ${{ vars.MONITORED_URLS }} - - - name: SSL certificate check - if: env.DOMAINS != '' - run: | - echo "$DOMAINS" > /tmp/domains.txt - php monitoring/ssl-check.php --domains /tmp/domains.txt --warn-days 30 || echo "::warning::SSL certificates expiring soon" - rm -f /tmp/domains.txt - env: - DOMAINS: ${{ vars.MONITORED_DOMAINS }} - - - name: Summary - if: always() - run: | - echo "### Site Health" >> $GITHUB_STEP_SUMMARY - echo "Uptime and SSL checks completed." >> $GITHUB_STEP_SUMMARY - - # ═══════════════════════════════════════════════════════════════════════ - # Issue Reporter — file issues for failed gates - # ═══════════════════════════════════════════════════════════════════════ - report-issues: - name: "Report Issues" - runs-on: ubuntu-latest - needs: [access_check, scripts_governance, repo_health] - if: >- - always() && - (needs.scripts_governance.result == 'failure' || - needs.repo_health.result == 'failure') - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - sparse-checkout: automation/ci-issue-reporter.sh - sparse-checkout-cone-mode: false - - - name: "File issues for failed gates" - env: - GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - run: | - chmod +x automation/ci-issue-reporter.sh - REPORTER="./automation/ci-issue-reporter.sh" - WF="Repo Health" - - report_gate() { - local gate="$1" result="$2" details="$3" - if [ "$result" = "failure" ]; then - "$REPORTER" --gate "$gate" --details "$details" --workflow "$WF" --severity error - fi - } - - report_gate "Scripts Governance" \ - "${{ needs.scripts_governance.result }}" \ - "Scripts directory policy violations detected. Review required and allowed directories." - - report_gate "Repository Health" \ - "${{ needs.repo_health.result }}" \ - "Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary." +# ============================================================================ +# Copyright (C) 2025 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: mokocli.Validation +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli +# PATH: /templates/workflows/joomla/repo_health.yml.template +# VERSION: 09.23.00 +# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts. +# ============================================================================ + +name: "Generic: Repo Health" + +defaults: + run: + shell: bash + +on: + workflow_dispatch: + inputs: + profile: + description: 'Validation profile: all, scripts, or repo' + required: true + default: all + type: choice + options: + - all + - scripts + - repo + pull_request: + branches: + - main + +permissions: + contents: read + +env: + # Scripts governance policy + SCRIPTS_REQUIRED_DIRS: + SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate + + # Repo health policy + REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.mokogitea/workflows/ + REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/ + REPO_DISALLOWED_DIRS: + REPO_DISALLOWED_FILES: TODO.md,todo.md + + # Extended checks toggles + EXTENDED_CHECKS: "true" + + # File / directory variables + DOCS_INDEX: docs/docs-index.md + SCRIPT_DIR: scripts + WORKFLOWS_DIR: .mokogitea/workflows + SHELLCHECK_PATTERN: '*.sh' + SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml' + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + access_check: + name: Access control + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + + outputs: + allowed: ${{ steps.perm.outputs.allowed }} + permission: ${{ steps.perm.outputs.permission }} + + steps: + - name: Check actor permission (admin only) + id: perm + env: + TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }} + REPO: ${{ github.repository }} + ACTOR: ${{ github.actor }} + run: | + set -euo pipefail + ALLOWED=false + PERMISSION=unknown + METHOD="" + + # Hardcoded authorized users — always allowed + case "$ACTOR" in + jmiller|gitea-actions[bot]) + ALLOWED=true + PERMISSION=admin + METHOD="hardcoded allowlist" + ;; + *) + # Detect platform and check permissions via API + API_BASE="${GITHUB_API_URL:-${GITEA_API_URL:-https://api.github.com}}" + RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}') + PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown") + if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "maintain" ] || [ "$PERMISSION" = "owner" ]; then + ALLOWED=true + fi + METHOD="collaborator API" + ;; + esac + + echo "permission=${PERMISSION}" >> "$GITHUB_OUTPUT" + echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT" + + { + echo "## Access Authorization" + echo "" + echo "| Field | Value |" + echo "|-------|-------|" + echo "| **Actor** | \`${ACTOR}\` |" + echo "| **Repository** | \`${REPO}\` |" + echo "| **Permission** | \`${PERMISSION}\` |" + echo "| **Method** | ${METHOD} |" + echo "| **Authorized** | ${ALLOWED} |" + echo "" + if [ "$ALLOWED" = "true" ]; then + echo "${ACTOR} authorized (${METHOD})" + else + echo "${ACTOR} is NOT authorized. Requires admin or maintain role." + fi + } >> "${GITHUB_STEP_SUMMARY}" + + - name: Deny execution when not permitted + if: ${{ steps.perm.outputs.allowed != 'true' }} + run: | + set -euo pipefail + printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}" + exit 1 + + scripts_governance: + name: Scripts governance + needs: access_check + if: ${{ needs.access_check.outputs.allowed == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + + - name: Scripts folder checks + env: + PROFILE_RAW: ${{ github.event.inputs.profile }} + run: | + set -euo pipefail + + profile="${PROFILE_RAW:-all}" + case "${profile}" in + all|scripts|repo) ;; + *) + printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" + exit 1 + ;; + esac + + if [ "${profile}" = 'repo' ]; then + { + printf '%s\n' '### Scripts governance' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' 'Status: SKIPPED' + printf '%s\n' 'Reason: profile excludes scripts governance' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 0 + fi + + if [ ! -d "${SCRIPT_DIR}" ]; then + { + printf '%s\n' '### Scripts governance' + printf '%s\n' 'Status: OK (advisory)' + printf '%s\n' 'scripts/ directory not present. No scripts governance enforced.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 0 + fi + + if [ -n "${SCRIPTS_REQUIRED_DIRS:-}" ]; then IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"; else required_dirs=(); fi + IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}" + + missing_dirs=() + unapproved_dirs=() + + for d in "${required_dirs[@]}"; do + req="${d%/}" + [ ! -d "${req}" ] && missing_dirs+=("${req}/") + done + + while IFS= read -r d; do + allowed=false + for a in "${allowed_dirs[@]}"; do + a_norm="${a%/}" + [ "${d%/}" = "${a_norm}" ] && allowed=true + done + [ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/") + done < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##') + + { + printf '%s\n' '### Scripts governance' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' '| Area | Status | Notes |' + printf '%s\n' '|---|---|---|' + + if [ "${#missing_dirs[@]}" -gt 0 ]; then + printf '%s\n' '| Required directories | Warning | Missing required subfolders |' + else + printf '%s\n' '| Required directories | OK | All required subfolders present |' + fi + + if [ "${#unapproved_dirs[@]}" -gt 0 ]; then + printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |' + else + printf '%s\n' '| Directory policy | OK | No unapproved directories |' + fi + + printf '%s\n' '| Enforcement mode | Advisory | scripts folder is optional |' + printf '\n' + + if [ "${#missing_dirs[@]}" -gt 0 ]; then + printf '%s\n' 'Missing required script directories:' + for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + else + printf '%s\n' 'Missing required script directories: none.' + printf '\n' + fi + + if [ "${#unapproved_dirs[@]}" -gt 0 ]; then + printf '%s\n' 'Unapproved script directories detected:' + for m in "${unapproved_dirs[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + else + printf '%s\n' 'Unapproved script directories detected: none.' + printf '\n' + fi + + printf '%s\n' 'Scripts governance completed in advisory mode.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + repo_health: + name: Repository health + needs: access_check + if: ${{ needs.access_check.outputs.allowed == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + + - name: Repository health checks + env: + PROFILE_RAW: ${{ github.event.inputs.profile }} + run: | + set -euo pipefail + + profile="${PROFILE_RAW:-all}" + case "${profile}" in + all|scripts|repo) ;; + *) + printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" + exit 1 + ;; + esac + + if [ "${profile}" = 'scripts' ]; then + { + printf '%s\n' '### Repository health' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' 'Status: SKIPPED' + printf '%s\n' 'Reason: profile excludes repository health' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 0 + fi + + IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}" + IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}" + if [ -n "${REPO_DISALLOWED_DIRS:-}" ]; then IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"; else disallowed_dirs=(); fi + IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES:-}" + + missing_required=() + missing_optional=() + + # Source directory: src/ or htdocs/ (either is valid for extension repos) + SOURCE_DIR="" + if [ -d "src" ]; then + SOURCE_DIR="src" + elif [ -d "htdocs" ]; then + SOURCE_DIR="htdocs" + elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then + # Platform/tooling repos don't need src/ + SOURCE_DIR="" + else + missing_required+=("src/ or htdocs/ (source directory required)") + fi + + for item in "${required_artifacts[@]}"; do + if printf '%s' "${item}" | grep -q '/$'; then + d="${item%/}" + [ ! -d "${d}" ] && missing_required+=("${item}") + else + [ ! -f "${item}" ] && missing_required+=("${item}") + fi + done + + for f in "${optional_files[@]}"; do + if printf '%s' "${f}" | grep -q '/$'; then + d="${f%/}" + [ ! -d "${d}" ] && missing_optional+=("${f}") + else + [ ! -f "${f}" ] && missing_optional+=("${f}") + fi + done + + for d in "${disallowed_dirs[@]}"; do + d_norm="${d%/}" + [ -d "${d_norm}" ] && missing_required+=("${d_norm}/ (disallowed)") + done + + for f in "${disallowed_files[@]}"; do + [ -f "${f}" ] && missing_required+=("${f} (disallowed)") + done + + git fetch origin --prune + + dev_paths=() + dev_branches=() + + while IFS= read -r b; do + name="${b#origin/}" + if [ "${name}" = 'dev' ]; then + dev_branches+=("${name}") + else + dev_paths+=("${name}") + fi + done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//') + + if [ "${#dev_paths[@]}" -eq 0 ] && [ "${#dev_branches[@]}" -eq 0 ]; then + missing_required+=("dev or dev/* branch") + fi + + content_warnings=() + + if [ -f 'CHANGELOG.md' ] && ! grep -Eq '^# Changelog' CHANGELOG.md; then + content_warnings+=("CHANGELOG.md missing '# Changelog' header") + fi + + if [ -f 'CHANGELOG.md' ] && grep -Eq '^[# ]*Unreleased' CHANGELOG.md; then + content_warnings+=("CHANGELOG.md contains Unreleased section (review release readiness)") + fi + + if [ -f 'LICENSE' ] && ! grep -qiE 'GNU GENERAL PUBLIC LICENSE|GPL' LICENSE; then + content_warnings+=("LICENSE does not look like a GPL text") + fi + + if [ -f 'README.md' ] && ! grep -qiE 'moko|Moko' README.md; then + content_warnings+=("README.md missing expected brand keyword") + fi + + export PROFILE_RAW="${profile}" + export MISSING_REQUIRED="$(printf '%s\n' "${missing_required[@]:-}")" + export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")" + export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")" + + report_json=$(printf '{"profile":"%s","missing_required":%d,"missing_optional":%d,"content_warnings":%d}' "$profile" "${#missing_required[@]}" "${#missing_optional[@]}" "${#content_warnings[@]}") + + { + printf '%s\n' '### Repository health' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' '| Metric | Value |' + printf '%s\n' '|---|---|' + printf '%s\n' "| Missing required | ${#missing_required[@]} |" + printf '%s\n' "| Missing optional | ${#missing_optional[@]} |" + printf '%s\n' "| Content warnings | ${#content_warnings[@]} |" + printf '\n' + + printf '%s\n' '### Guardrails report (JSON)' + printf '%s\n' '```json' + printf '%s\n' "${report_json}" + printf '%s\n' '```' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + if [ "${#missing_required[@]}" -gt 0 ]; then + { + printf '%s\n' '### Missing required repo artifacts' + for m in "${missing_required[@]}"; do printf '%s\n' "- ${m}"; done + printf '%s\n' 'ERROR: Guardrails failed. Missing required repository artifacts.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 1 + fi + + if [ "${#missing_optional[@]}" -gt 0 ]; then + { + printf '%s\n' '### Missing optional repo artifacts' + for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + if [ "${#content_warnings[@]}" -gt 0 ]; then + { + printf '%s\n' '### Repo content warnings' + for m in "${content_warnings[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + # -- Joomla-specific checks -- + joomla_findings=() + + MANIFEST="$(find . -maxdepth 2 -name '*.xml' -exec grep -l '/dev/null | head -1 || true)" + if [ -z "${MANIFEST}" ]; then + joomla_findings+=("Joomla XML manifest not found (no *.xml with tag)") + else + if ! grep -qP '' "${MANIFEST}"; then + joomla_findings+=("XML manifest: tag missing") + fi + if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then + joomla_findings+=("XML manifest: type attribute missing or invalid") + fi + if ! grep -qP '' "${MANIFEST}"; then + joomla_findings+=("XML manifest: tag missing") + fi + if ! grep -qP '' "${MANIFEST}"; then + joomla_findings+=("XML manifest: tag missing") + fi + if ! grep -qP ' missing (required for Joomla 5+)") + fi + fi + + INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)" + if [ "${INI_COUNT}" -eq 0 ]; then + joomla_findings+=("No .ini language files found") + fi + + if [ ! -f 'updates.xml' ]; then + joomla_findings+=("updates.xml missing in root (required for Joomla update server)") + fi + + if [ -n "${SOURCE_DIR}" ]; then + INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site") + for dir in "${INDEX_DIRS[@]}"; do + if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then + joomla_findings+=("${dir}/index.html missing (directory listing protection)") + fi + done + fi + + if [ "${#joomla_findings[@]}" -gt 0 ]; then + { + printf '%s\n' '### Joomla extension checks' + printf '%s\n' '| Check | Status |' + printf '%s\n' '|---|---|' + for f in "${joomla_findings[@]}"; do + printf '%s\n' "| ${f} | Warning |" + done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + else + { + printf '%s\n' '### Joomla extension checks' + printf '%s\n' 'All Joomla-specific checks passed.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + extended_enabled="${EXTENDED_CHECKS:-true}" + extended_findings=() + + if [ "${extended_enabled}" = 'true' ]; then + if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then + : + else + extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)") + fi + + if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then + bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)" + if [ -n "${bad_refs}" ]; then + extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt") + { + printf '%s\n' '### Workflow pinning advisory' + printf '%s\n' 'Found uses: entries pinned to main/master:' + printf '%s\n' '```' + printf '%s\n' "${bad_refs}" + printf '%s\n' '```' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + if [ -f "${DOCS_INDEX}" ]; then + missing_links="" + while IFS= read -r docline; do + for link in $(echo "$docline" | grep -oE '\]\([^)]+\)' | sed 's/\](//' | sed 's/)$//' || true); do + case "$link" in http://*|https://*|"#"*|mailto:*) continue ;; esac + linkpath="${link%%#*}" + linkpath="${linkpath%%\?*}" + [ -z "$linkpath" ] && continue + if [ "${linkpath:0:1}" = "/" ]; then + testpath="${linkpath#/}" + else + testpath="$(dirname "${DOCS_INDEX}")/${linkpath}" + fi + [ ! -e "$testpath" ] && missing_links="${missing_links}${testpath} " + done + done < "${DOCS_INDEX}" + if [ -n "${missing_links}" ]; then + extended_findings+=("docs/docs-index.md contains broken relative links") + { + printf '%s\n' '### Docs index link integrity' + printf '%s\n' 'Broken relative links:' + for bl in ${missing_links}; do + printf '%s\n' "- ${bl}" + done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + if [ -d "${SCRIPT_DIR}" ]; then + if ! command -v shellcheck >/dev/null 2>&1; then + sudo apt-get update -qq + sudo apt-get install -y shellcheck >/dev/null + fi + + sc_out='' + while IFS= read -r shf; do + [ -z "${shf}" ] && continue + out_one="$(shellcheck -S warning -x "${shf}" 2>/dev/null || true)" + if [ -n "${out_one}" ]; then + sc_out="${sc_out}${out_one}\n" + fi + done < <(find "${SCRIPT_DIR}" -type f -name "${SHELLCHECK_PATTERN}" 2>/dev/null | sort) + + if [ -n "${sc_out}" ]; then + extended_findings+=("ShellCheck warnings detected (advisory)") + sc_head="$(printf '%s' "${sc_out}" | head -n 200)" + { + printf '%s\n' '### ShellCheck (advisory)' + printf '%s\n' '```' + printf '%s\n' "${sc_head}" + printf '%s\n' '```' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + spdx_missing=() + IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}" + spdx_args=() + for g in "${spdx_globs[@]}"; do spdx_args+=("${g}"); done + + while IFS= read -r f; do + [ -z "${f}" ] && continue + if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then + spdx_missing+=("${f}") + fi + done < <(git ls-files "${spdx_args[@]}" 2>/dev/null || true) + + if [ "${#spdx_missing[@]}" -gt 0 ]; then + extended_findings+=("SPDX header missing in some tracked files (advisory)") + { + printf '%s\n' '### SPDX header advisory' + printf '%s\n' 'Files missing SPDX-License-Identifier (first 40 lines scan):' + for f in "${spdx_missing[@]}"; do printf '%s\n' "- ${f}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + stale_cutoff_days=180 + stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 > days*86400) print $1}' | head -50)" + if [ -n "${stale_branches}" ]; then + extended_findings+=("Stale remote branches detected (advisory)") + { + printf '%s\n' '### Git hygiene advisory' + printf '%s\n' "Branches with last commit older than ${stale_cutoff_days} days (sample up to 50):" + while IFS= read -r b; do [ -n "${b}" ] && printf '%s\n' "- ${b}"; done <<< "${stale_branches}" + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + { + printf '%s\n' '### Guardrails coverage matrix' + printf '%s\n' '| Domain | Status | Notes |' + printf '%s\n' '|---|---|---|' + printf '%s\n' '| Access control | OK | Admin-only execution gate |' + printf '%s\n' '| Release policy | N/A | Releases handled by MokoGitea |' + printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |' + printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |' + printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |' + if [ "${extended_enabled}" = 'true' ]; then + if [ "${#extended_findings[@]}" -gt 0 ]; then + printf '%s\n' '| Extended checks | Warning | See extended findings below |' + else + printf '%s\n' '| Extended checks | OK | No findings |' + fi + else + printf '%s\n' '| Extended checks | SKIPPED | EXTENDED_CHECKS disabled |' + fi + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + if [ "${extended_enabled}" = 'true' ] && [ "${#extended_findings[@]}" -gt 0 ]; then + { + printf '%s\n' '### Extended findings (advisory)' + for f in "${extended_findings[@]}"; do printf '%s\n' "- ${f}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}" + + + site-health: + name: Site Health + runs-on: ubuntu-latest + if: github.event_name == 'workflow_dispatch' + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + + - name: Uptime check + if: env.URLS != '' + run: | + echo "$URLS" > /tmp/urls.txt + php monitoring/uptime-probe.php --urls /tmp/urls.txt --timeout 15 || echo "::warning::Some sites are down" + rm -f /tmp/urls.txt + env: + URLS: ${{ vars.MONITORED_URLS }} + + - name: SSL certificate check + if: env.DOMAINS != '' + run: | + echo "$DOMAINS" > /tmp/domains.txt + php monitoring/ssl-check.php --domains /tmp/domains.txt --warn-days 30 || echo "::warning::SSL certificates expiring soon" + rm -f /tmp/domains.txt + env: + DOMAINS: ${{ vars.MONITORED_DOMAINS }} + + - name: Summary + if: always() + run: | + echo "### Site Health" >> $GITHUB_STEP_SUMMARY + echo "Uptime and SSL checks completed." >> $GITHUB_STEP_SUMMARY + + # ═══════════════════════════════════════════════════════════════════════ + # Issue Reporter — file issues for failed gates + # ═══════════════════════════════════════════════════════════════════════ + report-scripts: + name: "Report: Scripts Governance" + needs: [access_check, scripts_governance] + if: >- + always() && + needs.scripts_governance.result == 'failure' + uses: ./.mokogitea/workflows/ci-issue-reporter.yml + with: + gate: "Scripts Governance" + workflow: "Repo Health" + severity: error + details: "Scripts directory policy violations detected. Review required and allowed directories." + secrets: inherit + + report-health: + name: "Report: Repository Health" + needs: [access_check, repo_health] + if: >- + always() && + needs.repo_health.result == 'failure' + uses: ./.mokogitea/workflows/ci-issue-reporter.yml + with: + gate: "Repository Health" + workflow: "Repo Health" + severity: error + details: "Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary." + secrets: inherit diff --git a/.mokogitea/workflows/version-set.yml b/.mokogitea/workflows/version-set.yml index de6c5f4f..0bedeaae 100644 --- a/.mokogitea/workflows/version-set.yml +++ b/.mokogitea/workflows/version-set.yml @@ -48,7 +48,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - token: ${{ secrets.MOKOGITEA_TOKEN || secrets.GA_TOKEN || github.token }} + token: ${{ secrets.MOKOGITEA_TOKEN || github.token }} ref: ${{ inputs.branch || github.ref }} fetch-depth: 1 diff --git a/.mokogitea/workflows/workflow-sync-trigger.yml b/.mokogitea/workflows/workflow-sync-trigger.yml index 371910c4..34891e87 100644 --- a/.mokogitea/workflows/workflow-sync-trigger.yml +++ b/.mokogitea/workflows/workflow-sync-trigger.yml @@ -13,6 +13,7 @@ name: "Universal: Workflow Sync Trigger" on: + workflow_dispatch: pull_request: types: [closed] branches: @@ -26,8 +27,9 @@ jobs: 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]') + github.event_name == 'workflow_dispatch' || + (github.event.pull_request.merged == true && + !contains(github.event.pull_request.title, '[skip sync]')) steps: - name: Determine platform from repo name @@ -49,8 +51,14 @@ jobs: 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 + MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}" + git clone --depth 1 "${MOKOGITEA_URL}/MokoConsulting/mokocli.git" /tmp/mokocli + + - name: Install PHP + run: | + if ! command -v php &> /dev/null; then + apt-get update -qq && apt-get install -y -qq php-cli php-json php-curl > /dev/null 2>&1 + fi - name: Install dependencies run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 28551341..51ed2a44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog ## [Unreleased] +## [01.12.00] --- 2026-06-28 + +## [01.12.00] --- 2026-06-28 + ### Added - **Visual post calendar**: Monthly calendar grid view showing scheduled, queued, and posted cross-posts with status badges (#160) - **Calendar navigation**: Month-by-month navigation with today highlighting (#160) @@ -94,24 +98,3 @@ - **Bluesky**: Replaced md5() with hash('sha256', ...) for cache key - **ServiceController**: Exception details no longer exposed to client - **License warning**: Removed duplicate from system plugin -- install script already shows it with direct edit link - -## [01.04.01] --- 2026-06-21 - - -## [01.04.01] --- 2026-06-21 - - -## [01.04.00] --- 2026-06-21 - -### Fixed -- **Package manifest**: Added missing `plg_system_mokosuitecross_events` and `plg_system_mokosuitecross_gallery` to `pkg_mokosuitecross.xml` — these system plugins were not installed with the package -- **Cleanup**: Removed old `src/` directory (pre-rename cruft with `mokojoomcross` files) - -## [01.03.00] --- 2026-06-21 - - - - -All notable changes to MokoSuiteCross will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index b7f08286..193d7ad5 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -14,7 +14,7 @@ DEFGROUP: Template-Joomla INGROUP: Template-Joomla.Documentation REPO: https://github.com/mokoconsulting-tech/Template-Joomla/ - VERSION: 01.11.03 + VERSION: 01.12.00 PATH: ./CODE_OF_CONDUCT.md BRIEF: Community expectations and enforcement guidelines NOTE: Adapted with attribution from the Contributor Covenant v2.1 diff --git a/GOVERNANCE.md b/GOVERNANCE.md index bad8ed05..eb4252c6 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -1 +1,119 @@ -PCEtLQogQ29weXJpZ2h0IChDKSAyMDI2IE1va28gQ29uc3VsdGluZyA8aGVsbG9AbW9rb2NvbnN1bHRpbmcudGVjaD4KCiBUaGlzIGZpbGUgaXMgcGFydCBvZiBhIE1va28gQ29uc3VsdGluZyBwcm9qZWN0LgoKIFNQRFgtTGljZW5zZS1JZGVudGlmaWVyOiBHUEwtMy4wLW9yLWxhdGVyCgogVGhpcyBwcm9ncmFtIGlzIGZyZWUgc29mdHdhcmU7IHlvdSBjYW4gcmVkaXN0cmlidXRlIGl0IGFuZC9vciBtb2RpZnkgaXQgdW5kZXIgdGhlIHRlcm1zIG9mCiB0aGUgR05VIEdlbmVyYWwgUHVibGljIExpY2Vuc2UgYXMgcHVibGlzaGVkIGJ5IHRoZSBGcmVlIFNvZnR3YXJlIEZvdW5kYXRpb247IGVpdGhlciB2ZXJzaW9uIDMKIG9mIHRoZSBMaWNlbnNlLCBvciAoYXQgeW91ciBvcHRpb24pIGFueSBsYXRlciB2ZXJzaW9uLgoKIFRoaXMgcHJvZ3JhbSBpcyBkaXN0cmlidXRlZCBpbiB0aGUgaG9wZSB0aGF0IGl0IHdpbGwgYmUgdXNlZnVsLCBidXQgV0lUSE9VVCBBTlkgV0FSUkFOVFk7CiB3aXRob3V0IGV2ZW4gdGhlIGltcGxpZWQgd2FycmFudHkgb2YgTUVSQ0hBTlRBQklMSVRZIG9yIEZJVE5FU1MgRk9SIEEgUEFSVElDVUxBUiBQVVJQT1NFLgogU2VlIHRoZSBHTlUgR2VuZXJhbCBQdWJsaWMgTGljZW5zZSBmb3IgbW9yZSBkZXRhaWxzLgoKIFlvdSBzaG91bGQgaGF2ZSByZWNlaXZlZCBhIGNvcHkgb2YgdGhlIEdOVSBHZW5lcmFsIFB1YmxpYyBMaWNlbnNlICguL0xJQ0VOU0UpLgoKIEZJTEUgSU5GT1JNQVRJT04KIERFRkdST1VQOiBtb2tvY29uc3VsdGluZy10ZWNoLlRlbXBsYXRlLUpvb21sYQogSU5HUk9VUDogTW9rb1N0YW5kYXJkcy5Hb3Zlcm5hbmNlCiBSRVBPOiBodHRwczovL2dpdGh1Yi5jb20vbW9rb2NvbnN1bHRpbmctdGVjaC9UZW1wbGF0ZS1Kb29tbGEKIFZFUlNJT046IDAxLjAxLjAwCiBQQVRIOiAvR09WRVJOQU5DRS5tZAogQlJJRUY6IFByb2plY3QgZ292ZXJuYW5jZSBydWxlcywgcm9sZXMsIGFuZCBkZWNpc2lvbiBwcm9jZXNzIGZvciBUZW1wbGF0ZS1Kb29tbGEKLS0+CgpbIVtNb2tvU3RhbmRhcmRzXShodHRwczovL2ltZy5zaGllbGRzLmlvL2JhZGdlL01va29TdGFuZGFyZHMtMDQuMDAuMDQtYmx1ZSldKGh0dHBzOi8vZ2l0aHViLmNvbS9tb2tvY29uc3VsdGluZy10ZWNoL01va29TdGFuZGFyZHMpCgojIFByb2plY3QgR292ZXJuYW5jZQoKIyMgT3ZlcnZpZXcKClRoaXMgZG9jdW1lbnQgZGVmaW5lcyB0aGUgZ292ZXJuYW5jZSBtb2RlbCBmb3IgdGhlIGBUZW1wbGF0ZS1Kb29tbGFgIHJlcG9zaXRvcnkgd2l0aGluIHRoZQpgbW9rb2NvbnN1bHRpbmctdGVjaGAgb3JnYW5pemF0aW9uLiBJdCBpcyBhdXRvbWF0aWNhbGx5IG1haW50YWluZWQgYnkKW01va29TdGFuZGFyZHNdKGh0dHBzOi8vZ2l0aHViLmNvbS9tb2tvY29uc3VsdGluZy10ZWNoL01va29TdGFuZGFyZHMpIHYwNC4wMC4wNC4KCkZ1bGwgZ292ZXJuYW5jZSBwb2xpY3kgaXMgZGVmaW5lZCBpbiB0aGUgTW9rb1N0YW5kYXJkcyBzb3VyY2UgcmVwb3NpdG9yeToKW2RvY3MvcG9saWN5L0dPVkVSTkFOQ0UubWRdKGh0dHBzOi8vZ2l0aHViLmNvbS9tb2tvY29uc3VsdGluZy10ZWNoL01va29TdGFuZGFyZHMvYmxvYi9tYWluL2RvY3MvcG9saWN5L0dPVkVSTkFOQ0UubWQpCgotLS0KCiMjIFJvbGVzIGFuZCBSZXNwb25zaWJpbGl0aWVzCgojIyMgTWFpbnRhaW5lcgoKKipHaXRIdWIqKjogQG1va29jb25zdWx0aW5nLXRlY2gKCioqQXV0aG9yaXR5Kio6IEZpbmFsIGRlY2lzaW9uLW1ha2luZyBhdXRob3JpdHkgb24gYWxsIG1hdHRlcnMgZm9yIHRoaXMgcmVwb3NpdG9yeS4KCioqUmVzcG9uc2liaWxpdGllcyoqOgotIFJldmlldyBhbmQgbWVyZ2UgcHVsbCByZXF1ZXN0cwotIE1haW50YWluIGNvZGUgcXVhbGl0eSBhbmQgc3RhbmRhcmRzIGNvbXBsaWFuY2UKLSBNYW5hZ2UgcmVsZWFzZXMgYW5kIHZlcnNpb25pbmcKLSBSZXNwb25kIHRvIGlzc3VlcyBhbmQgc2VjdXJpdHkgcmVwb3J0cwoKIyMjIENvbnRyaWJ1dG9ycwoKKipBdXRob3JpdHkqKjogU3VibWl0IGNoYW5nZXMgdmlhIHB1bGwgcmVxdWVzdHMuCgoqKlJlcXVpcmVtZW50cyoqOgotIFJlYWQgYW5kIGFjY2VwdCBgQ09ERV9PRl9DT05EVUNULm1kYAotIEZvbGxvdyBgQ09OVFJJQlVUSU5HLm1kYCBndWlkZWxpbmVzCgotLS0KCiMjIERlY2lzaW9uLU1ha2luZwoKQWxsIGNoYW5nZXMgbXVzdCBiZSBzdWJtaXR0ZWQgYXMgcHVsbCByZXF1ZXN0cy4gVGhlIG1haW50YWluZXIgKEBtb2tvY29uc3VsdGluZy10ZWNoKQpyZXZpZXdzIGFuZCBhcHByb3ZlcyBhbGwgY2hhbmdlcyBiZWZvcmUgdGhleSBhcmUgbWVyZ2VkLgoKIyMjIFNvbGUgT3BlcmF0b3IgUG9saWN5CgpUaGlzIG9yZ2FuaXphdGlvbiBvcGVyYXRlcyB1bmRlciBhICoqc29sZSBvcGVyYXRvcioqIG1vZGVsLiBUaGUgbWFpbnRhaW5lciAoQG1va29jb25zdWx0aW5nLXRlY2gpCmlzIHRoZSBzb2xlIGVtcGxveWVlIGFuZCBvd25lciBhbmQgbWF5IHNlbGYtYXBwcm92ZSBwdWxsIHJlcXVlc3RzIHdoZW4gbm8gc2Vjb25kIHJldmlld2VyIGlzCmF2YWlsYWJsZS4gVGhlIGZvbGxvd2luZyByZXF1aXJlbWVudHMgcmVtYWluIG1hbmRhdG9yeSByZWdhcmRsZXNzOgoKMS4gKipQdWxsIFJlcXVlc3RzIFJlcXVpcmVkKiog4oCUIGFsbCBjaGFuZ2VzIHRvIHByb3RlY3RlZCBicmFuY2hlcyBnbyB0aHJvdWdoIGEgUFIuCjIuICoqQXV0b21hdGVkIENoZWNrcyoqIOKAlCBhbGwgQ0kgY2hlY2tzIG11c3QgcGFzcyBiZWZvcmUgbWVyZ2luZy4KMy4gKipBdWRpdCBUcmFpbCoqIOKAlCBpc3N1ZXMsIHB1bGwgcmVxdWVzdHMsIGFuZCBjb21taXQgaGlzdG9yeSBhcmUgcHJlc2VydmVkLgo0LiAqKkRvY3VtZW50YXRpb24qKiDigJQgY2hhbmdlcyBhcmUgZG9jdW1lbnRlZCBpbiBgQ0hBTkdFTE9HLm1kYC4KClNlZSB0aGUgZnVsbCBwb2xpY3k6CltTb2xlIE9wZXJhdG9yIFBvbGljeV0oaHR0cHM6Ly9naXRodWIuY29tL21va29jb25zdWx0aW5nLXRlY2gvTW9rb1N0YW5kYXJkcy9ibG9iL21haW4vZG9jcy9wb2xpY3kvR09WRVJOQU5DRS5tZCNzb2xlLW9wZXJhdG9yLXBvbGljeSkKCi0tLQoKIyMgQ2hhbmdlIE1hbmFnZW1lbnQKCnwgQ2hhbmdlIFR5cGUgfCBBcHByb3ZhbCB8IFByb2Nlc3MgfAp8LS0tLS0tLS0tLS0tLXwtLS0tLS0tLS0tfC0tLS0tLS0tLXwKfCBSb3V0aW5lIChkb2NzLCBidWcgZml4ZXMpIHwgTWFpbnRhaW5lciB8IFBSIOKGkiBDSSBwYXNzIOKGkiBtZXJnZSB8CnwgU2lnbmlmaWNhbnQgKG5ldyBmZWF0dXJlcykgfCBNYWludGFpbmVyIHwgUFIgd2l0aCBkZXNjcmlwdGlvbiDihpIgQ0kgcGFzcyDihpIgbWVyZ2UgfAp8IE1ham9yIChicmVha2luZywgYXJjaGl0ZWN0dXJlKSB8IE1haW50YWluZXIgfCBJc3N1ZSBkaXNjdXNzaW9uIOKGkiBQUiDihpIgQ0kgcGFzcyDihpIgbWVyZ2UgfAp8IEVtZXJnZW5jeSAoc2VjdXJpdHkpIHwgTWFpbnRhaW5lciB8IExhYmVsbGVkIGBFTUVSR0VOQ1lgIOKGkiBpbW1lZGlhdGUgbWVyZ2Ug4oaSIHBvc3QtbW9ydGVtIHwKCi0tLQoKIyMgUmVwb3J0aW5nIElzc3VlcwoKLSAqKkJ1Z3MgLyBGZWF0dXJlcyoqOiBPcGVuIGEgW0dpdEh1YiBJc3N1ZV0oaHR0cHM6Ly9naXRodWIuY29tL21va29jb25zdWx0aW5nLXRlY2gvVGVtcGxhdGUtSm9vbWxhL2lzc3VlcykKLSAqKlNlY3VyaXR5IHZ1bG5lcmFiaWxpdGllcyoqOiBTZWUgW1NFQ1VSSVRZLm1kXSguL1NFQ1VSSVRZLm1kKQotICoqQ29kZSBvZiBDb25kdWN0Kio6IFNlZSBbQ09ERV9PRl9DT05EVUNULm1kXSguL0NPREVfT0ZfQ09ORFVDVC5tZCkKLSAqKkNvbnRhY3QqKjogZGV2QG1va29jb25zdWx0aW5nLnRlY2gKCi0tLQoKIyMgTWV0YWRhdGEKCnwgRmllbGQgICAgICAgICB8IFZhbHVlICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHwKfCAtLS0tLS0tLS0tLS0tIHwgLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0gfAp8IERvY3VtZW50IFR5cGUgfCBQb2xpY3kgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB8CnwgRG9tYWluICAgICAgICB8IEdvdmVybmFuY2UgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHwKfCBBcHBsaWVzIFRvICAgIHwgbW9rb2NvbnN1bHRpbmctdGVjaC9UZW1wbGF0ZS1Kb29tbGEgICAgICAgICAgICAgICAgICAgICAgICAgICB8CnwgSnVyaXNkaWN0aW9uICB8IFRlbm5lc3NlZSwgVVNBICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHwKfCBNYWludGFpbmVyICAgIHwgQG1va29jb25zdWx0aW5nLXRlY2ggICAgICAgICAgICAgICAgICAgICAgICAgICAgfAp8IFN0YW5kYXJkcyAgICAgfCBNb2tvU3RhbmRhcmRzIHYwNC4wMC4wNCAgICAgICAgICAgIHwKfCBSZXBvICAgICAgICAgIHwgaHR0cHM6Ly9naXRodWIuY29tL21va29jb25zdWx0aW5nLXRlY2gvVGVtcGxhdGUtSm9vbWxhICAgICAgICB8CnwgUGF0aCAgICAgICAgICB8IC9HT1ZFUk5BTkNFLm1kICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHwKfCBTdGF0dXMgICAgICAgIHwgQWN0aXZlIOKAlCBhdXRvLW1haW50YWluZWQgYnkgTW9rb1N0YW5kYXJkcyAgICAgICB8Cg== \ No newline at end of file + + +[![MokoStandards](https://img.shields.io/badge/MokoStandards-04.00.04-blue)](https://github.com/mokoconsulting-tech/MokoStandards) + +# Project Governance + +## Overview + +This document defines the governance model for the `Template-Joomla` repository within the +`mokoconsulting-tech` organization. It is automatically maintained by +[MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards) v04.00.04. + +Full governance policy is defined in the MokoStandards source repository: +[docs/policy/GOVERNANCE.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/GOVERNANCE.md) + +--- + +## Roles and Responsibilities + +### Maintainer + +**GitHub**: @mokoconsulting-tech + +**Authority**: Final decision-making authority on all matters for this repository. + +**Responsibilities**: +- Review and merge pull requests +- Maintain code quality and standards compliance +- Manage releases and versioning +- Respond to issues and security reports + +### Contributors + +**Authority**: Submit changes via pull requests. + +**Requirements**: +- Read and accept `CODE_OF_CONDUCT.md` +- Follow `CONTRIBUTING.md` guidelines + +--- + +## Decision-Making + +All changes must be submitted as pull requests. The maintainer (@mokoconsulting-tech) +reviews and approves all changes before they are merged. + +### Sole Operator Policy + +This organization operates under a **sole operator** model. The maintainer (@mokoconsulting-tech) +is the sole employee and owner and may self-approve pull requests when no second reviewer is +available. The following requirements remain mandatory regardless: + +1. **Pull Requests Required** — all changes to protected branches go through a PR. +2. **Automated Checks** — all CI checks must pass before merging. +3. **Audit Trail** — issues, pull requests, and commit history are preserved. +4. **Documentation** — changes are documented in `CHANGELOG.md`. + +See the full policy: +[Sole Operator Policy](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/GOVERNANCE.md#sole-operator-policy) + +--- + +## Change Management + +| Change Type | Approval | Process | +|-------------|----------|---------| +| Routine (docs, bug fixes) | Maintainer | PR → CI pass → merge | +| Significant (new features) | Maintainer | PR with description → CI pass → merge | +| Major (breaking, architecture) | Maintainer | Issue discussion → PR → CI pass → merge | +| Emergency (security) | Maintainer | Labelled `EMERGENCY` → immediate merge → post-mortem | + +--- + +## Reporting Issues + +- **Bugs / Features**: Open a [GitHub Issue](https://github.com/mokoconsulting-tech/Template-Joomla/issues) +- **Security vulnerabilities**: See [SECURITY.md](./SECURITY.md) +- **Code of Conduct**: See [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) +- **Contact**: dev@mokoconsulting.tech + +--- + +## Metadata + +| Field | Value | +| ------------- | ----------------------------------------------- | +| Document Type | Policy | +| Domain | Governance | +| Applies To | mokoconsulting-tech/Template-Joomla | +| Jurisdiction | Tennessee, USA | +| Maintainer | @mokoconsulting-tech | +| Standards | MokoStandards v04.00.04 | +| Repo | https://github.com/mokoconsulting-tech/Template-Joomla | +| Path | /GOVERNANCE.md | +| Status | Active — auto-maintained by MokoStandards | diff --git a/README.md b/README.md index 6a3c6a93..64995ac0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MokoSuiteCross - + Cross-posting Joomla content to social media, email marketing, and chat platforms for Joomla 6. diff --git a/SECURITY.md b/SECURITY.md index 6718978c..bb261b18 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -23,7 +23,7 @@ DEFGROUP: Template-Joomla INGROUP: Template-Joomla.Documentation REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla PATH: /SECURITY.md -VERSION: 01.11.03 +VERSION: 01.12.00 BRIEF: Security vulnerability reporting and handling policy --> @@ -224,10 +224,10 @@ The following are explicitly out of scope: ## Metadata -| Field | Value | +| Field | Value | | ------------ | ------------------------------------------------------------------------------------------------------------ | -| Document | Security Policy | -| Path | /SECURITY.md | +| Document | Security Policy | +| Path | /SECURITY.md | | Repository | [https://github.com/mokoconsulting-tech/Template-Joomla](https://github.com/mokoconsulting-tech/Template-Joomla) | | Owner | Moko Consulting | | Scope | Security vulnerability handling | diff --git a/source/packages/com_mokosuitecross/mokosuitecross.xml b/source/packages/com_mokosuitecross/mokosuitecross.xml index 422808e9..2e69901a 100644 --- a/source/packages/com_mokosuitecross/mokosuitecross.xml +++ b/source/packages/com_mokosuitecross/mokosuitecross.xml @@ -1,7 +1,7 @@ com_mokosuitecross - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/com_mokosuitecross/sql/updates/mysql/01.11.00.sql b/source/packages/com_mokosuitecross/sql/updates/mysql/01.11.00.sql new file mode 100644 index 00000000..c86da24b --- /dev/null +++ b/source/packages/com_mokosuitecross/sql/updates/mysql/01.11.00.sql @@ -0,0 +1 @@ +/* 01.11.00 — no schema changes */ diff --git a/source/packages/com_mokosuitecross/sql/updates/mysql/01.12.00.sql b/source/packages/com_mokosuitecross/sql/updates/mysql/01.12.00.sql new file mode 100644 index 00000000..a2fb44d6 --- /dev/null +++ b/source/packages/com_mokosuitecross/sql/updates/mysql/01.12.00.sql @@ -0,0 +1 @@ +/* 01.12.00 — no schema changes */ diff --git a/source/packages/plg_content_mokosuitecross/mokosuitecross.xml b/source/packages/plg_content_mokosuitecross/mokosuitecross.xml index 315d5e98..a797d4ba 100644 --- a/source/packages/plg_content_mokosuitecross/mokosuitecross.xml +++ b/source/packages/plg_content_mokosuitecross/mokosuitecross.xml @@ -1,7 +1,7 @@ Content - MokoSuiteCross - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_activitypub/activitypub.xml b/source/packages/plg_mokosuitecross_activitypub/activitypub.xml index a6042312..e7c45c5e 100644 --- a/source/packages/plg_mokosuitecross_activitypub/activitypub.xml +++ b/source/packages/plg_mokosuitecross_activitypub/activitypub.xml @@ -1,7 +1,7 @@ MokoSuiteCross - ActivityPub (Fediverse) - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_blogger/blogger.xml b/source/packages/plg_mokosuitecross_blogger/blogger.xml index a09b25a5..6c7cac51 100644 --- a/source/packages/plg_mokosuitecross_blogger/blogger.xml +++ b/source/packages/plg_mokosuitecross_blogger/blogger.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Google Blogger - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_bluesky/bluesky.xml b/source/packages/plg_mokosuitecross_bluesky/bluesky.xml index 77b66919..e48c48ab 100644 --- a/source/packages/plg_mokosuitecross_bluesky/bluesky.xml +++ b/source/packages/plg_mokosuitecross_bluesky/bluesky.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Bluesky - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_brevo/brevo.xml b/source/packages/plg_mokosuitecross_brevo/brevo.xml index 1ce161a7..7cb3cfe1 100644 --- a/source/packages/plg_mokosuitecross_brevo/brevo.xml +++ b/source/packages/plg_mokosuitecross_brevo/brevo.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Brevo (Sendinblue) - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_constantcontact/constantcontact.xml b/source/packages/plg_mokosuitecross_constantcontact/constantcontact.xml index a2667704..12e964a8 100644 --- a/source/packages/plg_mokosuitecross_constantcontact/constantcontact.xml +++ b/source/packages/plg_mokosuitecross_constantcontact/constantcontact.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Constant Contact - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_convertkit/convertkit.xml b/source/packages/plg_mokosuitecross_convertkit/convertkit.xml index f1da8867..c0a9ff6a 100644 --- a/source/packages/plg_mokosuitecross_convertkit/convertkit.xml +++ b/source/packages/plg_mokosuitecross_convertkit/convertkit.xml @@ -1,7 +1,7 @@ MokoSuiteCross - ConvertKit - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_devto/devto.xml b/source/packages/plg_mokosuitecross_devto/devto.xml index a046bac6..3ab19bcc 100644 --- a/source/packages/plg_mokosuitecross_devto/devto.xml +++ b/source/packages/plg_mokosuitecross_devto/devto.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Dev.to - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_discord/discord.xml b/source/packages/plg_mokosuitecross_discord/discord.xml index 08a9685a..a7c9862a 100644 --- a/source/packages/plg_mokosuitecross_discord/discord.xml +++ b/source/packages/plg_mokosuitecross_discord/discord.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Discord - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_facebook/facebook.xml b/source/packages/plg_mokosuitecross_facebook/facebook.xml index 0c4d3964..2a249503 100644 --- a/source/packages/plg_mokosuitecross_facebook/facebook.xml +++ b/source/packages/plg_mokosuitecross_facebook/facebook.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Facebook / Meta - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_ghost/ghost.xml b/source/packages/plg_mokosuitecross_ghost/ghost.xml index 4c9d9f17..3dfaebca 100644 --- a/source/packages/plg_mokosuitecross_ghost/ghost.xml +++ b/source/packages/plg_mokosuitecross_ghost/ghost.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Ghost - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_googlebusiness/googlebusiness.xml b/source/packages/plg_mokosuitecross_googlebusiness/googlebusiness.xml index 245ca01d..0d7e9ecb 100644 --- a/source/packages/plg_mokosuitecross_googlebusiness/googlebusiness.xml +++ b/source/packages/plg_mokosuitecross_googlebusiness/googlebusiness.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Google Business Profile - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_googlechat/googlechat.xml b/source/packages/plg_mokosuitecross_googlechat/googlechat.xml index 83a56bbc..7aa1bae9 100644 --- a/source/packages/plg_mokosuitecross_googlechat/googlechat.xml +++ b/source/packages/plg_mokosuitecross_googlechat/googlechat.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Google Chat - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_hashnode/hashnode.xml b/source/packages/plg_mokosuitecross_hashnode/hashnode.xml index a0fa51fd..d474b7ee 100644 --- a/source/packages/plg_mokosuitecross_hashnode/hashnode.xml +++ b/source/packages/plg_mokosuitecross_hashnode/hashnode.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Hashnode - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_instagram/instagram.xml b/source/packages/plg_mokosuitecross_instagram/instagram.xml index 1c314848..42e505b0 100644 --- a/source/packages/plg_mokosuitecross_instagram/instagram.xml +++ b/source/packages/plg_mokosuitecross_instagram/instagram.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Instagram - 01.11.03 + 01.12.00 2026-06-23 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_linkedin/linkedin.xml b/source/packages/plg_mokosuitecross_linkedin/linkedin.xml index d91aa3e0..d8f66e2a 100644 --- a/source/packages/plg_mokosuitecross_linkedin/linkedin.xml +++ b/source/packages/plg_mokosuitecross_linkedin/linkedin.xml @@ -1,7 +1,7 @@ MokoSuiteCross - LinkedIn - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml b/source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml index 1c84eac5..bb728cc6 100644 --- a/source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml +++ b/source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Mailchimp - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_mastodon/mastodon.xml b/source/packages/plg_mokosuitecross_mastodon/mastodon.xml index 651d6fa6..3affc8a9 100644 --- a/source/packages/plg_mokosuitecross_mastodon/mastodon.xml +++ b/source/packages/plg_mokosuitecross_mastodon/mastodon.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Mastodon - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_matrix/matrix.xml b/source/packages/plg_mokosuitecross_matrix/matrix.xml index 8d147a75..e4f8cefb 100644 --- a/source/packages/plg_mokosuitecross_matrix/matrix.xml +++ b/source/packages/plg_mokosuitecross_matrix/matrix.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Matrix / Element - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_medium/medium.xml b/source/packages/plg_mokosuitecross_medium/medium.xml index 718e7205..5ee00266 100644 --- a/source/packages/plg_mokosuitecross_medium/medium.xml +++ b/source/packages/plg_mokosuitecross_medium/medium.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Medium - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.xml b/source/packages/plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.xml index db690034..8e57fc9a 100644 --- a/source/packages/plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.xml +++ b/source/packages/plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.xml @@ -1,7 +1,7 @@ MokoSuiteCross - MokoSuiteCalendar Events - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_mokosuitegallery/mokosuitegallery.xml b/source/packages/plg_mokosuitecross_mokosuitegallery/mokosuitegallery.xml index e37a995e..ae9c5d2d 100644 --- a/source/packages/plg_mokosuitecross_mokosuitegallery/mokosuitegallery.xml +++ b/source/packages/plg_mokosuitecross_mokosuitegallery/mokosuitegallery.xml @@ -1,7 +1,7 @@ MokoSuiteCross - MokoSuiteGallery - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_nostr/nostr.xml b/source/packages/plg_mokosuitecross_nostr/nostr.xml index 033f7288..d2d1e0a6 100644 --- a/source/packages/plg_mokosuitecross_nostr/nostr.xml +++ b/source/packages/plg_mokosuitecross_nostr/nostr.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Nostr - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_ntfy/ntfy.xml b/source/packages/plg_mokosuitecross_ntfy/ntfy.xml index f4ec061a..ba15ecaf 100644 --- a/source/packages/plg_mokosuitecross_ntfy/ntfy.xml +++ b/source/packages/plg_mokosuitecross_ntfy/ntfy.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Ntfy Push Notifications - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_pinterest/pinterest.xml b/source/packages/plg_mokosuitecross_pinterest/pinterest.xml index 654b1c57..d103a6ae 100644 --- a/source/packages/plg_mokosuitecross_pinterest/pinterest.xml +++ b/source/packages/plg_mokosuitecross_pinterest/pinterest.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Pinterest - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_reddit/reddit.xml b/source/packages/plg_mokosuitecross_reddit/reddit.xml index c43a40a1..42d35f37 100644 --- a/source/packages/plg_mokosuitecross_reddit/reddit.xml +++ b/source/packages/plg_mokosuitecross_reddit/reddit.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Reddit - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_rssfeed/rssfeed.xml b/source/packages/plg_mokosuitecross_rssfeed/rssfeed.xml index 97b8d1ee..a0d8c930 100644 --- a/source/packages/plg_mokosuitecross_rssfeed/rssfeed.xml +++ b/source/packages/plg_mokosuitecross_rssfeed/rssfeed.xml @@ -1,7 +1,7 @@ MokoSuiteCross - RSS Feed - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_sendgrid/sendgrid.xml b/source/packages/plg_mokosuitecross_sendgrid/sendgrid.xml index aefe1443..2e1c408c 100644 --- a/source/packages/plg_mokosuitecross_sendgrid/sendgrid.xml +++ b/source/packages/plg_mokosuitecross_sendgrid/sendgrid.xml @@ -1,7 +1,7 @@ MokoSuiteCross - SendGrid - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_slack/slack.xml b/source/packages/plg_mokosuitecross_slack/slack.xml index 94f8dc8f..df5b9c14 100644 --- a/source/packages/plg_mokosuitecross_slack/slack.xml +++ b/source/packages/plg_mokosuitecross_slack/slack.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Slack - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_teams/teams.xml b/source/packages/plg_mokosuitecross_teams/teams.xml index 1d4fa1c6..a3ba3569 100644 --- a/source/packages/plg_mokosuitecross_teams/teams.xml +++ b/source/packages/plg_mokosuitecross_teams/teams.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Microsoft Teams - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_telegram/telegram.xml b/source/packages/plg_mokosuitecross_telegram/telegram.xml index 6b0b3206..58414c7f 100644 --- a/source/packages/plg_mokosuitecross_telegram/telegram.xml +++ b/source/packages/plg_mokosuitecross_telegram/telegram.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Telegram - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_threads/threads.xml b/source/packages/plg_mokosuitecross_threads/threads.xml index c96e0494..e5d0e7f3 100644 --- a/source/packages/plg_mokosuitecross_threads/threads.xml +++ b/source/packages/plg_mokosuitecross_threads/threads.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Threads (Meta) - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_tiktok/tiktok.xml b/source/packages/plg_mokosuitecross_tiktok/tiktok.xml index cb301e6c..9dd7a5f1 100644 --- a/source/packages/plg_mokosuitecross_tiktok/tiktok.xml +++ b/source/packages/plg_mokosuitecross_tiktok/tiktok.xml @@ -1,7 +1,7 @@ MokoSuiteCross - TikTok - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_tumblr/tumblr.xml b/source/packages/plg_mokosuitecross_tumblr/tumblr.xml index b6ae55c9..b7d14cea 100644 --- a/source/packages/plg_mokosuitecross_tumblr/tumblr.xml +++ b/source/packages/plg_mokosuitecross_tumblr/tumblr.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Tumblr - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_twitter/twitter.xml b/source/packages/plg_mokosuitecross_twitter/twitter.xml index b415fd9f..8c87b722 100644 --- a/source/packages/plg_mokosuitecross_twitter/twitter.xml +++ b/source/packages/plg_mokosuitecross_twitter/twitter.xml @@ -1,7 +1,7 @@ MokoSuiteCross - X / Twitter - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_webhook/webhook.xml b/source/packages/plg_mokosuitecross_webhook/webhook.xml index 0f60acae..fdf655d1 100644 --- a/source/packages/plg_mokosuitecross_webhook/webhook.xml +++ b/source/packages/plg_mokosuitecross_webhook/webhook.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Generic Webhook - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_whatsapp/whatsapp.xml b/source/packages/plg_mokosuitecross_whatsapp/whatsapp.xml index f4c7a65e..d260ebcc 100644 --- a/source/packages/plg_mokosuitecross_whatsapp/whatsapp.xml +++ b/source/packages/plg_mokosuitecross_whatsapp/whatsapp.xml @@ -1,7 +1,7 @@ MokoSuiteCross - WhatsApp Business - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_wordpress/wordpress.xml b/source/packages/plg_mokosuitecross_wordpress/wordpress.xml index 1840e440..8184507d 100644 --- a/source/packages/plg_mokosuitecross_wordpress/wordpress.xml +++ b/source/packages/plg_mokosuitecross_wordpress/wordpress.xml @@ -1,7 +1,7 @@ MokoSuiteCross - WordPress - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_youtube/youtube.xml b/source/packages/plg_mokosuitecross_youtube/youtube.xml index 0b1eb055..10537c39 100644 --- a/source/packages/plg_mokosuitecross_youtube/youtube.xml +++ b/source/packages/plg_mokosuitecross_youtube/youtube.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Youtube - 01.11.03 + 01.12.00 2026-06-23 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_system_mokosuitecross/mokosuitecross.xml b/source/packages/plg_system_mokosuitecross/mokosuitecross.xml index 8eab3eb6..7401b98c 100644 --- a/source/packages/plg_system_mokosuitecross/mokosuitecross.xml +++ b/source/packages/plg_system_mokosuitecross/mokosuitecross.xml @@ -1,7 +1,7 @@ System - MokoSuiteCross - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_system_mokosuitecross_events/mokosuitecross_events.xml b/source/packages/plg_system_mokosuitecross_events/mokosuitecross_events.xml index 523b90b7..11767a92 100644 --- a/source/packages/plg_system_mokosuitecross_events/mokosuitecross_events.xml +++ b/source/packages/plg_system_mokosuitecross_events/mokosuitecross_events.xml @@ -1,7 +1,7 @@ System - MokoSuiteCross Events - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_system_mokosuitecross_gallery/mokosuitecross_gallery.xml b/source/packages/plg_system_mokosuitecross_gallery/mokosuitecross_gallery.xml index fb862b6f..a017db59 100644 --- a/source/packages/plg_system_mokosuitecross_gallery/mokosuitecross_gallery.xml +++ b/source/packages/plg_system_mokosuitecross_gallery/mokosuitecross_gallery.xml @@ -1,7 +1,7 @@ System - MokoSuiteCross Gallery - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_task_mokosuitecross/mokosuitecross.xml b/source/packages/plg_task_mokosuitecross/mokosuitecross.xml index f272064a..96473d2c 100644 --- a/source/packages/plg_task_mokosuitecross/mokosuitecross.xml +++ b/source/packages/plg_task_mokosuitecross/mokosuitecross.xml @@ -1,7 +1,7 @@ Task - MokoSuiteCross Queue Processor - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_webservices_mokosuitecross/mokosuitecross.xml b/source/packages/plg_webservices_mokosuitecross/mokosuitecross.xml index 8d6ab8d0..ae69ef00 100644 --- a/source/packages/plg_webservices_mokosuitecross/mokosuitecross.xml +++ b/source/packages/plg_webservices_mokosuitecross/mokosuitecross.xml @@ -1,7 +1,7 @@ Web Services - MokoSuiteCross - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/pkg_mokosuitecross.xml b/source/pkg_mokosuitecross.xml index bc23d98f..172bf5ef 100644 --- a/source/pkg_mokosuitecross.xml +++ b/source/pkg_mokosuitecross.xml @@ -2,7 +2,7 @@ MokoSuiteCross mokosuitecross - 01.11.03 + 01.12.00 2026-05-28 Moko Consulting hello@mokoconsulting.tech