From 27a0dae40c624b2881afcbb3d02ef8970bc4533d Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Fri, 17 Apr 2026 10:33:14 +0000 Subject: [PATCH 001/136] chore: add update.xml from MokoStandards --- update.xml | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 update.xml diff --git a/update.xml b/update.xml new file mode 100644 index 0000000..551709a --- /dev/null +++ b/update.xml @@ -0,0 +1,39 @@ + + + + {{EXTENSION_NAME}} + MokoJoomHero — Moko Consulting Joomla extension + {{EXTENSION_ELEMENT}} + {{EXTENSION_TYPE}} + {{VERSION}} + + + https://git.mokoconsulting.tech/mokoconsulting-tech/MokoJoomHero/releases/download/v{{VERSION}}/{{EXTENSION_ELEMENT}}.zip + + + https://github.com/mokoconsulting-tech/MokoJoomHero/releases/download/v{{VERSION}}/{{EXTENSION_ELEMENT}}.zip + + + + 8.1 + + \ No newline at end of file -- 2.52.0 From a90aa1af48ebe07904b2747245f8f59b6ada6227 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Fri, 17 Apr 2026 10:39:23 +0000 Subject: [PATCH 002/136] chore: update update.xml from MokoStandards -- 2.52.0 From e8c8847c1f722e9a901e9d5c1caa5f035015f911 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Fri, 17 Apr 2026 10:47:23 +0000 Subject: [PATCH 003/136] chore: update update.xml from MokoStandards -- 2.52.0 From 9daf37ded5f590da376e1109cca9c3d2d4a0bd7d Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 19 Apr 2026 05:54:14 +0000 Subject: [PATCH 004/136] chore: update updates.xml from MokoStandards --- updates.xml | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/updates.xml b/updates.xml index 9ced263..96e60df 100644 --- a/updates.xml +++ b/updates.xml @@ -1,3 +1,39 @@ - + - + + {{EXTENSION_NAME}} + MokoJoomHero — Moko Consulting Joomla extension + {{EXTENSION_ELEMENT}} + {{EXTENSION_TYPE}} + {{VERSION}} + + + https://git.mokoconsulting.tech/mokoconsulting-tech/MokoJoomHero/releases/download/v{{VERSION}}/{{EXTENSION_ELEMENT}}.zip + + + https://github.com/mokoconsulting-tech/MokoJoomHero/releases/download/v{{VERSION}}/{{EXTENSION_ELEMENT}}.zip + + + + 8.1 + + \ No newline at end of file -- 2.52.0 From 30113038431f439eb883c4a425c2d87d426f9e32 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Fri, 24 Apr 2026 00:29:41 +0000 Subject: [PATCH 005/136] chore: bump patch version for release pipeline fixes [skip ci] --- README.md | 224 +++++++++--------------------------------------------- 1 file changed, 35 insertions(+), 189 deletions(-) diff --git a/README.md b/README.md index 114aca2..a2552af 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,25 @@ -# MokoStandards-Template-Joomla-Module + + +# MokoJoomHero + +A Joomla Module developed following MokoStandards. [![License](https://img.shields.io/badge/license-GPL--3.0--or--later-blue.svg)](LICENSE) -## Table of Contents - -1. [Overview](#overview) -2. [Features](#features) -3. [Prerequisites](#prerequisites) -4. [Installation](#installation) -5. [Usage](#usage) -6. [Project Structure](#project-structure) -7. [Development](#development) -8. [Testing](#testing) -9. [Building](#building) -10. [Contributing](#contributing) -11. [Versioning](#versioning) -12. [License](#license) -13. [Support](#support) - ## Overview -This is a standardized template repository for creating Joomla modules that conform to Moko Consulting's development standards and best practices. It provides a consistent starting point with pre-configured tooling, documentation structure, and development workflows. - -Use this template when creating new Joomla modules to ensure: -- Consistent code structure and organization -- Pre-configured development tools (linters, formatters) -- Standardized documentation and contribution guidelines -- Built-in build and deployment workflows +This is a standardized Joomla module that conforms to Moko Consulting's development standards and best practices. It provides a consistent starting point with pre-configured tooling, documentation structure, and development workflows. ## Features @@ -41,182 +32,50 @@ Use this template when creating new Joomla modules to ensure: ## Prerequisites -Before using this template, ensure you have the following installed: - - **PHP**: 7.4 or higher (8.0+ recommended for Joomla 4.x/5.x) -- **Composer**: For PHP dependency management (optional but recommended) - **Joomla**: A working Joomla installation for testing (3.x, 4.x, or 5.x) -- **PHP CodeSniffer**: For code quality checks (`composer global require squizlabs/php_codesniffer`) - **Make**: GNU Make for running build commands -- **Git**: For version control ## Installation -### Using This Template +1. Build the module package: + ```bash + make build + ``` -1. **Create a new repository from this template**: - - Click "Use this template" button on GitHub - - Or clone and remove git history: - ```bash - git clone https://github.com/mokoconsulting-tech/MokoStandards-Template-Joomla-Module.git my-joomla-module - cd my-joomla-module - rm -rf .git - git init - ``` - -2. **Customize for your module**: - - Update `Makefile` with your module name, type, and version - - Update this README with your module's specific information - - Update LICENSE file with appropriate copyright holder - - Create your module files in the `src/` directory - -3. **Initialize git**: - ```bash - git add . - git commit -m "feat: initial commit from template" - ``` +2. Upload the generated ZIP file via Joomla's Extension Manager ## Usage -### Quick Start +```bash +# Display available commands +make help -1. **Configure your module** in the `Makefile`: - ```makefile - MODULE_NAME := yourmodulename - MODULE_TYPE := site # or 'admin' for backend modules - MODULE_VERSION := 1.0.0 - ``` +# Validate code +make validate -2. **Run the help command** to see available tasks: - ```bash - make help - ``` - -3. **Develop your module** in the `src/` directory - -4. **Validate your code**: - ```bash - make validate - ``` - -5. **Build the module package**: - ```bash - make build - ``` - -### Common Tasks - -See the [Development Guide](docs/DEVELOPMENT.md) for detailed development workflows. +# Build module package +make build +``` ## Project Structure ``` . -├── docs/ # Documentation files -├── scripts/ # Build and deployment scripts -├── src/ # Module source code -├── .editorconfig # Editor configuration -├── .gitignore # Git ignore patterns -├── .gitmessage # Git commit message template -├── Makefile # Build automation -└── README.md # This file +├── docs/ # Documentation files +├── scripts/ # Build and deployment scripts +├── src/ # Module source code +├── Makefile # Build automation +└── README.md # This file ``` -## Development - -### Setting Up Development Environment - -1. **Install dependencies**: - ```bash - # Install PHP CodeSniffer globally - composer global require squizlabs/php_codesniffer - composer global require dealerdirect/phpcodesniffer-composer-installer - ``` - -2. **Configure your IDE** to use the `.editorconfig` settings - -3. **Set up pre-commit hooks** (optional): - ```bash - git config commit.template .gitmessage - ``` - -### Development Workflow - -See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for detailed development guidelines. - -### Code Style - -This project follows: -- Joomla Coding Standards for PHP -- Tab indentation (width: 2 spaces) -- LF line endings -- UTF-8 encoding - -## Testing - -### Manual Testing - -1. **Create development symlink**: - ```bash - # Update JOOMLA_ROOT in Makefile first - make dev-install - ``` - -2. **Test in Joomla**: - - Enable the module in Joomla admin - - Assign to a module position - - Test on frontend/backend - -### Automated Testing - -```bash -make lint # PHP syntax check -make phpcs # Code standards check -make validate # Run all checks -``` - -## Building - -### Build Module Package - -```bash -make build -``` - -This creates a ZIP package in the `dist/` directory ready for installation in Joomla. - -### Installing to Joomla - -**Option 1: Upload via Joomla Admin** -```bash -make install-local -``` -Then upload the generated ZIP file via Extensions > Manage > Install - -**Option 2: Development Symlink** -```bash -make dev-install -``` -Creates a symlink for development (requires configured JOOMLA_ROOT) - ## Contributing We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. -### Quick Contribution Guide - -1. Fork the repository -2. Create a feature branch (`git checkout -b feat/amazing-feature`) -3. Commit your changes using conventional commits -4. Push to your branch -5. Open a Pull Request - ## Versioning -This project uses [Semantic Versioning](https://semver.org/): -- **MAJOR**: Incompatible API changes -- **MINOR**: New functionality (backward compatible) -- **PATCH**: Bug fixes (backward compatible) +This project uses [Semantic Versioning](https://semver.org/). See [CHANGELOG.md](CHANGELOG.md) for version history. @@ -226,23 +85,10 @@ Copyright (C) 2026 Moko Consulting This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along with this program. If not, see . - SPDX-License-Identifier: GPL-3.0-or-later -### License Management - -This repository includes a GitHub Actions workflow for syncing all GPL licenses (GPL-2.0, GPL-3.0, LGPL-2.1, LGPL-3.0) from www.gnu.org. The workflow must be triggered manually - automatic scheduling is disabled. The primary GPL-3.0 license is maintained in the LICENSE file, with additional licenses stored in the `licenses/` directory. For enterprise environments with firewall restrictions, see [Firewall Configuration Guide](docs/FIREWALL_CONFIGURATION.md) for required network access configuration. - ## Support - **Documentation**: See the [docs/](docs/) directory -- **Issues**: Report bugs and request features via [GitHub Issues](../../issues) - **Contact**: hello@mokoconsulting.tech - **Website**: [mokoconsulting.tech](https://mokoconsulting.tech) - -## Acknowledgments - -This template is maintained by Moko Consulting and follows MokoStandards for consistent, high-quality Joomla module development. -- 2.52.0 From 192e089253248d47bc89772769598d9ef3e099cf Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 26 Apr 2026 16:35:39 +0000 Subject: [PATCH 006/136] chore: add TODO.md from MokoStandards --- TODO.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 TODO.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..488a8ab --- /dev/null +++ b/TODO.md @@ -0,0 +1,12 @@ +# TODO + +> **Note:** This file is not tracked in version control (.gitignore). It is for local task tracking only. + +## Critical + - + +## Normal + - + +## Low + - -- 2.52.0 From 8aee1ce3c0c46173554752ff6118ea1a47ea2be9 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sun, 26 Apr 2026 21:48:31 -0500 Subject: [PATCH 007/136] ci: sync release workflows v2 [skip ci] --- .gitea/workflows/release.yml | 647 ++++ .gitea/workflows/update-server.yml | 495 ++++ .github/workflows/auto-assign.yml | 76 - .github/workflows/auto-dev-issue.yml | 207 -- .github/workflows/auto-release.yml | 337 --- .github/workflows/changelog-validation.yml | 101 - .github/workflows/ci-joomla.yml | 391 --- .github/workflows/codeql-analysis.yml | 115 - .github/workflows/deploy-demo.yml | 734 ----- .github/workflows/deploy-dev.yml | 700 ----- .github/workflows/deploy-manual.yml | 132 - .../workflows/enterprise-firewall-setup.yml | 758 ----- .github/workflows/repo_health.yml | 795 ----- .github/workflows/repository-cleanup.yml | 525 ---- .github/workflows/standards-compliance.yml | 2614 ----------------- .github/workflows/sync-version-on-merge.yml | 133 - .github/workflows/update-server.yml | 249 -- 17 files changed, 1142 insertions(+), 7867 deletions(-) create mode 100644 .gitea/workflows/release.yml create mode 100644 .gitea/workflows/update-server.yml delete mode 100644 .github/workflows/auto-assign.yml delete mode 100644 .github/workflows/auto-dev-issue.yml delete mode 100644 .github/workflows/auto-release.yml delete mode 100644 .github/workflows/changelog-validation.yml delete mode 100644 .github/workflows/ci-joomla.yml delete mode 100644 .github/workflows/codeql-analysis.yml delete mode 100644 .github/workflows/deploy-demo.yml delete mode 100644 .github/workflows/deploy-dev.yml delete mode 100644 .github/workflows/deploy-manual.yml delete mode 100644 .github/workflows/enterprise-firewall-setup.yml delete mode 100644 .github/workflows/repo_health.yml delete mode 100644 .github/workflows/repository-cleanup.yml delete mode 100644 .github/workflows/standards-compliance.yml delete mode 100644 .github/workflows/sync-version-on-merge.yml delete mode 100644 .github/workflows/update-server.yml diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..c3daa98 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,647 @@ +# Copyright (C) 2026 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: MokoStandards.Joomla +# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards +# PATH: /.gitea/workflows/release.yml +# VERSION: 02.00.00 +# BRIEF: Generic Joomla release — auto-detects element from manifest, stream tags, cascade + +name: Create Release + +on: + push: + tags: + - 'stable' + - 'release-candidate' + - 'beta' + - 'alpha' + - 'development' + workflow_dispatch: + inputs: + stability: + description: 'Stability tag' + required: true + default: 'stable' + type: choice + options: + - stable + - release-candidate + - beta + - alpha + - development + +permissions: + contents: write + +env: + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +jobs: + build: + name: Build Release Package + runs-on: release + + steps: + # Always checkout main for tag triggers (avoids detached HEAD). + # For workflow_dispatch, checkout whatever branch was selected. + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.event_name == 'push' && 'main' || github.ref }} + fetch-depth: 0 + token: ${{ secrets.GA_TOKEN }} + + - name: Setup PHP + run: | + if ! command -v php &> /dev/null; then + sudo apt-get update -qq + sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + echo "PHP: $(php -v | head -1)" + echo "Composer: $(composer --version 2>&1 | head -1)" + + - name: Get version and stability + id: meta + run: | + echo "=== Meta ===" + echo "event_name: ${{ github.event_name }}" + echo "ref: ${{ github.ref }}" + echo "ref_name: ${{ github.ref_name }}" + echo "sha: ${{ github.sha }}" + + # Derive stability from tag name or dispatch input + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + STABILITY="${{ inputs.stability }}" + else + TAG_PUSHED="${GITHUB_REF#refs/tags/}" + case "$TAG_PUSHED" in + stable) STABILITY="stable" ;; + release-candidate) STABILITY="rc" ;; + beta) STABILITY="beta" ;; + alpha) STABILITY="alpha" ;; + development) STABILITY="development" ;; + *) STABILITY="stable" ;; + esac + fi + + # Read version from README.md (will be bumped in next step) + VERSION=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1) + [ -z "$VERSION" ] && VERSION="00.00.00" + + # Auto-detect extension element from Joomla manifest + # Search depth 3 covers src/admin/com_xxx.xml and similar nested structures + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '/dev/null | head -1) + EXT_ELEMENT="" + if [ -n "$MANIFEST" ]; then + EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1) + # If no tag, derive from manifest filename or repo name + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') + case "$EXT_ELEMENT" in + templatedetails|manifest) EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;; + esac + fi + echo "Manifest: ${MANIFEST}, element: ${EXT_ELEMENT}" + else + EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + echo "No manifest found, using repo name: ${EXT_ELEMENT}" + fi + + case "$STABILITY" in + development) SUFFIX="-dev"; TAG_NAME="development" ;; + alpha) SUFFIX="-alpha"; TAG_NAME="alpha" ;; + beta) SUFFIX="-beta"; TAG_NAME="beta" ;; + rc) SUFFIX="-rc"; TAG_NAME="release-candidate" ;; + stable) SUFFIX=""; TAG_NAME="stable" ;; + *) SUFFIX="-dev"; TAG_NAME="development" ;; + esac + + PRERELEASE="true" + [ "$STABILITY" = "stable" ] && PRERELEASE="false" + + ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip" + + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" + echo "prerelease=${PRERELEASE}" >> "$GITHUB_OUTPUT" + echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" + echo "tag_name=${TAG_NAME}" >> "$GITHUB_OUTPUT" + echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" + echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" + + echo "=== Resolved ===" + echo "VERSION=${VERSION}" + echo "STABILITY=${STABILITY}" + echo "TAG_NAME=${TAG_NAME}" + echo "ZIP_NAME=${ZIP_NAME}" + echo "Branch: $(git branch --show-current)" + + - name: Auto-bump patch version + id: bump + env: + GA_TOKEN: ${{ secrets.GA_TOKEN }} + INPUT_VERSION: ${{ steps.meta.outputs.version }} + INPUT_STABILITY: ${{ steps.meta.outputs.stability }} + INPUT_SUFFIX: ${{ steps.meta.outputs.suffix }} + EXT_ELEMENT: ${{ steps.meta.outputs.ext_element }} + run: | + BRANCH=$(git branch --show-current) + GITEA_API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" + + echo "=== Version Bump ===" + echo "On branch: ${BRANCH}" + + # Read current version from README.md + CURRENT=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1) + echo "Current version in README: ${CURRENT}" + + if [ -z "$CURRENT" ]; then + echo "No VERSION in README.md — using input version" + echo "version=${INPUT_VERSION}" >> "$GITHUB_OUTPUT" + echo "zip_name=${EXT_ELEMENT}-${INPUT_VERSION}${INPUT_SUFFIX}.zip" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Bump patch: XX.YY.ZZ → XX.YY.(ZZ+1) + MAJOR=$(echo "$CURRENT" | cut -d. -f1) + MINOR=$(echo "$CURRENT" | cut -d. -f2) + PATCH=$(echo "$CURRENT" | cut -d. -f3) + NEW_PATCH=$(printf "%02d" $((10#$PATCH + 1))) + NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}" + TODAY=$(date +%Y-%m-%d) + + echo "Bumping: ${CURRENT} → ${NEW_VERSION} (date: ${TODAY})" + + # Update README.md + sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${NEW_VERSION}/" README.md + + # Update manifest (templateDetails.xml / *.xml with ) + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '/dev/null | head -1) + if [ -n "$MANIFEST" ]; then + echo "Manifest: ${MANIFEST}" + sed -i "s|${CURRENT}|${NEW_VERSION}|" "$MANIFEST" + sed -i "s|[^<]*|${TODAY}|" "$MANIFEST" + fi + + # Update matching stability channel in updates.xml + if [ -f "updates.xml" ]; then + export PY_OLD="$CURRENT" PY_NEW="$NEW_VERSION" PY_STABILITY="$INPUT_STABILITY" PY_DATE="$TODAY" + python3 << 'PYEOF' + import re, os + old = os.environ["PY_OLD"] + new = os.environ["PY_NEW"] + stability = os.environ["PY_STABILITY"] + date = os.environ["PY_DATE"] + with open("updates.xml") as f: + content = f.read() + pattern = r"((?:(?!).)*?" + re.escape(stability) + r".*?)" + match = re.search(pattern, content, re.DOTALL) + if match: + block = match.group(1) + updated = block.replace(old, new) + updated = re.sub(r"[^<]*", f"{date}", updated) + content = content.replace(block, updated) + print(f"Updated {stability} channel: {old} -> {new}") + else: + print(f"WARNING: No block found for {stability}") + with open("updates.xml", "w") as f: + f.write(content) + PYEOF + fi + + # Commit and push version bump + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://jmiller:${GA_TOKEN}@git.mokoconsulting.tech/${{ github.repository }}.git" + git add -A + git diff --cached --quiet && echo "No changes to commit" || { + git commit -m "chore(version): bump ${CURRENT} → ${NEW_VERSION} [skip ci]" \ + --author="gitea-actions[bot] " + echo "Pushing version bump to ${BRANCH}..." + git push origin HEAD:${BRANCH} 2>&1 + echo "Push exit code: $?" + } + + # For stable releases from non-main: merge to main via Gitea API + if [ "$INPUT_STABILITY" = "stable" ] && [ "$BRANCH" != "main" ]; then + echo "Merging ${BRANCH} → main via Gitea API..." + HTTP_CODE=$(curl -sS -o /tmp/merge_response.json -w "%{http_code}" \ + -X POST -H "Authorization: token ${GA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${GITEA_API}/merges" \ + -d "$(jq -n \ + --arg base "main" \ + --arg head "${BRANCH}" \ + --arg msg "chore(release): merge ${BRANCH} for stable ${NEW_VERSION} [skip ci]" \ + '{base: $base, head: $head, merge_message_field: $msg}' + )") + echo "Merge response (HTTP ${HTTP_CODE}):" + cat /tmp/merge_response.json 2>/dev/null; echo + fi + + echo "version=${NEW_VERSION}" >> "$GITHUB_OUTPUT" + echo "zip_name=${EXT_ELEMENT}-${NEW_VERSION}${INPUT_SUFFIX}.zip" >> "$GITHUB_OUTPUT" + echo "=== Bump complete: ${NEW_VERSION} ===" + + - name: Install dependencies + env: + COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_TOKEN }}"}}}' + run: | + if [ -f "composer.json" ]; then + echo "Installing composer dependencies..." + composer install --no-dev --optimize-autoloader --no-interaction 2>&1 + else + echo "No composer.json — skipping" + fi + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Minify CSS and JS + run: | + if [ -f "package.json" ] && [ -f "scripts/minify.js" ]; then + npm ci --ignore-scripts + node scripts/minify.js + else + echo "No minify setup — skipping" + fi + + - name: Create package + run: | + # Detect source directory (src/ or htdocs/) + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + if [ ! -d "$SOURCE_DIR" ]; then + echo "::error::No src/ or htdocs/ directory found" + exit 1 + fi + echo "Source directory: ${SOURCE_DIR}" + + mkdir -p build/package + rsync -av \ + --exclude='sftp-config*' \ + --exclude='.ftpignore' \ + --exclude='*.ppk' \ + --exclude='*.pem' \ + --exclude='*.key' \ + --exclude='.env*' \ + --exclude='*.local' \ + --exclude='.build-trigger' \ + --exclude='.beta-trigger' \ + --exclude='.rc-trigger' \ + "${SOURCE_DIR}/" build/package/ + echo "Package contents:" + ls -la build/package/ | head -20 + + - name: Build ZIP + id: zip + run: | + ZIP_NAME="${{ steps.bump.outputs.zip_name }}" + echo "Building: ${ZIP_NAME}" + cd build/package + zip -r "../${ZIP_NAME}" . + cd .. + + SHA256=$(sha256sum "${ZIP_NAME}" | cut -d' ' -f1) + SIZE=$(stat -c%s "${ZIP_NAME}") + + echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT" + echo "size=${SIZE}" >> "$GITHUB_OUTPUT" + echo "=== Package Built ===" + echo "ZIP: ${ZIP_NAME}" + echo "SHA-256: ${SHA256}" + echo "Size: ${SIZE} bytes" + + # ── Gitea Release (PRIMARY) ─────────────────────────���──────────── + - name: "Gitea: Create or update release" + id: gitea_release + env: + EXT_ELEMENT: ${{ steps.meta.outputs.ext_element }} + run: | + TAG="${{ steps.meta.outputs.tag_name }}" + VERSION="${{ steps.bump.outputs.version }}" + STABILITY="${{ steps.meta.outputs.stability }}" + SHA256="${{ steps.zip.outputs.sha256 }}" + TOKEN="${{ secrets.GA_TOKEN }}" + API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + BRANCH=$(git branch --show-current) + MAX_HISTORY=5 + + IS_PRE="true" + [ "$STABILITY" = "stable" ] && IS_PRE="false" + + # Build this version's entry + NEW_ENTRY="## ${VERSION} ($(date +%Y-%m-%d)) + **SHA-256:** \`${SHA256}\`" + + if [ -f "CHANGELOG.md" ]; then + NOTES=$(awk "/## \[${VERSION}\]/,/## \[/{if(/## \[${VERSION}\]/)next;if(/## \[/)exit;print}" CHANGELOG.md) + [ -n "$NOTES" ] && NEW_ENTRY="## ${VERSION} ($(date +%Y-%m-%d)) + ${NOTES} + **SHA-256:** \`${SHA256}\`" + fi + + # Check for existing release — keep last N versions in body + EXISTING_BODY="" + EXISTING_ID="" + RELEASE_JSON=$(curl -sS -H "Authorization: token ${TOKEN}" \ + "${API}/releases/tags/${TAG}" 2>/dev/null) + EXISTING_ID=$(echo "$RELEASE_JSON" | jq -r '.id // empty') + + if [ -n "$EXISTING_ID" ]; then + echo "Existing release found: id=${EXISTING_ID}" + EXISTING_BODY=$(echo "$RELEASE_JSON" | jq -r '.body // ""') + + # Keep only last (MAX_HISTORY - 1) version entries to make room for new one + TRIMMED_BODY=$(echo "$EXISTING_BODY" | python3 -c " + import sys, re + content = sys.stdin.read() + # Split on version headers (## XX.YY.ZZ) + parts = re.split(r'(?=^## \d)', content, flags=re.MULTILINE) + # Keep only version entries (skip any preamble) + versions = [p for p in parts if re.match(r'^## \d', p)] + # Keep last $((MAX_HISTORY - 1)) entries + kept = versions[:$((MAX_HISTORY - 1))] + print('\n---\n'.join(kept)) + " 2>/dev/null || echo "") + + # Delete old release and tag so we can recreate + curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API}/releases/${EXISTING_ID}" 2>/dev/null || true + curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API}/tags/${TAG}" 2>/dev/null || true + fi + + # Compose full body: new entry + previous entries + if [ -n "$TRIMMED_BODY" ]; then + FULL_BODY="${NEW_ENTRY} + + --- + + ${TRIMMED_BODY}" + else + FULL_BODY="${NEW_ENTRY}" + fi + + echo "=== Create Release ===" + echo "TAG=${TAG} VERSION=${VERSION} BRANCH=${BRANCH} PRE=${IS_PRE} HISTORY=${MAX_HISTORY}" + + HTTP_CODE=$(curl -sS -o /tmp/create_release.json -w "%{http_code}" \ + -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/releases" \ + -d "$(jq -n \ + --arg tag "$TAG" \ + --arg target "$BRANCH" \ + --arg name "${EXT_ELEMENT} ${VERSION} (${STABILITY})" \ + --arg body "$FULL_BODY" \ + --argjson pre "$IS_PRE" \ + '{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: $pre}' + )") + + echo "Response (HTTP ${HTTP_CODE}):" + cat /tmp/create_release.json | jq . 2>/dev/null || cat /tmp/create_release.json + echo + + RELEASE_ID=$(jq -r '.id // empty' /tmp/create_release.json) + if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then + echo "::error::Failed to create release (HTTP ${HTTP_CODE})" + exit 1 + fi + + echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT" + echo "Release created: id=${RELEASE_ID}" + + - name: "Gitea: Upload ZIP" + run: | + RELEASE_ID="${{ steps.gitea_release.outputs.release_id }}" + ZIP_NAME="${{ steps.bump.outputs.zip_name }}" + TOKEN="${{ secrets.GA_TOKEN }}" + API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + echo "Uploading ${ZIP_NAME} to release ${RELEASE_ID}..." + HTTP_CODE=$(curl -sS -o /tmp/upload_response.json -w "%{http_code}" \ + -X POST \ + -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/octet-stream" \ + "${API}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \ + --data-binary "@build/${ZIP_NAME}") + + echo "Upload response (HTTP ${HTTP_CODE}):" + cat /tmp/upload_response.json | jq . 2>/dev/null || cat /tmp/upload_response.json + echo + + if [ "$HTTP_CODE" -ge 400 ]; then + echo "::error::Upload failed (HTTP ${HTTP_CODE})" + exit 1 + fi + echo "Uploaded ${ZIP_NAME}" + + # ── GitHub Mirror (BACKUP) ─────────────────────────────────────── + - name: "GitHub: Mirror release (stable/rc only)" + if: ${{ (steps.meta.outputs.stability == 'stable' || steps.meta.outputs.stability == 'rc') && secrets.GH_TOKEN != '' }} + continue-on-error: true + env: + EXT_ELEMENT: ${{ steps.meta.outputs.ext_element }} + run: | + TAG="${{ steps.meta.outputs.tag_name }}" + VERSION="${{ steps.bump.outputs.version }}" + STABILITY="${{ steps.meta.outputs.stability }}" + ZIP_NAME="${{ steps.bump.outputs.zip_name }}" + SHA256="${{ steps.zip.outputs.sha256 }}" + TOKEN="${{ secrets.GH_TOKEN }}" + GH_REPO="mokoconsulting-tech/${GITEA_REPO}" + GH_API="https://api.github.com/repos/${GH_REPO}" + + echo "=== GitHub Mirror ===" + IS_PRE="true" + [ "$STABILITY" = "stable" ] && IS_PRE="false" + + # Clean up existing + EXISTING=$(curl -sf -H "Authorization: token ${TOKEN}" \ + "${GH_API}/releases/tags/${TAG}" 2>/dev/null | jq -r '.id // empty') + [ -n "$EXISTING" ] && curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${GH_API}/releases/${EXISTING}" || true + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${GH_API}/git/refs/tags/${TAG}" 2>/dev/null || true + + RELEASE_ID=$(curl -sS -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${GH_API}/releases" \ + -d "$(jq -n \ + --arg tag "$TAG" \ + --arg target "${{ github.sha }}" \ + --arg name "${EXT_ELEMENT} ${VERSION} ${STABILITY^} (mirror)" \ + --arg body "Mirror of Gitea release. SHA-256: \`${SHA256}\`" \ + --argjson pre "$IS_PRE" \ + '{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: $pre, draft: false}' + )" | jq -r '.id') + + if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "null" ]; then + curl -sf -X POST \ + -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/octet-stream" \ + "https://uploads.github.com/repos/${GH_REPO}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \ + --data-binary "@build/${ZIP_NAME}" + echo "GitHub mirror uploaded: ${ZIP_NAME}" + fi + + # ── Update updates.xml ────────────────────────────────────────── + - name: "Update updates.xml with SHA and sync to main" + run: | + STABILITY="${{ steps.meta.outputs.stability }}" + VERSION="${{ steps.bump.outputs.version }}" + SHA256="${{ steps.zip.outputs.sha256 }}" + ZIP_NAME="${{ steps.bump.outputs.zip_name }}" + TAG="${{ steps.meta.outputs.tag_name }}" + DATE=$(date +%Y-%m-%d) + BRANCH=$(git branch --show-current) + + echo "=== Update updates.xml ===" + echo "STABILITY=${STABILITY} VERSION=${VERSION} SHA=${SHA256:0:16}..." + + if [ ! -f "updates.xml" ] || [ -z "$SHA256" ]; then + echo "No updates.xml or no SHA — skipping" + exit 0 + fi + + # Cascade map: each stability level updates itself + all lower levels + # stable → all | rc → rc,beta,alpha,dev | beta → beta,alpha,dev | alpha → alpha,dev | dev → dev + case "$STABILITY" in + stable) CASCADE="development,alpha,beta,rc,stable" ;; + rc) CASCADE="development,alpha,beta,rc" ;; + beta) CASCADE="development,alpha,beta" ;; + alpha) CASCADE="development,alpha" ;; + development) CASCADE="development" ;; + *) CASCADE="$STABILITY" ;; + esac + + echo "Cascade: ${STABILITY} → ${CASCADE}" + + export PY_CASCADE="$CASCADE" PY_VERSION="$VERSION" PY_SHA256="$SHA256" \ + PY_ZIP_NAME="$ZIP_NAME" PY_TAG="$TAG" PY_DATE="$DATE" \ + PY_GITEA_ORG="$GITEA_ORG" PY_GITEA_REPO="$GITEA_REPO" + python3 << 'PYEOF' + import re, os + + cascade = os.environ["PY_CASCADE"].split(",") + version = os.environ["PY_VERSION"] + sha256 = os.environ["PY_SHA256"] + zip_name = os.environ["PY_ZIP_NAME"] + tag = os.environ["PY_TAG"] + date = os.environ["PY_DATE"] + gitea_org = os.environ["PY_GITEA_ORG"] + gitea_repo = os.environ["PY_GITEA_REPO"] + + gitea_url = f"https://git.mokoconsulting.tech/{gitea_org}/{gitea_repo}/releases/download/{tag}/{zip_name}" + + with open("updates.xml", "r") as f: + content = f.read() + + for xml_tag in cascade: + xml_tag = xml_tag.strip() + block_pattern = r"((?:(?!).)*?" + re.escape(xml_tag) + r".*?)" + match = re.search(block_pattern, content, re.DOTALL) + + if not match: + print(f" SKIP: no {xml_tag} block found") + continue + + block = match.group(1) + original_block = block + + # Update version and date + block = re.sub(r"[^<]*", f"{version}", block) + block = re.sub(r"[^<]*", f"{date}", block) + + # Set SHA — add if missing, update if present, never leave empty + if "" in block: + block = re.sub(r"[^<]*", f"{sha256}", block) + else: + block = block.replace("", f"\n {sha256}") + + # Update download URL + block = re.sub( + r"(]*>)https://git\.mokoconsulting\.tech/[^<]*()", + rf"\g<1>{gitea_url}\g<2>", + block + ) + + content = content.replace(original_block, block) + print(f" OK: {xml_tag} → version={version}, sha={sha256[:16]}...") + + with open("updates.xml", "w") as f: + f.write(content) + + print(f"Cascaded {len(cascade)} channel(s)") + PYEOF + + # Commit and push + if git diff --quiet updates.xml 2>/dev/null; then + echo "No changes to updates.xml" + exit 0 + fi + + git add updates.xml + git commit -m "chore: update ${STABILITY} SHA-256 for ${VERSION} [skip ci]" \ + --author="gitea-actions[bot] " + echo "Pushing updates.xml to ${BRANCH}..." + git push origin HEAD:${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed" + + # Always sync updates.xml to main via API (Joomla reads from main) + GA_TOKEN="${{ secrets.GA_TOKEN }}" + API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" + + echo "Syncing updates.xml to main via API..." + FILE_SHA=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \ + "${API}/contents/updates.xml?ref=main" | jq -r '.sha // empty') + + if [ -n "$FILE_SHA" ]; then + CONTENT=$(base64 -w0 updates.xml) + HTTP_CODE=$(curl -sS -o /tmp/sync_response.json -w "%{http_code}" \ + -X PUT -H "Authorization: token ${GA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/contents/updates.xml" \ + -d "$(jq -n \ + --arg content "$CONTENT" \ + --arg sha "$FILE_SHA" \ + --arg msg "chore: sync updates.xml ${STABILITY} ${VERSION} [skip ci]" \ + --arg branch "main" \ + '{content: $content, sha: $sha, message: $msg, branch: $branch}' + )") + echo "Sync response (HTTP ${HTTP_CODE}):" + cat /tmp/sync_response.json | jq -r '.content.name // .message // "unknown"' 2>/dev/null + if [ "$HTTP_CODE" -ge 400 ]; then + echo "::warning::Sync to main failed (HTTP ${HTTP_CODE})" + fi + else + echo "::warning::Could not get updates.xml SHA from main" + fi + + - name: Summary + if: always() + run: | + VERSION="${{ steps.bump.outputs.version }}" + STABILITY="${{ steps.meta.outputs.stability }}" + ZIP_NAME="${{ steps.bump.outputs.zip_name }}" + SHA256="${{ steps.zip.outputs.sha256 }}" + TAG="${{ steps.meta.outputs.tag_name }}" + + echo "### Release Created" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Stability | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY + echo "| Tag | \`${TAG}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY + echo "| SHA-256 | \`${SHA256}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Gitea | [Release](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${TAG}) |" >> $GITHUB_STEP_SUMMARY diff --git a/.gitea/workflows/update-server.yml b/.gitea/workflows/update-server.yml new file mode 100644 index 0000000..4b0caa6 --- /dev/null +++ b/.gitea/workflows/update-server.yml @@ -0,0 +1,495 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Joomla +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# PATH: /templates/workflows/joomla/update-server.yml.template +# VERSION: 04.06.00 +# BRIEF: Update Joomla update server XML feed with stable/rc/dev entries +# +# Writes updates.xml with multiple entries: +# - stable on push to main (from auto-release) +# - rc on push to rc/** +# - development on push to dev or dev/** +# +# Joomla filters by user's "Minimum Stability" setting. + +name: Update Joomla Update Server XML Feed + +on: + push: + branches: + - 'dev' + - 'dev/**' + - 'alpha/**' + - 'beta/**' + - 'rc/**' + paths: + - 'src/**' + - 'htdocs/**' + pull_request: + types: [closed] + branches: + - 'dev' + - 'dev/**' + - 'alpha/**' + - 'beta/**' + - 'rc/**' + paths: + - 'src/**' + - 'htdocs/**' + workflow_dispatch: + inputs: + stability: + description: 'Stability tag' + required: true + default: 'development' + type: choice + options: + - development + - alpha + - beta + - rc + - stable + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +permissions: + contents: write + +jobs: + update-xml: + name: Update updates.xml + runs-on: release + if: >- + github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push' + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.GA_TOKEN }} + fetch-depth: 0 + + - name: Setup MokoStandards tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN }}"}}' + run: | + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + 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: Generate updates.xml entry + id: update + run: | + BRANCH="${{ github.ref_name }}" + REPO="${{ github.repository }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "0.0.0") + + # Auto-bump patch on all branches (dev, alpha, beta, rc) + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + BUMPED=$(php /tmp/mokostandards-api/cli/version_bump.php --path . 2>/dev/null || true) + if [ -n "$BUMPED" ]; then + VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "$VERSION") + git add -A + git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \ + --author="gitea-actions[bot] " 2>/dev/null || true + git push 2>/dev/null || true + fi + + # Determine stability from branch or input + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + STABILITY="${{ inputs.stability }}" + elif [[ "$BRANCH" == rc/* ]]; then + STABILITY="rc" + elif [[ "$BRANCH" == beta/* ]]; then + STABILITY="beta" + elif [[ "$BRANCH" == alpha/* ]]; then + STABILITY="alpha" + elif [[ "$BRANCH" == dev/* ]] || [[ "$BRANCH" == "dev" ]]; then + STABILITY="development" + else + STABILITY="stable" + fi + + echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" + + # Parse manifest (portable — no grep -P) + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '/dev/null | head -1) + if [ -z "$MANIFEST" ]; then + echo "No Joomla manifest found — skipping" + exit 0 + fi + + # Extract fields using sed (works on all runners) + EXT_NAME=$(sed -n 's/.*\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1) + EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1) + EXT_CLIENT=$(sed -n 's/.*]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_FOLDER=$(sed -n 's/.*]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_VERSION=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1) + TARGET_PLATFORM=$(sed -n 's/.*\(\).*/\1/p' "$MANIFEST" | head -1) + PHP_MINIMUM=$(sed -n 's/.*\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1) + + # Fallbacks + [ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}" + [ -z "$EXT_TYPE" ] && EXT_TYPE="component" + + # Derive element if not in manifest: try XML filename, then repo name + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') + case "$EXT_ELEMENT" in + templatedetails|manifest|*.xml) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;; + esac + fi + + # Use manifest version if README version is empty + [ "$VERSION" = "0.0.0" ] && [ -n "$EXT_VERSION" ] && VERSION="$EXT_VERSION" + + [ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '' "/") + + CLIENT_TAG="" + [ -n "$EXT_CLIENT" ] && CLIENT_TAG="${EXT_CLIENT}" + [ -z "$CLIENT_TAG" ] && ([ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]) && CLIENT_TAG="site" + + FOLDER_TAG="" + [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ] && FOLDER_TAG="${EXT_FOLDER}" + + PHP_TAG="" + [ -n "$PHP_MINIMUM" ] && PHP_TAG="${PHP_MINIMUM}" + + # Version suffix for non-stable + DISPLAY_VERSION="$VERSION" + case "$STABILITY" in + development) DISPLAY_VERSION="${VERSION}-dev" ;; + alpha) DISPLAY_VERSION="${VERSION}-alpha" ;; + beta) DISPLAY_VERSION="${VERSION}-beta" ;; + rc) DISPLAY_VERSION="${VERSION}-rc" ;; + esac + + MAJOR=$(echo "$VERSION" | awk -F. '{print $1}') + + # Each stability level has its own release tag + case "$STABILITY" in + development) RELEASE_TAG="development" ;; + alpha) RELEASE_TAG="alpha" ;; + beta) RELEASE_TAG="beta" ;; + rc) RELEASE_TAG="release-candidate" ;; + *) RELEASE_TAG="v${MAJOR}" ;; + esac + + PACKAGE_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.zip" + DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}" + INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}" + + # -- Build install packages (ZIP + tar.gz) -------------------- + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + if [ -d "$SOURCE_DIR" ]; then + EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*" + TAR_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.tar.gz" + + cd "$SOURCE_DIR" + zip -r "/tmp/${PACKAGE_NAME}" . -x $EXCLUDES + cd .. + tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \ + --exclude='.ftpignore' --exclude='sftp-config*' \ + --exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' . + + SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1) + + # Ensure release exists on Gitea + RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true) + RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + + if [ -z "$RELEASE_ID" ]; then + # Create release + RELEASE_JSON=$(curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/releases" \ + -d "$(python3 -c "import json; print(json.dumps({ + 'tag_name': '${RELEASE_TAG}', + 'name': '${RELEASE_TAG} (${DISPLAY_VERSION})', + 'body': '${STABILITY} release', + 'prerelease': True, + 'target_commitish': 'main' + }))")" 2>/dev/null || true) + RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + fi + + if [ -n "$RELEASE_ID" ]; then + # Delete existing assets with same name before uploading + ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]") + for ASSET_FILE in "$PACKAGE_NAME" "$TAR_NAME"; do + ASSET_ID=$(echo "$ASSETS" | python3 -c " + import sys,json + assets = json.load(sys.stdin) + for a in assets: + if a['name'] == '${ASSET_FILE}': + print(a['id']); break + " 2>/dev/null || true) + if [ -n "$ASSET_ID" ]; then + curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true + fi + done + + # Upload both formats + curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @"/tmp/${PACKAGE_NAME}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets?name=${PACKAGE_NAME}" > /dev/null 2>&1 || true + + curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @"/tmp/${TAR_NAME}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets?name=${TAR_NAME}" > /dev/null 2>&1 || true + fi + + echo "Packages: ${PACKAGE_NAME} + ${TAR_NAME} (SHA: ${SHA256})" >> $GITHUB_STEP_SUMMARY + else + SHA256="" + fi + + # -- Build the new entry ----------------------------------------- + NEW_ENTRY="" + NEW_ENTRY="${NEW_ENTRY} \n" + NEW_ENTRY="${NEW_ENTRY} ${EXT_NAME}\n" + NEW_ENTRY="${NEW_ENTRY} ${EXT_NAME} (${STABILITY})\n" + NEW_ENTRY="${NEW_ENTRY} ${EXT_ELEMENT}\n" + NEW_ENTRY="${NEW_ENTRY} ${EXT_TYPE}\n" + NEW_ENTRY="${NEW_ENTRY} ${DISPLAY_VERSION}\n" + NEW_ENTRY="${NEW_ENTRY} $(date +%Y-%m-%d)\n" + [ -n "$CLIENT_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${CLIENT_TAG}\n" + [ -n "$FOLDER_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${FOLDER_TAG}\n" + NEW_ENTRY="${NEW_ENTRY} \n" + NEW_ENTRY="${NEW_ENTRY} ${STABILITY}\n" + NEW_ENTRY="${NEW_ENTRY} \n" + NEW_ENTRY="${NEW_ENTRY} ${INFO_URL}\n" + NEW_ENTRY="${NEW_ENTRY} \n" + NEW_ENTRY="${NEW_ENTRY} ${DOWNLOAD_URL}\n" + NEW_ENTRY="${NEW_ENTRY} \n" + [ -n "$SHA256" ] && NEW_ENTRY="${NEW_ENTRY} ${SHA256}\n" + NEW_ENTRY="${NEW_ENTRY} ${TARGET_PLATFORM}\n" + [ -n "$PHP_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${PHP_TAG}\n" + NEW_ENTRY="${NEW_ENTRY} Moko Consulting\n" + NEW_ENTRY="${NEW_ENTRY} https://mokoconsulting.tech\n" + NEW_ENTRY="${NEW_ENTRY} " + + # -- Write new entry to temp file -------------------------------- + printf '%b' "$NEW_ENTRY" > /tmp/new_entry.xml + + # -- Merge into updates.xml (only update this stability channel) - + # Cascading update: each stability level updates itself and all lower levels + # stable → all | rc → rc,beta,alpha,dev | beta → beta,alpha,dev | alpha → alpha,dev | dev → dev + CASCADE_MAP="stable:development,alpha,beta,rc,stable rc:development,alpha,beta,rc beta:development,alpha,beta alpha:development,alpha development:development" + TARGETS="" + for entry in $CASCADE_MAP; do + key="${entry%%:*}" + vals="${entry#*:}" + if [ "$key" = "${STABILITY}" ]; then + TARGETS="$vals" + break + fi + done + [ -z "$TARGETS" ] && TARGETS="${STABILITY}" + + if [ ! -f "updates.xml" ]; then + printf '%s\n' "" > updates.xml + printf '%s\n' "" >> updates.xml + printf '%s\n' "" >> updates.xml + printf '%s\n' '' >> updates.xml + cat /tmp/new_entry.xml >> updates.xml + printf '\n%s\n' '' >> updates.xml + else + # Replace each cascading channel with the new entry (different tag) + export PY_TARGETS="$TARGETS" + python3 << PYEOF + import re, os + targets = os.environ["PY_TARGETS"].split(",") + stability = "${STABILITY}" + with open("updates.xml") as f: + content = f.read() + with open("/tmp/new_entry.xml") as f: + new_entry_template = f.read() + for tag in targets: + tag = tag.strip() + # Build entry with this tag + new_entry = re.sub(r"[^<]*", f"{tag}", new_entry_template) + # Remove existing entry for this tag + pattern = r" .*?" + re.escape(tag) + r".*?\n?" + content = re.sub(pattern, "", content, flags=re.DOTALL) + # Insert before + content = content.replace("", new_entry + "\n") + content = re.sub(r"\n{3,}", "\n\n", content) + with open("updates.xml", "w") as f: + f.write(content) + PYEOF + if [ $? -ne 0 ]; then + # Fallback: rebuild keeping other stability entries + { + printf '%s\n' "" + printf '%s\n' "" + printf '%s\n' "" + printf '%s\n' '' + for TAG in stable rc development; do + [ "$TAG" = "${STABILITY}" ] && continue + if grep -q "${TAG}" updates.xml 2>/dev/null; then + sed -n "//,/<\/update>/{ /${TAG}<\/tag>/p; }" updates.xml + fi + done + cat /tmp/new_entry.xml + printf '\n%s\n' '' + } > /tmp/updates_new.xml + mv /tmp/updates_new.xml updates.xml + fi + fi + + # Commit + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git add updates.xml + git diff --cached --quiet || { + git commit -m "chore: update updates.xml (${STABILITY}: ${DISPLAY_VERSION}) [skip ci]" \ + --author="gitea-actions[bot] " + git push + } + + # -- Sync updates.xml to main (for non-main branches) ---------------------- + - name: Sync updates.xml to main + if: github.ref_name != 'main' + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + GA_TOKEN="${{ secrets.GA_TOKEN }}" + + FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \ + "${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true) + + if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then + CONTENT=$(base64 -w0 updates.xml) + curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/contents/updates.xml" \ + -d "$(python3 -c "import json; print(json.dumps({ + 'content': '${CONTENT}', + 'sha': '${FILE_SHA}', + 'message': 'chore: sync updates.xml from ${STABILITY} [skip ci]', + 'branch': 'main' + }))")" > /dev/null 2>&1 \ + && echo "updates.xml synced to main (${STABILITY})" >> $GITHUB_STEP_SUMMARY \ + || echo "WARNING: failed to sync updates.xml to main" >> $GITHUB_STEP_SUMMARY + else + echo "WARNING: could not get updates.xml SHA from main" >> $GITHUB_STEP_SUMMARY + fi + + # -- Mirror to GitHub (stable and rc only) -------------------------------- + - name: Mirror release to GitHub + if: >- + (steps.update.outputs.stability == 'stable' || steps.update.outputs.stability == 'rc') && + secrets.GH_TOKEN != '' + continue-on-error: true + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + run: | + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + STABILITY="${{ steps.update.outputs.stability }}" + echo "GitHub mirror sync for ${STABILITY} — ${GH_REPO}" >> $GITHUB_STEP_SUMMARY + # Mirror packages if they exist + for PKG in /tmp/*.zip /tmp/*.tar.gz; do + if [ -f "$PKG" ]; then + _RELID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/${RELEASE_TAG}" 2>/dev/null | jq -r ".id // empty") + [ -n "$_RELID" ] && curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" -H "Content-Type: application/octet-stream" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/${_RELID}/assets?name=$(basename $PKG)" --data-binary "@$PKG" > /dev/null 2>&1 || true + fi + done + + - name: SFTP deploy to dev server + if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev' + env: + DEV_HOST: ${{ vars.DEV_FTP_HOST }} + DEV_PATH: ${{ vars.DEV_FTP_PATH }} + DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }} + DEV_USER: ${{ vars.DEV_FTP_USERNAME }} + DEV_PORT: ${{ vars.DEV_FTP_PORT }} + DEV_KEY: ${{ secrets.DEV_FTP_KEY }} + DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }} + run: | + # -- Permission check: admin or maintain role required -------- + ACTOR="${{ github.actor }}" + REPO="${{ github.repository }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \ + python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read") + case "$PERMISSION" in + admin|maintain|write) ;; + *) + echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write" + exit 0 + ;; + esac + + [ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; } + + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + [ ! -d "$SOURCE_DIR" ] && exit 0 + + PORT="${DEV_PORT:-22}" + REMOTE="${DEV_PATH%/}" + [ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}" + + printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \ + "$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json + if [ -n "$DEV_KEY" ]; then + echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key + printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json + else + printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json + fi + + PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true) + if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then + php /tmp/mokostandards-api/deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json + elif [ -f "/tmp/mokostandards-api/deploy/deploy-sftp.php" ]; then + php /tmp/mokostandards-api/deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json + fi + rm -f /tmp/deploy_key /tmp/sftp-config.json + echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY + + - name: Summary + if: always() + run: | + echo "## Joomla Update Server" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${DISPLAY_VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Element | \`${EXT_ELEMENT}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Download | [ZIP](${DOWNLOAD_URL}) |" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/auto-assign.yml b/.github/workflows/auto-assign.yml deleted file mode 100644 index d0b70f6..0000000 --- a/.github/workflows/auto-assign.yml +++ /dev/null @@ -1,76 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: GitHub.Workflow -# INGROUP: MokoStandards.Workflows.Shared -# REPO: https://github.com/mokoconsulting-tech/MokoStandards -# PATH: /.github/workflows/auto-assign.yml -# VERSION: 04.06.00 -# BRIEF: Auto-assign jmiller-moko to unassigned issues and PRs every 15 minutes - -name: Auto-Assign Issues & PRs - -on: - issues: - types: [opened] - pull_request_target: - types: [opened] - schedule: - - cron: '0 */12 * * *' - workflow_dispatch: - -permissions: - issues: write - pull-requests: write - -jobs: - auto-assign: - name: Assign unassigned issues and PRs - runs-on: ubuntu-latest - - steps: - - name: Assign unassigned issues - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - run: | - REPO="${{ github.repository }}" - ASSIGNEE="jmiller-moko" - - echo "## 🏷️ Auto-Assign Report" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - ASSIGNED_ISSUES=0 - ASSIGNED_PRS=0 - - # Assign unassigned open issues - ISSUES=$(gh api "repos/$REPO/issues?state=open&per_page=100&assignee=none" --jq '.[].number' 2>/dev/null || true) - for NUM in $ISSUES; do - # Skip PRs (the issues endpoint returns PRs too) - IS_PR=$(gh api "repos/$REPO/issues/$NUM" --jq '.pull_request // empty' 2>/dev/null || true) - if [ -z "$IS_PR" ]; then - gh api "repos/$REPO/issues/$NUM/assignees" -X POST -f "assignees[]=$ASSIGNEE" --silent 2>/dev/null && { - ASSIGNED_ISSUES=$((ASSIGNED_ISSUES + 1)) - echo " Assigned issue #$NUM" - } || true - fi - done - - # Assign unassigned open PRs - PRS=$(gh api "repos/$REPO/pulls?state=open&per_page=100" --jq '.[] | select(.assignees | length == 0) | .number' 2>/dev/null || true) - for NUM in $PRS; do - gh api "repos/$REPO/issues/$NUM/assignees" -X POST -f "assignees[]=$ASSIGNEE" --silent 2>/dev/null && { - ASSIGNED_PRS=$((ASSIGNED_PRS + 1)) - echo " Assigned PR #$NUM" - } || true - done - - echo "| Type | Assigned |" >> $GITHUB_STEP_SUMMARY - echo "|------|----------|" >> $GITHUB_STEP_SUMMARY - echo "| Issues | $ASSIGNED_ISSUES |" >> $GITHUB_STEP_SUMMARY - echo "| Pull Requests | $ASSIGNED_PRS |" >> $GITHUB_STEP_SUMMARY - - if [ "$ASSIGNED_ISSUES" -eq 0 ] && [ "$ASSIGNED_PRS" -eq 0 ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "✅ All issues and PRs already have assignees" >> $GITHUB_STEP_SUMMARY - fi diff --git a/.github/workflows/auto-dev-issue.yml b/.github/workflows/auto-dev-issue.yml deleted file mode 100644 index 9b5fbe2..0000000 --- a/.github/workflows/auto-dev-issue.yml +++ /dev/null @@ -1,207 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# This file is part of a Moko Consulting project. -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: GitHub.Workflow -# INGROUP: MokoStandards.Automation -# REPO: https://github.com/mokoconsulting-tech/MokoStandards -# PATH: /templates/workflows/shared/auto-dev-issue.yml.template -# VERSION: 04.06.00 -# BRIEF: Auto-create tracking issue with sub-issues for dev/rc branch workflow -# NOTE: Synced via bulk-repo-sync to .github/workflows/auto-dev-issue.yml in all governed repos. - -name: Dev/RC Branch Issue - -on: - # Auto-create on RC branch creation - create: - # Manual trigger for dev branches - workflow_dispatch: - inputs: - branch: - description: 'Branch name (e.g., dev/my-feature or dev/04.06)' - required: true - type: string - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -permissions: - contents: read - issues: write - -jobs: - create-issue: - name: Create version tracking issue - runs-on: ubuntu-latest - if: >- - (github.event_name == 'workflow_dispatch') || - (github.event.ref_type == 'branch' && - (startsWith(github.event.ref, 'rc/') || - startsWith(github.event.ref, 'alpha/') || - startsWith(github.event.ref, 'beta/'))) - - steps: - - name: Create tracking issue and sub-issues - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - run: | - # For manual dispatch, use input; for auto, use event ref - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - BRANCH="${{ inputs.branch }}" - else - BRANCH="${{ github.event.ref }}" - fi - REPO="${{ github.repository }}" - ACTOR="${{ github.actor }}" - NOW=$(date -u '+%Y-%m-%d %H:%M UTC') - - # Determine branch type and version - if [[ "$BRANCH" == rc/* ]]; then - VERSION="${BRANCH#rc/}" - BRANCH_TYPE="Release Candidate" - LABEL_TYPE="type: release" - TITLE_PREFIX="rc" - elif [[ "$BRANCH" == beta/* ]]; then - VERSION="${BRANCH#beta/}" - BRANCH_TYPE="Beta" - LABEL_TYPE="type: release" - TITLE_PREFIX="beta" - elif [[ "$BRANCH" == alpha/* ]]; then - VERSION="${BRANCH#alpha/}" - BRANCH_TYPE="Alpha" - LABEL_TYPE="type: release" - TITLE_PREFIX="alpha" - else - VERSION="${BRANCH#dev/}" - BRANCH_TYPE="Development" - LABEL_TYPE="type: feature" - TITLE_PREFIX="feat" - fi - - TITLE="${TITLE_PREFIX}(${VERSION}): ${BRANCH_TYPE} tracking for ${BRANCH}" - - # Check for existing issue with same title prefix - EXISTING=$(gh api "repos/${REPO}/issues?state=open&per_page=10" \ - --jq ".[] | select(.title | startswith(\"${TITLE_PREFIX}(${VERSION})\")) | .number" 2>/dev/null | head -1) - - if [ -n "$EXISTING" ]; then - echo "ℹ️ Issue #${EXISTING} already exists for ${VERSION}" >> $GITHUB_STEP_SUMMARY - exit 0 - fi - - # ── Define sub-issues for the workflow ───────────────────────── - if [[ "$BRANCH" == rc/* ]]; then - SUB_ISSUES=( - "RC Testing|Verify all features work on rc branch|type: test,release-candidate" - "Regression Testing|Run full regression suite before merge|type: test,release-candidate" - "Version Bump|Bump version in README.md and all headers|type: version,release-candidate" - "Changelog Update|Update CHANGELOG.md with release notes|documentation,release-candidate" - "Merge to Version Branch|Create PR to version/XX|type: release,needs-review" - ) - elif [[ "$BRANCH" == alpha/* ]] || [[ "$BRANCH" == beta/* ]]; then - SUB_ISSUES=( - "Testing|Verify features on ${BRANCH_TYPE} branch|type: test,status: in-progress" - "Bug Fixes|Fix issues found during ${BRANCH_TYPE} testing|type: bug,status: pending" - "Promote to Next Stage|Create PR to promote to next release stage|type: release,needs-review" - ) - else - SUB_ISSUES=( - "Development|Implement feature/fix on dev branch|type: feature,status: in-progress" - "Unit Testing|Write and pass unit tests|type: test,status: pending" - "Code Review|Request and complete code review|needs-review,status: pending" - "Version Bump|Bump version in README.md and all headers|type: version,status: pending" - "Changelog Update|Update CHANGELOG.md with release notes|documentation,status: pending" - "Create RC Branch|Promote dev to rc branch for final testing|type: release,status: pending" - "Merge to Main|Create PR from rc/dev to main|type: release,needs-review,status: pending" - ) - fi - - # ── Create sub-issues first ─────────────────────────────────────── - SUB_LIST="" - SUB_NUMBERS="" - for SUB in "${SUB_ISSUES[@]}"; do - IFS='|' read -r SUB_TITLE SUB_DESC SUB_LABELS <<< "$SUB" - SUB_FULL_TITLE="${TITLE_PREFIX}(${VERSION}): ${SUB_TITLE}" - - SUB_BODY=$(printf '### %s\n\n%s\n\n| Field | Value |\n|-------|-------|\n| **Parent Branch** | `%s` |\n| **Version** | `%s` |\n\n---\n*Sub-issue of the %s tracking issue for `%s`.*' \ - "$SUB_TITLE" "$SUB_DESC" "$BRANCH" "$VERSION" "$BRANCH_TYPE" "$BRANCH") - - SUB_URL=$(gh issue create \ - --repo "$REPO" \ - --title "$SUB_FULL_TITLE" \ - --body "$SUB_BODY" \ - --label "${SUB_LABELS}" \ - --assignee "jmiller-moko" 2>&1) - - SUB_NUM=$(echo "$SUB_URL" | grep -oE '[0-9]+$') - if [ -n "$SUB_NUM" ]; then - SUB_LIST="${SUB_LIST}\n- [ ] ${SUB_TITLE} (#${SUB_NUM})" - SUB_NUMBERS="${SUB_NUMBERS} #${SUB_NUM}" - fi - sleep 0.3 - done - - # ── Create parent tracking issue ────────────────────────────────── - PARENT_BODY=$(printf '## %s Branch Created\n\n| Field | Value |\n|-------|-------|\n| **Branch** | `%s` |\n| **Version** | `%s` |\n| **Type** | %s |\n| **Created by** | @%s |\n| **Created at** | %s |\n| **Repository** | `%s` |\n\n## Workflow Sub-Issues\n\n%b\n\n---\n*Auto-created by [auto-dev-issue.yml](.github/workflows/auto-dev-issue.yml) on branch creation.*' \ - "$BRANCH_TYPE" "$BRANCH" "$VERSION" "$BRANCH_TYPE" "$ACTOR" "$NOW" "$REPO" "$SUB_LIST") - - PARENT_URL=$(gh issue create \ - --repo "$REPO" \ - --title "$TITLE" \ - --body "$PARENT_BODY" \ - --label "${LABEL_TYPE},version" \ - --assignee "jmiller-moko" 2>&1) - - PARENT_NUM=$(echo "$PARENT_URL" | grep -oE '[0-9]+$') - - # ── Link sub-issues back to parent ──────────────────────────────── - if [ -n "$PARENT_NUM" ]; then - for SUB in "${SUB_ISSUES[@]}"; do - IFS='|' read -r SUB_TITLE _ _ <<< "$SUB" - SUB_FULL_TITLE="${TITLE_PREFIX}(${VERSION}): ${SUB_TITLE}" - SUB_NUM=$(gh api "repos/${REPO}/issues?state=open&per_page=20" \ - --jq ".[] | select(.title == \"${SUB_FULL_TITLE}\") | .number" 2>/dev/null | head -1) - if [ -n "$SUB_NUM" ]; then - gh api "repos/${REPO}/issues/${SUB_NUM}" -X PATCH \ - -f body="$(gh api "repos/${REPO}/issues/${SUB_NUM}" --jq '.body' 2>/dev/null) - - > **Parent Issue:** #${PARENT_NUM}" --silent 2>/dev/null || true - fi - sleep 0.2 - done - fi - - # ── Create or update prerelease for alpha/beta/rc ──────────────── - if [[ "$BRANCH" == rc/* ]] || [[ "$BRANCH" == alpha/* ]] || [[ "$BRANCH" == beta/* ]]; then - case "$BRANCH_TYPE" in - Alpha) RELEASE_TAG="alpha" ;; - Beta) RELEASE_TAG="beta" ;; - "Release Candidate") RELEASE_TAG="release-candidate" ;; - esac - - EXISTING=$(gh release view "$RELEASE_TAG" --json tagName -q .tagName 2>/dev/null || true) - if [ -z "$EXISTING" ]; then - gh release create "$RELEASE_TAG" \ - --title "${RELEASE_TAG} (${VERSION})" \ - --notes "## ${BRANCH_TYPE} ${VERSION}\n\nBranch: \`${BRANCH}\`\nTracking issue: ${PARENT_URL}" \ - --prerelease \ - --target main 2>/dev/null || true - echo "${BRANCH_TYPE} release created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY - else - gh release edit "$RELEASE_TAG" \ - --title "${RELEASE_TAG} (${VERSION})" --prerelease 2>/dev/null || true - echo "${BRANCH_TYPE} release updated: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY - fi - fi - - # ── Summary ─────────────────────────────────────────────────────── - echo "## Dev Workflow Issues Created" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Item | Issue |" >> $GITHUB_STEP_SUMMARY - echo "|------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| **Parent** | ${PARENT_URL} |" >> $GITHUB_STEP_SUMMARY - echo "| **Sub-issues** |${SUB_NUMBERS} |" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml deleted file mode 100644 index eabe619..0000000 --- a/.github/workflows/auto-release.yml +++ /dev/null @@ -1,337 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: GitHub.Workflow -# INGROUP: MokoStandards.Release -# REPO: https://github.com/mokoconsulting-tech/MokoStandards -# PATH: /templates/workflows/shared/auto-release.yml.template -# VERSION: 04.06.00 -# BRIEF: Generic build & release pipeline — version branch, platform version, badges, tag, release -# -# +========================================================================+ -# | BUILD & RELEASE PIPELINE | -# +========================================================================+ -# | | -# | Triggers on push to main (skips bot commits + [skip ci]): | -# | | -# | Every push: | -# | 1. Read version from README.md | -# | 3. Set platform version | -# | 4. Update [VERSION: XX.YY.ZZ] badges in markdown files | -# | 6. Create git tag vXX.YY.ZZ | -# | 7a. Patch: update existing GitHub Release for this minor | -# | | -# | Every version change: archives main -> version/XX.YY branch | -# | Patch 00 = development (no release). First release = patch 01. | -# | First release only (patch == 01): | -# | 7b. Create new GitHub Release | -# | | -# +========================================================================+ - -name: Build & Release - -on: - push: - branches: - - main - - master - paths: - - 'src/**' - - 'htdocs/**' - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -permissions: - contents: write - -jobs: - release: - name: Build & Release Pipeline - runs-on: ubuntu-latest - if: >- - !contains(github.event.head_commit.message, '[skip ci]') && - github.actor != 'github-actions[bot]' - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.GH_TOKEN || github.token }} - fetch-depth: 0 - - - name: Setup MokoStandards tools - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' - run: | - git clone --depth 1 --branch version/04 --quiet \ - "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ - /tmp/mokostandards - cd /tmp/mokostandards - composer install --no-dev --no-interaction --quiet - - # -- STEP 1: Read version ----------------------------------------------- - - name: "Step 1: Read version from README.md" - id: version - run: | - VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null) - if [ -z "$VERSION" ]; then - echo "No VERSION in README.md — skipping release" - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - # Derive major.minor for branch naming (patches update existing branch) - MINOR=$(echo "$VERSION" | awk -F. '{printf "%s.%s", $1, $2}') - PATCH=$(echo "$VERSION" | awk -F. '{print $3}') - - MAJOR=$(echo "$VERSION" | awk -F. '{print $1}') - MINOR_NUM=$(echo "$VERSION" | awk -F. '{print $2}') - - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT" - echo "minor=$MINOR" >> "$GITHUB_OUTPUT" - echo "major=$MAJOR" >> "$GITHUB_OUTPUT" - echo "release_tag=v${MAJOR}" >> "$GITHUB_OUTPUT" - if [ "$PATCH" = "00" ]; then - echo "skip=true" >> "$GITHUB_OUTPUT" - echo "is_minor=false" >> "$GITHUB_OUTPUT" - echo "Version: $VERSION (patch 00 = development — skipping release)" - else - echo "skip=false" >> "$GITHUB_OUTPUT" - if [ "$PATCH" = "01" ]; then - echo "is_minor=true" >> "$GITHUB_OUTPUT" - echo "Version: $VERSION (first release — full pipeline)" - else - echo "is_minor=false" >> "$GITHUB_OUTPUT" - echo "Version: $VERSION (patch — platform version + badges only)" - fi - fi - - - name: Check if already released - if: steps.version.outputs.skip != 'true' - id: check - run: | - TAG="${{ steps.version.outputs.release_tag }}" - BRANCH="${{ steps.version.outputs.branch }}" - - TAG_EXISTS=false - BRANCH_EXISTS=false - - git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true - git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH" && BRANCH_EXISTS=true - - echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT" - echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT" - - if [ "$TAG_EXISTS" = "true" ] && [ "$BRANCH_EXISTS" = "true" ]; then - echo "already_released=true" >> "$GITHUB_OUTPUT" - else - echo "already_released=false" >> "$GITHUB_OUTPUT" - fi - - # -- SANITY CHECKS ------------------------------------------------------- - - name: "Sanity: Pre-release validation" - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' - run: | - VERSION="${{ steps.version.outputs.version }}" - ERRORS=0 - - echo "## Pre-Release Sanity Checks" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # -- Version drift check (must pass before release) -------- - README_VER=$(grep -oP 'VERSION:\s*\K[\d.]+' README.md 2>/dev/null | head -1) - if [ "$README_VER" != "$VERSION" ]; then - echo "- Version drift: README says \`${README_VER}\` but releasing \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS+1)) - else - echo "- Version consistent: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY - fi - - # Check CHANGELOG version matches - CL_VER=$(grep -oP 'VERSION:\s*\K[\d.]+' CHANGELOG.md 2>/dev/null | head -1) - if [ -n "$CL_VER" ] && [ "$CL_VER" != "$VERSION" ]; then - echo "- CHANGELOG drift: \`${CL_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS+1)) - fi - - # Check composer.json version if present - if [ -f "composer.json" ]; then - COMP_VER=$(grep -oP '"version"\s*:\s*"\K[^"]+' composer.json 2>/dev/null | head -1) - if [ -n "$COMP_VER" ] && [ "$COMP_VER" != "$VERSION" ]; then - echo "- composer.json drift: \`${COMP_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS+1)) - fi - fi - - # Common checks - if [ ! -f "LICENSE" ]; then - echo "- Missing LICENSE file" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS+1)) - else - echo "- LICENSE present" >> $GITHUB_STEP_SUMMARY - fi - - if [ ! -d "src" ] && [ ! -d "htdocs" ]; then - echo "- Warning: No src/ or htdocs/ directory" >> $GITHUB_STEP_SUMMARY - else - echo "- Source directory present" >> $GITHUB_STEP_SUMMARY - fi - - echo "" >> $GITHUB_STEP_SUMMARY - if [ "$ERRORS" -gt 0 ]; then - echo "**${ERRORS} error(s) — release may be incomplete**" >> $GITHUB_STEP_SUMMARY - else - echo "**All sanity checks passed**" >> $GITHUB_STEP_SUMMARY - fi - - # -- STEP 2: Create or update version/XX.YY archive branch --------------- - # Always runs — every version change on main archives to version/XX.YY - - name: "Step 2: Version archive branch" - if: steps.check.outputs.already_released != 'true' - run: | - BRANCH="${{ steps.version.outputs.branch }}" - IS_MINOR="${{ steps.version.outputs.is_minor }}" - PATCH="${{ steps.version.outputs.version }}" - PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}') - - # Check if branch exists - if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then - git push origin HEAD:"$BRANCH" --force - echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY - else - git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH" - git push origin "$BRANCH" --force - echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY - fi - - # -- STEP 3: Set platform version ---------------------------------------- - - name: "Step 3: Set platform version" - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' - run: | - VERSION="${{ steps.version.outputs.version }}" - php /tmp/mokostandards/api/cli/version_set_platform.php \ - --path . --version "$VERSION" --branch main - - # -- STEP 4: Update version badges ---------------------------------------- - - name: "Step 4: Update version badges" - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' - run: | - VERSION="${{ steps.version.outputs.version }}" - find . -name "*.md" ! -path "./.git/*" ! -path "./vendor/*" | while read -r f; do - if grep -q '\[VERSION:' "$f" 2>/dev/null; then - sed -i "s/\[VERSION:[[:space:]]*[0-9]\{2\}\.[0-9]\{2\}\.[0-9]\{2\}\]/[VERSION: ${VERSION}]/" "$f" - fi - done - - # -- Commit all changes --------------------------------------------------- - - name: Commit release changes - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' - run: | - if git diff --quiet && git diff --cached --quiet; then - echo "No changes to commit" - exit 0 - fi - VERSION="${{ steps.version.outputs.version }}" - git config --local user.email "github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" - git add -A - git commit -m "chore(release): build ${VERSION} [skip ci]" \ - --author="github-actions[bot] " - git push - - # -- STEP 6: Create tag --------------------------------------------------- - - name: "Step 6: Create git tag" - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.tag_exists != 'true' && - steps.version.outputs.is_minor == 'true' - run: | - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - # Only create the major release tag if it doesn't exist yet - if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then - git tag "$RELEASE_TAG" - git push origin "$RELEASE_TAG" - echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY - else - echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY - fi - echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY - - # -- STEP 7: Create or update GitHub Release ------------------------------ - - name: "Step 7: GitHub Release" - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.tag_exists != 'true' - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - run: | - VERSION="${{ steps.version.outputs.version }}" - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - BRANCH="${{ steps.version.outputs.branch }}" - MAJOR="${{ steps.version.outputs.major }}" - - NOTES=$(php /tmp/mokostandards/api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null) - [ -z "$NOTES" ] && NOTES="Release ${VERSION}" - echo "$NOTES" > /tmp/release_notes.md - - # Check if the major release already exists - EXISTING=$(gh release view "$RELEASE_TAG" --json tagName -q .tagName 2>/dev/null || true) - - if [ -z "$EXISTING" ]; then - # First release for this major: create GitHub Release - gh release create "$RELEASE_TAG" \ - --title "v${MAJOR} (latest: ${VERSION})" \ - --notes-file /tmp/release_notes.md \ - --target "$BRANCH" - echo "Release created: ${RELEASE_TAG} (${VERSION})" >> $GITHUB_STEP_SUMMARY - else - # Update existing major release with new version info - CURRENT_NOTES=$(gh release view "$RELEASE_TAG" --json body -q .body 2>/dev/null || true) - { - echo "$CURRENT_NOTES" - echo "" - echo "---" - echo "### ${VERSION}" - echo "" - cat /tmp/release_notes.md - } > /tmp/updated_notes.md - - gh release edit "$RELEASE_TAG" \ - --title "v${MAJOR} (latest: ${VERSION})" \ - --notes-file /tmp/updated_notes.md - echo "Release updated: ${RELEASE_TAG} -> ${VERSION}" >> $GITHUB_STEP_SUMMARY - fi - - # -- Summary -------------------------------------------------------------- - - name: Pipeline Summary - if: always() - run: | - VERSION="${{ steps.version.outputs.version }}" - if [ "${{ steps.version.outputs.skip }}" = "true" ]; then - echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY - echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY - elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then - echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY - else - echo "" >> $GITHUB_STEP_SUMMARY - echo "## Build & Release Complete" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY - echo "|------|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Release | [View](https://github.com/${{ github.repository }}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY - fi diff --git a/.github/workflows/changelog-validation.yml b/.github/workflows/changelog-validation.yml deleted file mode 100644 index e2ec667..0000000 --- a/.github/workflows/changelog-validation.yml +++ /dev/null @@ -1,101 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# This file is part of a Moko Consulting project. -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: GitHub.Workflow.Template -# INGROUP: MokoStandards.CI -# REPO: https://github.com/mokoconsulting-tech/MokoStandards -# PATH: /templates/workflows/shared/changelog-validation.yml.template -# VERSION: 04.06.00 -# BRIEF: Validates CHANGELOG.md format and version consistency -# NOTE: Deployed to .github/workflows/changelog-validation.yml in governed repos. - -name: Changelog Validation - -on: - push: - branches: - - main - pull_request: - branches: - - main - workflow_dispatch: - -permissions: - contents: read - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - validate-changelog: - name: Validate CHANGELOG.md - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Check CHANGELOG.md exists - run: | - echo "### Changelog Validation" >> $GITHUB_STEP_SUMMARY - if [ ! -f "CHANGELOG.md" ]; then - echo "CHANGELOG.md not found in repository root." >> $GITHUB_STEP_SUMMARY - exit 1 - fi - echo "CHANGELOG.md exists." >> $GITHUB_STEP_SUMMARY - - - name: Check VERSION header matches README.md - run: | - # Extract version from README.md FILE INFORMATION block - README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md | head -1) - if [ -z "$README_VERSION" ]; then - echo "No VERSION found in README.md FILE INFORMATION block." >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - # Check that CHANGELOG.md has a matching version header - CHANGELOG_VERSION=$(grep -oP '^\#\#\s*\[\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' CHANGELOG.md | head -1) - if [ -z "$CHANGELOG_VERSION" ]; then - echo "No version header found in CHANGELOG.md (expected \`## [XX.YY.ZZ] - YYYY-MM-DD\`)." >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - if [ "$CHANGELOG_VERSION" != "$README_VERSION" ]; then - echo "CHANGELOG latest version \`${CHANGELOG_VERSION}\` does not match README VERSION \`${README_VERSION}\`." >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - echo "CHANGELOG version \`${CHANGELOG_VERSION}\` matches README VERSION." >> $GITHUB_STEP_SUMMARY - - - name: Validate conventional changelog format - run: | - ERRORS=0 - - # Check that version entries follow ## [XX.YY.ZZ] - YYYY-MM-DD format - while IFS= read -r LINE; do - if ! echo "$LINE" | grep -qP '^\#\#\s*\[[0-9]{2}\.[0-9]{2}\.[0-9]{2}\]\s*-\s*[0-9]{4}-[0-9]{2}-[0-9]{2}'; then - echo "Malformed version header: \`${LINE}\`" >> $GITHUB_STEP_SUMMARY - echo " Expected format: \`## [XX.YY.ZZ] - YYYY-MM-DD\`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - fi - done < <(grep -P '^\#\#\s*\[' CHANGELOG.md) - - ENTRY_COUNT=$(grep -cP '^\#\#\s*\[' CHANGELOG.md || echo "0") - if [ "$ENTRY_COUNT" -eq 0 ]; then - echo "No version entries found in CHANGELOG.md." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - echo "Found ${ENTRY_COUNT} version entr(ies) in CHANGELOG.md." >> $GITHUB_STEP_SUMMARY - fi - - echo "" >> $GITHUB_STEP_SUMMARY - if [ "${ERRORS}" -gt 0 ]; then - echo "**${ERRORS} format issue(s) found.**" >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "**Changelog format validation passed.**" >> $GITHUB_STEP_SUMMARY - fi diff --git a/.github/workflows/ci-joomla.yml b/.github/workflows/ci-joomla.yml deleted file mode 100644 index 01e1edb..0000000 --- a/.github/workflows/ci-joomla.yml +++ /dev/null @@ -1,391 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# This file is part of a Moko Consulting project. -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: GitHub.Workflow.Template -# INGROUP: MokoStandards.CI -# REPO: https://github.com/mokoconsulting-tech/MokoStandards -# PATH: /templates/workflows/joomla/ci-joomla.yml.template -# VERSION: 04.06.00 -# BRIEF: CI workflow for Joomla extensions — lint, validate, test -# NOTE: Deployed to .github/workflows/ci-joomla.yml in governed Joomla extension repos. - -name: Joomla Extension CI - -on: - push: - branches: - - main - - dev/** - - rc/** - - version/** - pull_request: - branches: - - main - - dev/** - - rc/** - workflow_dispatch: - -permissions: - contents: read - pull-requests: write - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - lint-and-validate: - name: Lint & Validate - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Setup PHP - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 - with: - php-version: '8.2' - extensions: mbstring, xml, zip, gd, curl, json, simplexml - tools: composer:v2 - coverage: none - - - name: Clone MokoStandards - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - run: | - git clone --depth 1 --branch version/04 --quiet \ - "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ - /tmp/mokostandards - - - name: Install dependencies - env: - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' - run: | - if [ -f "composer.json" ]; then - composer install \ - --no-interaction \ - --prefer-dist \ - --optimize-autoloader - else - echo "No composer.json found — skipping dependency install" - fi - - - name: PHP syntax check - run: | - ERRORS=0 - for DIR in src/ htdocs/; do - if [ -d "$DIR" ]; then - FOUND=1 - while IFS= read -r -d '' FILE; do - OUTPUT=$(php -l "$FILE" 2>&1) - if echo "$OUTPUT" | grep -q "Parse error"; then - echo "::error file=${FILE}::${OUTPUT}" - ERRORS=$((ERRORS + 1)) - fi - done < <(find "$DIR" -name "*.php" -print0) - fi - done - echo "### PHP Syntax Check" >> $GITHUB_STEP_SUMMARY - if [ "${ERRORS}" -gt 0 ]; then - echo "**${ERRORS} syntax error(s) found.**" >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY - fi - - - name: XML manifest validation - run: | - echo "### XML Manifest Validation" >> $GITHUB_STEP_SUMMARY - ERRORS=0 - - # Find the extension manifest (XML with /dev/null; then - MANIFEST="$XML_FILE" - break - fi - done - - if [ -z "$MANIFEST" ]; then - echo "No Joomla extension manifest found (XML file with \`> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - echo "Manifest found: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY - - # Validate well-formed XML - php -r " - \$xml = @simplexml_load_file('$MANIFEST'); - if (\$xml === false) { - echo 'INVALID'; - exit(1); - } - echo 'VALID'; - " > /tmp/xml_result 2>&1 - XML_RESULT=$(cat /tmp/xml_result) - if [ "$XML_RESULT" != "VALID" ]; then - echo "Manifest is not well-formed XML." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - echo "Manifest is well-formed XML." >> $GITHUB_STEP_SUMMARY - fi - - # Check required tags: name, version, author, namespace (Joomla 5+) - for TAG in name version author namespace; do - if ! grep -q "<${TAG}>" "$MANIFEST" 2>/dev/null; then - echo "Missing required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - echo "Found required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY - fi - done - fi - - if [ "${ERRORS}" -gt 0 ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "**${ERRORS} manifest issue(s) found.**" >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Manifest validation passed.**" >> $GITHUB_STEP_SUMMARY - fi - - - name: Check language files referenced in manifest - run: | - echo "### Language File Check" >> $GITHUB_STEP_SUMMARY - ERRORS=0 - - MANIFEST="" - for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do - if grep -q "/dev/null; then - MANIFEST="$XML_FILE" - break - fi - done - - if [ -n "$MANIFEST" ]; then - # Extract language file references from manifest - LANG_FILES=$(grep -oP 'language\s+tag="[^"]*"[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true) - if [ -z "$LANG_FILES" ]; then - echo "No language file references found in manifest — skipping." >> $GITHUB_STEP_SUMMARY - else - while IFS= read -r LANG_FILE; do - LANG_FILE=$(echo "$LANG_FILE" | xargs) - if [ -z "$LANG_FILE" ]; then - continue - fi - # Check in common locations - FOUND=0 - for BASE in "." "src" "htdocs"; do - if [ -f "${BASE}/${LANG_FILE}" ]; then - FOUND=1 - break - fi - done - if [ "$FOUND" -eq 0 ]; then - echo "Missing language file: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - echo "Language file present: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY - fi - done <<< "$LANG_FILES" - fi - else - echo "No manifest found — skipping language check." >> $GITHUB_STEP_SUMMARY - fi - - if [ "${ERRORS}" -gt 0 ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "**${ERRORS} missing language file(s).**" >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Language file check passed.**" >> $GITHUB_STEP_SUMMARY - fi - - - name: Check index.html files in directories - run: | - echo "### Index.html Check" >> $GITHUB_STEP_SUMMARY - MISSING=0 - CHECKED=0 - - for DIR in src/ htdocs/; do - if [ -d "$DIR" ]; then - while IFS= read -r -d '' SUBDIR; do - CHECKED=$((CHECKED + 1)) - if [ ! -f "${SUBDIR}/index.html" ]; then - echo "Missing index.html in: \`${SUBDIR}\`" >> $GITHUB_STEP_SUMMARY - MISSING=$((MISSING + 1)) - fi - done < <(find "$DIR" -type d -print0) - fi - done - - if [ "${CHECKED}" -eq 0 ]; then - echo "No src/ or htdocs/ directories found — skipping." >> $GITHUB_STEP_SUMMARY - elif [ "${MISSING}" -gt 0 ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "**${MISSING} director(ies) missing index.html out of ${CHECKED} checked.**" >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "All ${CHECKED} directories contain index.html." >> $GITHUB_STEP_SUMMARY - fi - - release-readiness: - name: Release Readiness Check - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' && github.base_ref == 'main' - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Validate release readiness - run: | - echo "## Release Readiness" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - ERRORS=0 - - # Extract version from README.md - README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md | head -1) - if [ -z "$README_VERSION" ]; then - echo "No VERSION found in README.md FILE INFORMATION block." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - echo "README version: \`${README_VERSION}\`" >> $GITHUB_STEP_SUMMARY - fi - - # Find the extension manifest - MANIFEST="" - for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do - if grep -q "/dev/null; then - MANIFEST="$XML_FILE" - break - fi - done - - if [ -z "$MANIFEST" ]; then - echo "No Joomla extension manifest found." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - echo "Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY - - # Check matches README VERSION - MANIFEST_VERSION=$(grep -oP '\K[^<]+' "$MANIFEST" | head -1) - if [ -z "$MANIFEST_VERSION" ]; then - echo "No \`\` tag in manifest." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - elif [ -n "$README_VERSION" ] && [ "$MANIFEST_VERSION" != "$README_VERSION" ]; then - echo "Manifest version \`${MANIFEST_VERSION}\` does not match README \`${README_VERSION}\`." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - echo "Manifest version: \`${MANIFEST_VERSION}\`" >> $GITHUB_STEP_SUMMARY - fi - - # Check extension type, element, client attributes - EXT_TYPE=$(grep -oP ']*\btype="\K[^"]+' "$MANIFEST" | head -1) - if [ -z "$EXT_TYPE" ]; then - echo "Missing \`type\` attribute on \`\` tag." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - echo "Extension type: \`${EXT_TYPE}\`" >> $GITHUB_STEP_SUMMARY - fi - - # Element check (component/module/plugin name) - HAS_ELEMENT=$(grep -cP '<(element|name)>' "$MANIFEST" 2>/dev/null || echo "0") - if [ "$HAS_ELEMENT" -eq 0 ]; then - echo "Missing \`\` or \`\` in manifest." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - fi - - # Client attribute for site/admin modules and plugins - if echo "$EXT_TYPE" | grep -qP "^(module|plugin)$"; then - HAS_CLIENT=$(grep -cP ']*\bclient=' "$MANIFEST" 2>/dev/null || echo "0") - if [ "$HAS_CLIENT" -eq 0 ]; then - echo "Missing \`client\` attribute for ${EXT_TYPE} extension." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - fi - fi - fi - - # Check updates.xml exists - if [ -f "updates.xml" ] || [ -f "updates.xml" ]; then - echo "Update XML present." >> $GITHUB_STEP_SUMMARY - else - echo "No updates.xml found." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - fi - - # Check CHANGELOG.md exists - if [ -f "CHANGELOG.md" ]; then - echo "CHANGELOG.md present." >> $GITHUB_STEP_SUMMARY - else - echo "No CHANGELOG.md found." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - fi - - echo "" >> $GITHUB_STEP_SUMMARY - if [ $ERRORS -gt 0 ]; then - echo "**${ERRORS} issue(s) must be resolved before release.**" >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "**Extension is ready for release.**" >> $GITHUB_STEP_SUMMARY - fi - - test: - name: Tests (PHP ${{ matrix.php }}) - runs-on: ubuntu-latest - needs: lint-and-validate - - strategy: - fail-fast: false - matrix: - php: ['8.2', '8.3'] - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Setup PHP ${{ matrix.php }} - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 - with: - php-version: ${{ matrix.php }} - extensions: mbstring, xml, zip, gd, curl, json, simplexml - tools: composer:v2 - coverage: none - - - name: Install dependencies - env: - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' - run: | - if [ -f "composer.json" ]; then - composer install \ - --no-interaction \ - --prefer-dist \ - --optimize-autoloader - else - echo "No composer.json found — skipping dependency install" - fi - - - name: Run tests - run: | - echo "### Test Results (PHP ${{ matrix.php }})" >> $GITHUB_STEP_SUMMARY - if [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then - vendor/bin/phpunit --testdox 2>&1 | tee /tmp/test-output.log - EXIT=${PIPESTATUS[0]} - if [ $EXIT -eq 0 ]; then - echo "All tests passed." >> $GITHUB_STEP_SUMMARY - else - echo "Test failures detected — see log." >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - cat /tmp/test-output.log >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - fi - exit $EXIT - else - echo "No phpunit.xml found — skipping tests." >> $GITHUB_STEP_SUMMARY - fi diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 3abfb02..0000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,115 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# This file is part of a Moko Consulting project. -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: GitHub.Workflow.Template -# INGROUP: MokoStandards.Security -# REPO: https://github.com/mokoconsulting-tech/MokoStandards -# PATH: /templates/workflows/generic/codeql-analysis.yml.template -# VERSION: 04.05.00 -# BRIEF: CodeQL security scanning workflow (generic — all repo types) -# NOTE: Deployed to .github/workflows/codeql-analysis.yml in governed repos. -# CodeQL does not support PHP directly; JavaScript scans JSON/YAML/shell. -# For PHP-specific security scanning see standards-compliance.yml. - -name: CodeQL Security Scanning - -on: - push: - branches: - - main - - dev/** - - rc/** - - version/** - pull_request: - branches: - - main - - dev/** - - rc/** - schedule: - # Weekly on Monday at 06:00 UTC - - cron: '0 6 * * 1' - workflow_dispatch: - -permissions: - actions: read - contents: read - security-events: write - pull-requests: read - -jobs: - analyze: - name: Analyze (${{ matrix.language }}) - runs-on: ubuntu-latest - timeout-minutes: 360 - - strategy: - fail-fast: false - matrix: - # CodeQL does not support PHP. Use 'javascript' to scan JSON, YAML, - # and shell scripts. Add 'actions' to scan GitHub Actions workflows. - language: ['javascript', 'actions'] - - steps: - - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - queries: security-extended,security-and-quality - - - name: Autobuild - uses: github/codeql-action/autobuild@v3 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{ matrix.language }}" - upload: true - output: sarif-results - wait-for-processing: true - - - name: Upload SARIF results - if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.5.0 - with: - name: codeql-results-${{ matrix.language }} - path: sarif-results - retention-days: 30 - - - name: Step summary - if: always() - run: | - echo "### 🔍 CodeQL — ${{ matrix.language }}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - URL="https://github.com/${{ github.repository }}/security/code-scanning" - echo "See the [Security tab]($URL) for findings." >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Severity | SLA |" >> $GITHUB_STEP_SUMMARY - echo "|----------|-----|" >> $GITHUB_STEP_SUMMARY - echo "| Critical | 7 days |" >> $GITHUB_STEP_SUMMARY - echo "| High | 14 days |" >> $GITHUB_STEP_SUMMARY - echo "| Medium | 30 days |" >> $GITHUB_STEP_SUMMARY - echo "| Low | 60 days / next release |" >> $GITHUB_STEP_SUMMARY - - summary: - name: Security Scan Summary - runs-on: ubuntu-latest - needs: analyze - if: always() - - steps: - - name: Summary - run: | - echo "### 🛡️ CodeQL Complete" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Trigger:** ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY - echo "**Branch:** ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY - SECURITY_URL="https://github.com/${{ github.repository }}/security" - echo "" >> $GITHUB_STEP_SUMMARY - echo "📊 [View all security alerts]($SECURITY_URL)" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/deploy-demo.yml b/.github/workflows/deploy-demo.yml deleted file mode 100644 index f5fac4a..0000000 --- a/.github/workflows/deploy-demo.yml +++ /dev/null @@ -1,734 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# This file is part of a Moko Consulting project. -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# FILE INFORMATION -# DEFGROUP: GitHub.Workflow -# INGROUP: MokoStandards.Deploy -# REPO: https://github.com/mokoconsulting-tech/MokoStandards -# PATH: /templates/workflows/shared/deploy-demo.yml.template -# VERSION: 04.06.00 -# BRIEF: SFTP deployment workflow for demo server — synced to all governed repos -# NOTE: Synced via bulk-repo-sync to .github/workflows/deploy-demo.yml in all governed repos. -# Port is resolved in order: DEMO_FTP_PORT variable → :port suffix in DEMO_FTP_HOST → 22. - -name: Deploy to Demo Server (SFTP) - -# Deploys the contents of the src/ directory to the demo server via SFTP. -# Triggers on push/merge to main — deploys the production-ready build to the demo server. -# -# Required org-level variables: DEMO_FTP_HOST, DEMO_FTP_PATH, DEMO_FTP_USERNAME -# Optional org-level variable: DEMO_FTP_PORT (auto-detected from host or defaults to 22) -# Optional org/repo variable: DEMO_FTP_SUFFIX — when set, appended to DEMO_FTP_PATH to form the -# full remote destination: DEMO_FTP_PATH/DEMO_FTP_SUFFIX -# Ignore rules: Place a .ftpignore file in the src/ directory. Each non-empty, -# non-comment line is a glob pattern tested against the relative path -# of each file (e.g. "subdir/file.txt"). The .gitignore is NOT used. -# Required org-level secret: DEMO_FTP_KEY (preferred) or DEMO_FTP_PASSWORD -# -# Access control: only users with admin or maintain role on the repository may deploy. - -on: - push: - branches: - - main - - master - paths: - - 'src/**' - - 'htdocs/**' - pull_request: - types: [opened, synchronize, reopened, closed] - branches: - - main - - master - paths: - - 'src/**' - - 'htdocs/**' - workflow_dispatch: - inputs: - clear_remote: - description: 'Delete all files inside the remote destination folder before uploading' - required: false - default: false - type: boolean - -permissions: - contents: read - pull-requests: write - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - check-permission: - name: Verify Deployment Permission - runs-on: ubuntu-latest - steps: - - name: Check actor permission - env: - # Prefer the org-scoped GH_TOKEN secret (needed for the org membership - # fallback). Falls back to the built-in github.token so the collaborator - # endpoint still works even if GH_TOKEN is not configured. - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - run: | - ACTOR="${{ github.actor }}" - REPO="${{ github.repository }}" - ORG="${{ github.repository_owner }}" - - METHOD="" - AUTHORIZED="false" - - # Hardcoded authorized users — always allowed to deploy - AUTHORIZED_USERS="jmiller-moko github-actions[bot]" - for user in $AUTHORIZED_USERS; do - if [ "$ACTOR" = "$user" ]; then - AUTHORIZED="true" - METHOD="hardcoded allowlist" - PERMISSION="admin" - break - fi - done - - # For other actors, check repo/org permissions via API - if [ "$AUTHORIZED" != "true" ]; then - PERMISSION=$(gh api "repos/${REPO}/collaborators/${ACTOR}/permission" \ - --jq '.permission' 2>/dev/null) - METHOD="repo collaborator API" - - if [ -z "$PERMISSION" ]; then - ORG_ROLE=$(gh api "orgs/${ORG}/memberships/${ACTOR}" \ - --jq '.role' 2>/dev/null) - METHOD="org membership API" - if [ "$ORG_ROLE" = "owner" ]; then - PERMISSION="admin" - else - PERMISSION="none" - fi - fi - - case "$PERMISSION" in - admin|maintain) AUTHORIZED="true" ;; - esac - fi - - # Write detailed summary - { - echo "## 🔐 Deploy Authorization" - echo "" - echo "| Field | Value |" - echo "|-------|-------|" - echo "| **Actor** | \`${ACTOR}\` |" - echo "| **Repository** | \`${REPO}\` |" - echo "| **Permission** | \`${PERMISSION}\` |" - echo "| **Method** | ${METHOD} |" - echo "| **Authorized** | ${AUTHORIZED} |" - echo "| **Trigger** | \`${{ github.event_name }}\` |" - echo "| **Branch** | \`${{ github.ref_name }}\` |" - echo "" - } >> "$GITHUB_STEP_SUMMARY" - - if [ "$AUTHORIZED" = "true" ]; then - echo "✅ ${ACTOR} authorized to deploy (${METHOD})" >> "$GITHUB_STEP_SUMMARY" - else - echo "❌ ${ACTOR} is NOT authorized to deploy." >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "Deployment requires one of:" >> "$GITHUB_STEP_SUMMARY" - echo "- Being in the hardcoded allowlist" >> "$GITHUB_STEP_SUMMARY" - echo "- Having \`admin\` or \`maintain\` role on the repository" >> "$GITHUB_STEP_SUMMARY" - exit 1 - fi - - deploy: - name: SFTP Deploy → Demo - runs-on: ubuntu-latest - needs: [check-permission] - if: >- - !startsWith(github.head_ref || github.ref_name, 'chore/') && - (github.event_name == 'workflow_dispatch' || - github.event_name == 'push' || - (github.event_name == 'pull_request' && - (github.event.action == 'opened' || - github.event.action == 'synchronize' || - github.event.action == 'reopened' || - github.event.pull_request.merged == true))) - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Resolve source directory - id: source - run: | - # Resolve source directory: src/ preferred, htdocs/ as fallback - if [ -d "src" ]; then - SRC="src" - elif [ -d "htdocs" ]; then - SRC="htdocs" - else - echo "⚠️ No src/ or htdocs/ directory found — skipping deployment" - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - COUNT=$(find "$SRC" -type f | wc -l) - echo "✅ Source: ${SRC}/ (${COUNT} file(s))" - echo "skip=false" >> "$GITHUB_OUTPUT" - echo "dir=${SRC}" >> "$GITHUB_OUTPUT" - - - name: Preview files to deploy - if: steps.source.outputs.skip == 'false' - env: - SOURCE_DIR: ${{ steps.source.outputs.dir }} - run: | - # ── Convert a ftpignore-style glob line to an ERE pattern ────────────── - ftpignore_to_regex() { - local line="$1" - local anchored=false - # Strip inline comments and whitespace - line=$(printf '%s' "$line" | sed 's/[[:space:]]*#.*$//' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') - [ -z "$line" ] && return - # Skip negation patterns (not supported) - [[ "$line" == !* ]] && return - # Trailing slash = directory marker; strip it - line="${line%/}" - # Leading slash = anchored to root; strip it - if [[ "$line" == /* ]]; then - anchored=true - line="${line#/}" - fi - # Escape ERE special chars, then restore glob semantics - local regex - regex=$(printf '%s' "$line" \ - | sed 's/[.+^${}()|[\\]/\\&/g' \ - | sed 's/\\\*\\\*/\x01/g' \ - | sed 's/\\\*/[^\/]*/g' \ - | sed 's/\x01/.*/g' \ - | sed 's/\\\?/[^\/]/g') - if $anchored; then - printf '^%s(/|$)' "$regex" - else - printf '(^|/)%s(/|$)' "$regex" - fi - } - - # ── Read .ftpignore (ftpignore-style globs) ───────────────────────── - IGNORE_PATTERNS=() - IGNORE_SOURCES=() - if [ -f "${SOURCE_DIR}/.ftpignore" ]; then - while IFS= read -r line; do - [[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]] && continue - regex=$(ftpignore_to_regex "$line") - [ -n "$regex" ] && IGNORE_PATTERNS+=("$regex") && IGNORE_SOURCES+=("$line") - done < "${SOURCE_DIR}/.ftpignore" - fi - - # ── Walk src/ and classify every file ──────────────────────────────── - WILL_UPLOAD=() - IGNORED_FILES=() - while IFS= read -r -d '' file; do - rel="${file#${SOURCE_DIR}/}" - SKIP=false - for i in "${!IGNORE_PATTERNS[@]}"; do - if echo "$rel" | grep -qE "${IGNORE_PATTERNS[$i]}" 2>/dev/null; then - IGNORED_FILES+=("$rel | .ftpignore \`${IGNORE_SOURCES[$i]}\`") - SKIP=true; break - fi - done - $SKIP && continue - WILL_UPLOAD+=("$rel") - done < <(find "$SOURCE_DIR" -type f -print0 | sort -z) - - UPLOAD_COUNT="${#WILL_UPLOAD[@]}" - IGNORE_COUNT="${#IGNORED_FILES[@]}" - - echo "ℹ️ ${UPLOAD_COUNT} file(s) will be uploaded, ${IGNORE_COUNT} ignored" - - # ── Write deployment preview to step summary ────────────────────────── - { - echo "## 📋 Deployment Preview" - echo "" - echo "| Field | Value |" - echo "|---|---|" - echo "| Source | \`${SOURCE_DIR}/\` |" - echo "| Files to upload | **${UPLOAD_COUNT}** |" - echo "| Files ignored | **${IGNORE_COUNT}** |" - echo "" - if [ "${UPLOAD_COUNT}" -gt 0 ]; then - echo "### 📂 Files that will be uploaded" - echo '```' - printf '%s\n' "${WILL_UPLOAD[@]}" - echo '```' - echo "" - fi - if [ "${IGNORE_COUNT}" -gt 0 ]; then - echo "### ⏭️ Files excluded" - echo "| File | Reason |" - echo "|---|---|" - for entry in "${IGNORED_FILES[@]}"; do - f="${entry% | *}"; r="${entry##* | }" - echo "| \`${f}\` | ${r} |" - done - echo "" - fi - } >> "$GITHUB_STEP_SUMMARY" - - - name: Resolve SFTP host and port - if: steps.source.outputs.skip == 'false' - id: conn - env: - HOST_RAW: ${{ vars.DEMO_FTP_HOST }} - PORT_VAR: ${{ vars.DEMO_FTP_PORT }} - run: | - HOST="$HOST_RAW" - PORT="$PORT_VAR" - - if [ -z "$HOST" ]; then - echo "⏭️ DEMO_FTP_HOST not configured — skipping demo deployment." - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - - # Priority 1 — explicit DEMO_FTP_PORT variable - if [ -n "$PORT" ]; then - echo "ℹ️ Using explicit DEMO_FTP_PORT=${PORT}" - - # Priority 2 — port embedded in DEMO_FTP_HOST (host:port) - elif [[ "$HOST" == *:* ]]; then - PORT="${HOST##*:}" - HOST="${HOST%:*}" - echo "ℹ️ Extracted port ${PORT} from DEMO_FTP_HOST" - - # Priority 3 — SFTP default - else - PORT="22" - echo "ℹ️ No port specified — defaulting to SFTP port 22" - fi - - echo "host=${HOST}" >> "$GITHUB_OUTPUT" - echo "port=${PORT}" >> "$GITHUB_OUTPUT" - echo "SFTP target: ${HOST}:${PORT}" - - - name: Build remote path - if: steps.source.outputs.skip == 'false' && steps.conn.outputs.skip != 'true' - id: remote - env: - DEMO_FTP_PATH: ${{ vars.DEMO_FTP_PATH }} - DEMO_FTP_SUFFIX: ${{ vars.DEMO_FTP_SUFFIX }} - run: | - BASE="$DEMO_FTP_PATH" - - if [ -z "$BASE" ]; then - echo "⏭️ DEMO_FTP_PATH not configured — skipping demo deployment." - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - - # DEMO_FTP_SUFFIX is required — it identifies the remote subdirectory for this repo. - # Without it we cannot safely determine the deployment target. - if [ -z "$DEMO_FTP_SUFFIX" ]; then - echo "⏭️ DEMO_FTP_SUFFIX variable is not set — skipping deployment." - echo " Set DEMO_FTP_SUFFIX as a repo or org variable to enable deploy-demo." - echo "skip=true" >> "$GITHUB_OUTPUT" - echo "path=" >> "$GITHUB_OUTPUT" - exit 0 - fi - - REMOTE="${BASE%/}/${DEMO_FTP_SUFFIX#/}" - - # ── Platform-specific path safety guards ────────────────────────────── - PLATFORM="" - MOKO_FILE=".github/.mokostandards"; [ ! -f "$MOKO_FILE" ] && MOKO_FILE=".mokostandards"; if [ -f "$MOKO_FILE" ]; then - PLATFORM=$(grep -E '^platform:' "$MOKO_FILE" | sed 's/.*:[[:space:]]*//' | tr -d '"') - fi - - if [ "$PLATFORM" = "crm-module" ]; then - # Dolibarr modules must deploy under htdocs/custom/ — guard against - # accidentally overwriting server root or unrelated directories. - if [[ "$REMOTE" != *custom* ]]; then - echo "❌ Safety check failed: Dolibarr (crm-module) remote path must contain 'custom'." - echo " Current path: ${REMOTE}" - echo " Set DEMO_FTP_SUFFIX to the module's htdocs/custom/ subdirectory." - exit 1 - fi - fi - - if [ "$PLATFORM" = "waas-component" ]; then - # Joomla extensions may only deploy to the server's tmp/ directory. - if [[ "$REMOTE" != *tmp* ]]; then - echo "❌ Safety check failed: Joomla (waas-component) remote path must contain 'tmp'." - echo " Current path: ${REMOTE}" - echo " Set DEMO_FTP_SUFFIX to a path under the server tmp/ directory." - exit 1 - fi - fi - - echo "ℹ️ Remote path: ${REMOTE}" - echo "path=${REMOTE}" >> "$GITHUB_OUTPUT" - - - name: Detect SFTP authentication method - if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true' - id: auth - env: - HAS_KEY: ${{ secrets.DEMO_FTP_KEY }} - HAS_PASSWORD: ${{ secrets.DEMO_FTP_PASSWORD }} - run: | - if [ -n "$HAS_KEY" ] && [ -n "$HAS_PASSWORD" ]; then - # Both set: key auth with password as passphrase; falls back to password-only if key fails - echo "method=key" >> "$GITHUB_OUTPUT" - echo "use_passphrase=true" >> "$GITHUB_OUTPUT" - echo "has_password=true" >> "$GITHUB_OUTPUT" - echo "ℹ️ Primary: SSH key + passphrase (DEMO_FTP_KEY / DEMO_FTP_PASSWORD)" - echo "ℹ️ Fallback: password-only auth if key authentication fails" - elif [ -n "$HAS_KEY" ]; then - # Key only: no passphrase, no password fallback - echo "method=key" >> "$GITHUB_OUTPUT" - echo "use_passphrase=false" >> "$GITHUB_OUTPUT" - echo "has_password=false" >> "$GITHUB_OUTPUT" - echo "ℹ️ Using SSH key authentication (DEMO_FTP_KEY, no passphrase, no fallback)" - elif [ -n "$HAS_PASSWORD" ]; then - # Password only: direct SFTP password auth - echo "method=password" >> "$GITHUB_OUTPUT" - echo "use_passphrase=false" >> "$GITHUB_OUTPUT" - echo "has_password=true" >> "$GITHUB_OUTPUT" - echo "ℹ️ Using password authentication (DEMO_FTP_PASSWORD)" - else - echo "❌ No SFTP credentials configured." - echo " Set DEMO_FTP_KEY (preferred) or DEMO_FTP_PASSWORD as an org-level secret." - exit 1 - fi - - - name: Setup PHP - if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true' - uses: shivammathur/setup-php@fcafdd6392932010c2bd5094439b8e33be2a8a09 # v2.37.0 - with: - php-version: '8.1' - tools: composer - - - name: Setup MokoStandards deploy tools - if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true' - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' - run: | - git clone --depth 1 --branch version/04 --quiet \ - "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ - /tmp/mokostandards - cd /tmp/mokostandards - composer install --no-dev --no-interaction --quiet - - - name: Clear remote destination folder (manual only) - if: >- - steps.source.outputs.skip == 'false' && - steps.remote.outputs.skip != 'true' && - inputs.clear_remote == true - env: - SFTP_HOST: ${{ steps.conn.outputs.host }} - SFTP_PORT: ${{ steps.conn.outputs.port }} - SFTP_USER: ${{ vars.DEMO_FTP_USERNAME }} - SFTP_KEY: ${{ secrets.DEMO_FTP_KEY }} - SFTP_PASSWORD: ${{ secrets.DEMO_FTP_PASSWORD }} - AUTH_METHOD: ${{ steps.auth.outputs.method }} - USE_PASSPHRASE: ${{ steps.auth.outputs.use_passphrase }} - HAS_PASSWORD: ${{ steps.auth.outputs.has_password }} - REMOTE_PATH: ${{ steps.remote.outputs.path }} - run: | - cat > /tmp/moko_clear.php << 'PHPEOF' - login($username, $key)) { - if ($password !== '') { - echo "⚠️ Key auth failed — falling back to password\n"; - if (!$sftp->login($username, $password)) { - fwrite(STDERR, "❌ Both key and password authentication failed\n"); - exit(1); - } - echo "✅ Connected via password authentication (key fallback)\n"; - } else { - fwrite(STDERR, "❌ Key authentication failed and no password fallback is available\n"); - exit(1); - } - } else { - echo "✅ Connected via SSH key authentication\n"; - } - } else { - if (!$sftp->login($username, (string) getenv('SFTP_PASSWORD'))) { - fwrite(STDERR, "❌ Password authentication failed\n"); - exit(1); - } - echo "✅ Connected via password authentication\n"; - } - - // ── Recursive delete ──────────────────────────────────────────── - function rmrf(SFTP $sftp, string $path): void - { - $entries = $sftp->nlist($path); - if ($entries === false) { - return; // path does not exist — nothing to clear - } - foreach ($entries as $name) { - if ($name === '.' || $name === '..') { - continue; - } - $entry = "{$path}/{$name}"; - if ($sftp->is_dir($entry)) { - rmrf($sftp, $entry); - $sftp->rmdir($entry); - echo " 🗑️ Removed dir: {$entry}\n"; - } else { - $sftp->delete($entry); - echo " 🗑️ Removed file: {$entry}\n"; - } - } - } - - // ── Create remote directory tree ──────────────────────────────── - function sftpMakedirs(SFTP $sftp, string $path): void - { - $parts = array_values(array_filter(explode('/', $path), fn(string $p) => $p !== '')); - $current = str_starts_with($path, '/') ? '' : ''; - foreach ($parts as $part) { - $current .= '/' . $part; - $sftp->mkdir($current); // silently returns false if already exists - } - } - - rmrf($sftp, $remotePath); - sftpMakedirs($sftp, $remotePath); - echo "✅ Remote folder ready: {$remotePath}\n"; - PHPEOF - php /tmp/moko_clear.php - - - name: Deploy via SFTP - if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true' - env: - SFTP_HOST: ${{ steps.conn.outputs.host }} - SFTP_PORT: ${{ steps.conn.outputs.port }} - SFTP_USER: ${{ vars.DEMO_FTP_USERNAME }} - SFTP_KEY: ${{ secrets.DEMO_FTP_KEY }} - SFTP_PASSWORD: ${{ secrets.DEMO_FTP_PASSWORD }} - AUTH_METHOD: ${{ steps.auth.outputs.method }} - USE_PASSPHRASE: ${{ steps.auth.outputs.use_passphrase }} - REMOTE_PATH: ${{ steps.remote.outputs.path }} - SOURCE_DIR: ${{ steps.source.outputs.dir }} - run: | - # ── Write SSH key to temp file (key auth only) ──────────────────────── - if [ "$AUTH_METHOD" = "key" ]; then - printf '%s' "$SFTP_KEY" > /tmp/deploy_key - chmod 600 /tmp/deploy_key - fi - - # ── Generate sftp-config.json safely via jq ─────────────────────────── - if [ "$AUTH_METHOD" = "key" ]; then - jq -n \ - --arg host "$SFTP_HOST" \ - --argjson port "${SFTP_PORT:-22}" \ - --arg user "$SFTP_USER" \ - --arg path "$REMOTE_PATH" \ - --arg key "/tmp/deploy_key" \ - '{host:$host, port:$port, user:$user, remote_path:$path, ssh_key_file:$key}' \ - > /tmp/sftp-config.json - else - jq -n \ - --arg host "$SFTP_HOST" \ - --argjson port "${SFTP_PORT:-22}" \ - --arg user "$SFTP_USER" \ - --arg path "$REMOTE_PATH" \ - --arg pass "$SFTP_PASSWORD" \ - '{host:$host, port:$port, user:$user, remote_path:$path, password:$pass}' \ - > /tmp/sftp-config.json - fi - - # ── Write update files (demo = stable) ───────────────────────────── - PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true) - VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null || echo "unknown") - REPO="${{ github.repository }}" - - if [ "$PLATFORM" = "crm-module" ]; then - printf '%s' "$VERSION" > update.txt - fi - - if [ "$PLATFORM" = "waas-component" ]; then - MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1 || true) - if [ -n "$MANIFEST" ]; then - EXT_NAME=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "${{ github.event.repository.name }}") - EXT_TYPE=$(grep -oP ']+type="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "component") - EXT_ELEMENT=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || basename "$MANIFEST" .xml) - EXT_CLIENT=$(grep -oP ']+client="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "") - EXT_FOLDER=$(grep -oP ']+group="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "") - TARGET_PLATFORM=$(grep -oP '/dev/null | head -1 || true) - [ -n "$TARGET_PLATFORM" ] && TARGET_PLATFORM="${TARGET_PLATFORM}>" - [ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '' "/") - - CLIENT_TAG="" - if [ -n "$EXT_CLIENT" ]; then CLIENT_TAG="${EXT_CLIENT}"; elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then CLIENT_TAG="site"; fi - FOLDER_TAG="" - if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then FOLDER_TAG="${EXT_FOLDER}"; fi - - DOWNLOAD_URL="https://github.com/${REPO}/releases/download/v${VERSION}/${EXT_ELEMENT}-${VERSION}.zip" - { - printf '%s\n' '' - printf '%s\n' '' - printf '%s\n' ' ' - printf '%s\n' " ${EXT_NAME}" - printf '%s\n' " ${EXT_NAME} update" - printf '%s\n' " ${EXT_ELEMENT}" - printf '%s\n' " ${EXT_TYPE}" - printf '%s\n' " ${VERSION}" - [ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}" - [ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}" - printf '%s\n' ' ' - printf '%s\n' ' stable' - printf '%s\n' ' ' - printf '%s\n' " https://github.com/${REPO}" - printf '%s\n' ' ' - printf '%s\n' " ${DOWNLOAD_URL}" - printf '%s\n' ' ' - printf '%s\n' " ${TARGET_PLATFORM}" - printf '%s\n' ' Moko Consulting' - printf '%s\n' ' https://mokoconsulting.tech' - printf '%s\n' ' ' - printf '%s\n' '' - } > updates.xml - fi - fi - - # ── Run deploy-sftp.php from MokoStandards ──────────────────────────── - DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json) - if [ "$USE_PASSPHRASE" = "true" ]; then - DEPLOY_ARGS+=(--key-passphrase "$SFTP_PASSWORD") - fi - - PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true) - if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards/api/deploy/deploy-joomla.php" ]; then - php /tmp/mokostandards/api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}" - else - php /tmp/mokostandards/api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}" - fi - # Remove temp files that should never be left behind - rm -f /tmp/deploy_key /tmp/sftp-config.json - - - name: Create or update failure issue - if: failure() && steps.remote.outputs.skip != 'true' && steps.conn.outputs.skip != 'true' - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - run: | - REPO="${{ github.repository }}" - RUN_URL="${{ github.server_url }}/${REPO}/actions/runs/${{ github.run_id }}" - ACTOR="${{ github.actor }}" - BRANCH="${{ github.ref_name }}" - EVENT="${{ github.event_name }}" - NOW=$(date -u '+%Y-%m-%d %H:%M:%S UTC') - LABEL="deploy-failure" - - TITLE="fix: Demo deployment failed — ${REPO}" - BODY="## Demo Deployment Failed - - A deployment to the demo server failed and requires attention. - - | Field | Value | - |-------|-------| - | **Repository** | \`${REPO}\` | - | **Branch** | \`${BRANCH}\` | - | **Trigger** | ${EVENT} | - | **Actor** | @${ACTOR} | - | **Failed at** | ${NOW} | - | **Run** | [View workflow run](${RUN_URL}) | - - ### Next steps - 1. Review the [workflow run log](${RUN_URL}) for the specific error. - 2. Fix the underlying issue (credentials, SFTP connectivity, permissions). - 3. Re-trigger the deployment via **Actions → Deploy to Demo Server → Run workflow**. - - --- - *Auto-created by deploy-demo.yml — close this issue once the deployment is resolved.*" - - # Ensure the label exists (idempotent — no-op if already present) - gh label create "$LABEL" \ - --repo "$REPO" \ - --color "CC0000" \ - --description "Automated deploy failure tracking" \ - --force 2>/dev/null || true - - # Look for an existing open deploy-failure issue - EXISTING=$(gh api "repos/${REPO}/issues?labels=${LABEL}&state=all&per_page=1&sort=created&direction=desc" \ - --jq '.[0].number' 2>/dev/null) - - if [ -n "$EXISTING" ] && [ "$EXISTING" != "null" ]; then - gh api "repos/${REPO}/issues/${EXISTING}" \ - -X PATCH \ - -f title="$TITLE" \ - -f body="$BODY" \ - -f state="open" \ - --silent - echo "📋 Failure issue #${EXISTING} updated/reopened: ${REPO}" >> "$GITHUB_STEP_SUMMARY" - else - gh issue create \ - --repo "$REPO" \ - --title "$TITLE" \ - --body "$BODY" \ - --label "$LABEL" \ - --assignee "jmiller-moko" \ - | tee -a "$GITHUB_STEP_SUMMARY" - fi - - - name: Deployment summary - if: always() - run: | - if [ "${{ steps.source.outputs.skip }}" == "true" ]; then - echo "### ⏭️ Deployment Skipped" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "No \`src/\` directory found in this repository." >> "$GITHUB_STEP_SUMMARY" - elif [ "${{ job.status }}" == "success" ]; then - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "### ✅ Demo Deployment Successful" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "| Field | Value |" >> "$GITHUB_STEP_SUMMARY" - echo "|-------|-------|" >> "$GITHUB_STEP_SUMMARY" - echo "| Host | \`${{ steps.conn.outputs.host }}:${{ steps.conn.outputs.port }}\` |" >> "$GITHUB_STEP_SUMMARY" - echo "| Remote path | \`${{ steps.remote.outputs.path }}\` |" >> "$GITHUB_STEP_SUMMARY" - echo "| Source | \`src/\` |" >> "$GITHUB_STEP_SUMMARY" - echo "| Trigger | ${{ github.event_name }} |" >> "$GITHUB_STEP_SUMMARY" - echo "| Auth | ${{ steps.auth.outputs.method }} |" >> "$GITHUB_STEP_SUMMARY" - echo "| Clear remote | ${{ inputs.clear_remote || 'false' }} |" >> "$GITHUB_STEP_SUMMARY" - else - echo "### ❌ Demo Deployment Failed" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "Check the job log above for error details." >> "$GITHUB_STEP_SUMMARY" - fi diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml deleted file mode 100644 index 7781d00..0000000 --- a/.github/workflows/deploy-dev.yml +++ /dev/null @@ -1,700 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# This file is part of a Moko Consulting project. -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# FILE INFORMATION -# DEFGROUP: GitHub.Workflow -# INGROUP: MokoStandards.Deploy -# REPO: https://github.com/mokoconsulting-tech/MokoStandards -# PATH: /templates/workflows/shared/deploy-dev.yml.template -# VERSION: 04.06.00 -# BRIEF: SFTP deployment workflow for development server — synced to all governed repos -# NOTE: Synced via bulk-repo-sync to .github/workflows/deploy-dev.yml in all governed repos. -# Port is resolved in order: DEV_FTP_PORT variable → :port suffix in DEV_FTP_HOST → 22. - -name: Deploy to Dev Server (SFTP) - -# Deploys the contents of the src/ directory to the development server via SFTP. -# Triggers on every pull_request to development branches (so the dev server always -# reflects the latest PR state) and on push/merge to main branches. -# -# Required org-level variables: DEV_FTP_HOST, DEV_FTP_PATH, DEV_FTP_USERNAME -# Optional org-level variable: DEV_FTP_PORT (auto-detected from host or defaults to 22) -# Optional org/repo variable: DEV_FTP_SUFFIX — when set, appended to DEV_FTP_PATH to form the -# full remote destination: DEV_FTP_PATH/DEV_FTP_SUFFIX -# Ignore rules: Place a .ftpignore file in the src/ directory. Each non-empty, -# non-comment line is a glob pattern tested against the relative path -# of each file (e.g. "subdir/file.txt"). The .gitignore is NOT used. -# Required org-level secret: DEV_FTP_KEY (preferred) or DEV_FTP_PASSWORD -# -# Access control: only users with admin or maintain role on the repository may deploy. - -on: - push: - branches: - - 'dev/**' - - 'rc/**' - - develop - - development - paths: - - 'src/**' - - 'htdocs/**' - pull_request: - types: [opened, synchronize, reopened, closed] - branches: - - 'dev/**' - - 'rc/**' - - develop - - development - paths: - - 'src/**' - - 'htdocs/**' - workflow_dispatch: - inputs: - clear_remote: - description: 'Delete all files inside the remote destination folder before uploading' - required: false - default: false - type: boolean - -permissions: - contents: read - pull-requests: write - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - check-permission: - name: Verify Deployment Permission - runs-on: ubuntu-latest - steps: - - name: Check actor permission - env: - # Prefer the org-scoped GH_TOKEN secret (needed for the org membership - # fallback). Falls back to the built-in github.token so the collaborator - # endpoint still works even if GH_TOKEN is not configured. - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - run: | - ACTOR="${{ github.actor }}" - REPO="${{ github.repository }}" - ORG="${{ github.repository_owner }}" - - METHOD="" - AUTHORIZED="false" - - # Hardcoded authorized users — always allowed to deploy - AUTHORIZED_USERS="jmiller-moko github-actions[bot]" - for user in $AUTHORIZED_USERS; do - if [ "$ACTOR" = "$user" ]; then - AUTHORIZED="true" - METHOD="hardcoded allowlist" - PERMISSION="admin" - break - fi - done - - # For other actors, check repo/org permissions via API - if [ "$AUTHORIZED" != "true" ]; then - PERMISSION=$(gh api "repos/${REPO}/collaborators/${ACTOR}/permission" \ - --jq '.permission' 2>/dev/null) - METHOD="repo collaborator API" - - if [ -z "$PERMISSION" ]; then - ORG_ROLE=$(gh api "orgs/${ORG}/memberships/${ACTOR}" \ - --jq '.role' 2>/dev/null) - METHOD="org membership API" - if [ "$ORG_ROLE" = "owner" ]; then - PERMISSION="admin" - else - PERMISSION="none" - fi - fi - - case "$PERMISSION" in - admin|maintain) AUTHORIZED="true" ;; - esac - fi - - # Write detailed summary - { - echo "## 🔐 Deploy Authorization" - echo "" - echo "| Field | Value |" - echo "|-------|-------|" - echo "| **Actor** | \`${ACTOR}\` |" - echo "| **Repository** | \`${REPO}\` |" - echo "| **Permission** | \`${PERMISSION}\` |" - echo "| **Method** | ${METHOD} |" - echo "| **Authorized** | ${AUTHORIZED} |" - echo "| **Trigger** | \`${{ github.event_name }}\` |" - echo "| **Branch** | \`${{ github.ref_name }}\` |" - echo "" - } >> "$GITHUB_STEP_SUMMARY" - - if [ "$AUTHORIZED" = "true" ]; then - echo "✅ ${ACTOR} authorized to deploy (${METHOD})" >> "$GITHUB_STEP_SUMMARY" - else - echo "❌ ${ACTOR} is NOT authorized to deploy." >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "Deployment requires one of:" >> "$GITHUB_STEP_SUMMARY" - echo "- Being in the hardcoded allowlist" >> "$GITHUB_STEP_SUMMARY" - echo "- Having \`admin\` or \`maintain\` role on the repository" >> "$GITHUB_STEP_SUMMARY" - exit 1 - fi - - deploy: - name: SFTP Deploy → Dev - runs-on: ubuntu-latest - needs: [check-permission] - if: >- - !startsWith(github.head_ref || github.ref_name, 'chore/') && - (github.event_name == 'workflow_dispatch' || - github.event_name == 'push' || - (github.event_name == 'pull_request' && - (github.event.action == 'opened' || - github.event.action == 'synchronize' || - github.event.action == 'reopened' || - github.event.pull_request.merged == true))) - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Resolve source directory - id: source - run: | - # Resolve source directory: src/ preferred, htdocs/ as fallback - if [ -d "src" ]; then - SRC="src" - elif [ -d "htdocs" ]; then - SRC="htdocs" - else - echo "⚠️ No src/ or htdocs/ directory found — skipping deployment" - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - COUNT=$(find "$SRC" -type f | wc -l) - echo "✅ Source: ${SRC}/ (${COUNT} file(s))" - echo "skip=false" >> "$GITHUB_OUTPUT" - echo "dir=${SRC}" >> "$GITHUB_OUTPUT" - - - name: Preview files to deploy - if: steps.source.outputs.skip == 'false' - env: - SOURCE_DIR: ${{ steps.source.outputs.dir }} - run: | - # ── Convert a ftpignore-style glob line to an ERE pattern ────────────── - ftpignore_to_regex() { - local line="$1" - local anchored=false - # Strip inline comments and whitespace - line=$(printf '%s' "$line" | sed 's/[[:space:]]*#.*$//' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') - [ -z "$line" ] && return - # Skip negation patterns (not supported) - [[ "$line" == !* ]] && return - # Trailing slash = directory marker; strip it - line="${line%/}" - # Leading slash = anchored to root; strip it - if [[ "$line" == /* ]]; then - anchored=true - line="${line#/}" - fi - # Escape ERE special chars, then restore glob semantics - local regex - regex=$(printf '%s' "$line" \ - | sed 's/[.+^${}()|[\\]/\\&/g' \ - | sed 's/\\\*\\\*/\x01/g' \ - | sed 's/\\\*/[^\/]*/g' \ - | sed 's/\x01/.*/g' \ - | sed 's/\\\?/[^\/]/g') - if $anchored; then - printf '^%s(/|$)' "$regex" - else - printf '(^|/)%s(/|$)' "$regex" - fi - } - - # ── Read .ftpignore (ftpignore-style globs) ───────────────────────── - IGNORE_PATTERNS=() - IGNORE_SOURCES=() - if [ -f "${SOURCE_DIR}/.ftpignore" ]; then - while IFS= read -r line; do - [[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]] && continue - regex=$(ftpignore_to_regex "$line") - [ -n "$regex" ] && IGNORE_PATTERNS+=("$regex") && IGNORE_SOURCES+=("$line") - done < "${SOURCE_DIR}/.ftpignore" - fi - - # ── Walk src/ and classify every file ──────────────────────────────── - WILL_UPLOAD=() - IGNORED_FILES=() - while IFS= read -r -d '' file; do - rel="${file#${SOURCE_DIR}/}" - SKIP=false - for i in "${!IGNORE_PATTERNS[@]}"; do - if echo "$rel" | grep -qE "${IGNORE_PATTERNS[$i]}" 2>/dev/null; then - IGNORED_FILES+=("$rel | .ftpignore \`${IGNORE_SOURCES[$i]}\`") - SKIP=true; break - fi - done - $SKIP && continue - WILL_UPLOAD+=("$rel") - done < <(find "$SOURCE_DIR" -type f -print0 | sort -z) - - UPLOAD_COUNT="${#WILL_UPLOAD[@]}" - IGNORE_COUNT="${#IGNORED_FILES[@]}" - - echo "ℹ️ ${UPLOAD_COUNT} file(s) will be uploaded, ${IGNORE_COUNT} ignored" - - # ── Write deployment preview to step summary ────────────────────────── - { - echo "## 📋 Deployment Preview" - echo "" - echo "| Field | Value |" - echo "|---|---|" - echo "| Source | \`${SOURCE_DIR}/\` |" - echo "| Files to upload | **${UPLOAD_COUNT}** |" - echo "| Files ignored | **${IGNORE_COUNT}** |" - echo "" - if [ "${UPLOAD_COUNT}" -gt 0 ]; then - echo "### 📂 Files that will be uploaded" - echo '```' - printf '%s\n' "${WILL_UPLOAD[@]}" - echo '```' - echo "" - fi - if [ "${IGNORE_COUNT}" -gt 0 ]; then - echo "### ⏭️ Files excluded" - echo "| File | Reason |" - echo "|---|---|" - for entry in "${IGNORED_FILES[@]}"; do - f="${entry% | *}"; r="${entry##* | }" - echo "| \`${f}\` | ${r} |" - done - echo "" - fi - } >> "$GITHUB_STEP_SUMMARY" - - - name: Resolve SFTP host and port - if: steps.source.outputs.skip == 'false' - id: conn - env: - HOST_RAW: ${{ vars.DEV_FTP_HOST }} - PORT_VAR: ${{ vars.DEV_FTP_PORT }} - run: | - HOST="$HOST_RAW" - PORT="$PORT_VAR" - - # Priority 1 — explicit DEV_FTP_PORT variable - if [ -n "$PORT" ]; then - echo "ℹ️ Using explicit DEV_FTP_PORT=${PORT}" - - # Priority 2 — port embedded in DEV_FTP_HOST (host:port) - elif [[ "$HOST" == *:* ]]; then - PORT="${HOST##*:}" - HOST="${HOST%:*}" - echo "ℹ️ Extracted port ${PORT} from DEV_FTP_HOST" - - # Priority 3 — SFTP default - else - PORT="22" - echo "ℹ️ No port specified — defaulting to SFTP port 22" - fi - - echo "host=${HOST}" >> "$GITHUB_OUTPUT" - echo "port=${PORT}" >> "$GITHUB_OUTPUT" - echo "SFTP target: ${HOST}:${PORT}" - - - name: Build remote path - if: steps.source.outputs.skip == 'false' - id: remote - env: - DEV_FTP_PATH: ${{ vars.DEV_FTP_PATH }} - DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }} - run: | - BASE="$DEV_FTP_PATH" - - if [ -z "$BASE" ]; then - echo "❌ DEV_FTP_PATH is not set." - echo " Configure it as an org-level variable (Settings → Variables) and" - echo " ensure this repository has been granted access to it." - exit 1 - fi - - # DEV_FTP_SUFFIX is required — it identifies the remote subdirectory for this repo. - # Without it we cannot safely determine the deployment target. - if [ -z "$DEV_FTP_SUFFIX" ]; then - echo "⏭️ DEV_FTP_SUFFIX variable is not set — skipping deployment." - echo " Set DEV_FTP_SUFFIX as a repo or org variable to enable deploy-dev." - echo "skip=true" >> "$GITHUB_OUTPUT" - echo "path=" >> "$GITHUB_OUTPUT" - exit 0 - fi - - REMOTE="${BASE%/}/${DEV_FTP_SUFFIX#/}" - - # ── Platform-specific path safety guards ────────────────────────────── - PLATFORM="" - MOKO_FILE=".github/.mokostandards"; [ ! -f "$MOKO_FILE" ] && MOKO_FILE=".mokostandards"; if [ -f "$MOKO_FILE" ]; then - PLATFORM=$(grep -oP '^platform:.*' "$MOKO_FILE" 2>/dev/null || true) - fi - - if [ "$PLATFORM" = "crm-module" ]; then - # Dolibarr modules must deploy under htdocs/custom/ — guard against - # accidentally overwriting server root or unrelated directories. - if [[ "$REMOTE" != *custom* ]]; then - echo "❌ Safety check failed: Dolibarr (crm-module) remote path must contain 'custom'." - echo " Current path: ${REMOTE}" - echo " Set DEV_FTP_SUFFIX to the module's htdocs/custom/ subdirectory." - exit 1 - fi - fi - - if [ "$PLATFORM" = "waas-component" ]; then - # Joomla extensions may only deploy to the server's tmp/ directory. - if [[ "$REMOTE" != *tmp* ]]; then - echo "❌ Safety check failed: Joomla (waas-component) remote path must contain 'tmp'." - echo " Current path: ${REMOTE}" - echo " Set DEV_FTP_SUFFIX to a path under the server tmp/ directory." - exit 1 - fi - fi - - echo "ℹ️ Remote path: ${REMOTE}" - echo "path=${REMOTE}" >> "$GITHUB_OUTPUT" - - - name: Detect SFTP authentication method - if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true' - id: auth - env: - HAS_KEY: ${{ secrets.DEV_FTP_KEY }} - HAS_PASSWORD: ${{ secrets.DEV_FTP_PASSWORD }} - run: | - if [ -n "$HAS_KEY" ] && [ -n "$HAS_PASSWORD" ]; then - # Both set: key auth with password as passphrase; falls back to password-only if key fails - echo "method=key" >> "$GITHUB_OUTPUT" - echo "use_passphrase=true" >> "$GITHUB_OUTPUT" - echo "has_password=true" >> "$GITHUB_OUTPUT" - echo "ℹ️ Primary: SSH key + passphrase (DEV_FTP_KEY / DEV_FTP_PASSWORD)" - echo "ℹ️ Fallback: password-only auth if key authentication fails" - elif [ -n "$HAS_KEY" ]; then - # Key only: no passphrase, no password fallback - echo "method=key" >> "$GITHUB_OUTPUT" - echo "use_passphrase=false" >> "$GITHUB_OUTPUT" - echo "has_password=false" >> "$GITHUB_OUTPUT" - echo "ℹ️ Using SSH key authentication (DEV_FTP_KEY, no passphrase, no fallback)" - elif [ -n "$HAS_PASSWORD" ]; then - # Password only: direct SFTP password auth - echo "method=password" >> "$GITHUB_OUTPUT" - echo "use_passphrase=false" >> "$GITHUB_OUTPUT" - echo "has_password=true" >> "$GITHUB_OUTPUT" - echo "ℹ️ Using password authentication (DEV_FTP_PASSWORD)" - else - echo "❌ No SFTP credentials configured." - echo " Set DEV_FTP_KEY (preferred) or DEV_FTP_PASSWORD as an org-level secret." - exit 1 - fi - - - name: Setup PHP - if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true' - uses: shivammathur/setup-php@fcafdd6392932010c2bd5094439b8e33be2a8a09 # v2.37.0 - with: - php-version: '8.1' - tools: composer - - - name: Setup MokoStandards deploy tools - if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true' - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' - run: | - git clone --depth 1 --branch version/04 --quiet \ - "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ - /tmp/mokostandards - cd /tmp/mokostandards - composer install --no-dev --no-interaction --quiet - - - name: Clear remote destination folder (manual only) - if: >- - steps.source.outputs.skip == 'false' && - steps.remote.outputs.skip != 'true' && - inputs.clear_remote == true - env: - SFTP_HOST: ${{ steps.conn.outputs.host }} - SFTP_PORT: ${{ steps.conn.outputs.port }} - SFTP_USER: ${{ vars.DEV_FTP_USERNAME }} - SFTP_KEY: ${{ secrets.DEV_FTP_KEY }} - SFTP_PASSWORD: ${{ secrets.DEV_FTP_PASSWORD }} - AUTH_METHOD: ${{ steps.auth.outputs.method }} - USE_PASSPHRASE: ${{ steps.auth.outputs.use_passphrase }} - HAS_PASSWORD: ${{ steps.auth.outputs.has_password }} - REMOTE_PATH: ${{ steps.remote.outputs.path }} - run: | - cat > /tmp/moko_clear.php << 'PHPEOF' - login($username, $key)) { - if ($password !== '') { - echo "⚠️ Key auth failed — falling back to password\n"; - if (!$sftp->login($username, $password)) { - fwrite(STDERR, "❌ Both key and password authentication failed\n"); - exit(1); - } - echo "✅ Connected via password authentication (key fallback)\n"; - } else { - fwrite(STDERR, "❌ Key authentication failed and no password fallback is available\n"); - exit(1); - } - } else { - echo "✅ Connected via SSH key authentication\n"; - } - } else { - if (!$sftp->login($username, (string) getenv('SFTP_PASSWORD'))) { - fwrite(STDERR, "❌ Password authentication failed\n"); - exit(1); - } - echo "✅ Connected via password authentication\n"; - } - - // ── Recursive delete ──────────────────────────────────────────── - function rmrf(SFTP $sftp, string $path): void - { - $entries = $sftp->nlist($path); - if ($entries === false) { - return; // path does not exist — nothing to clear - } - foreach ($entries as $name) { - if ($name === '.' || $name === '..') { - continue; - } - $entry = "{$path}/{$name}"; - if ($sftp->is_dir($entry)) { - rmrf($sftp, $entry); - $sftp->rmdir($entry); - echo " 🗑️ Removed dir: {$entry}\n"; - } else { - $sftp->delete($entry); - echo " 🗑️ Removed file: {$entry}\n"; - } - } - } - - // ── Create remote directory tree ──────────────────────────────── - function sftpMakedirs(SFTP $sftp, string $path): void - { - $parts = array_values(array_filter(explode('/', $path), fn(string $p) => $p !== '')); - $current = str_starts_with($path, '/') ? '' : ''; - foreach ($parts as $part) { - $current .= '/' . $part; - $sftp->mkdir($current); // silently returns false if already exists - } - } - - rmrf($sftp, $remotePath); - sftpMakedirs($sftp, $remotePath); - echo "✅ Remote folder ready: {$remotePath}\n"; - PHPEOF - php /tmp/moko_clear.php - - - name: Deploy via SFTP - if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true' - env: - SFTP_HOST: ${{ steps.conn.outputs.host }} - SFTP_PORT: ${{ steps.conn.outputs.port }} - SFTP_USER: ${{ vars.DEV_FTP_USERNAME }} - SFTP_KEY: ${{ secrets.DEV_FTP_KEY }} - SFTP_PASSWORD: ${{ secrets.DEV_FTP_PASSWORD }} - AUTH_METHOD: ${{ steps.auth.outputs.method }} - USE_PASSPHRASE: ${{ steps.auth.outputs.use_passphrase }} - REMOTE_PATH: ${{ steps.remote.outputs.path }} - SOURCE_DIR: ${{ steps.source.outputs.dir }} - run: | - # ── Write SSH key to temp file (key auth only) ──────────────────────── - if [ "$AUTH_METHOD" = "key" ]; then - printf '%s' "$SFTP_KEY" > /tmp/deploy_key - chmod 600 /tmp/deploy_key - fi - - # ── Generate sftp-config.json safely via jq ─────────────────────────── - if [ "$AUTH_METHOD" = "key" ]; then - jq -n \ - --arg host "$SFTP_HOST" \ - --argjson port "${SFTP_PORT:-22}" \ - --arg user "$SFTP_USER" \ - --arg path "$REMOTE_PATH" \ - --arg key "/tmp/deploy_key" \ - '{host:$host, port:$port, user:$user, remote_path:$path, ssh_key_file:$key}' \ - > /tmp/sftp-config.json - else - jq -n \ - --arg host "$SFTP_HOST" \ - --argjson port "${SFTP_PORT:-22}" \ - --arg user "$SFTP_USER" \ - --arg path "$REMOTE_PATH" \ - --arg pass "$SFTP_PASSWORD" \ - '{host:$host, port:$port, user:$user, remote_path:$path, password:$pass}' \ - > /tmp/sftp-config.json - fi - - # Dev deploys skip minified files — use unminified sources for debugging - echo "*.min.js" >> "${SOURCE_DIR}/.ftpignore" - echo "*.min.css" >> "${SOURCE_DIR}/.ftpignore" - - # ── Run deploy-sftp.php from MokoStandards ──────────────────────────── - DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json) - if [ "$USE_PASSPHRASE" = "true" ]; then - DEPLOY_ARGS+=(--key-passphrase "$SFTP_PASSWORD") - fi - - # Set platform version to "development" before deploy (Dolibarr + Joomla) - php /tmp/mokostandards/api/cli/version_set_platform.php --path . --version development - - # Write update files — dev/** = development, rc/** = rc - PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true) - REPO="${{ github.repository }}" - BRANCH="${{ github.ref_name }}" - - # Determine stability tag from branch prefix - STABILITY="development" - VERSION_LABEL="development" - if [[ "$BRANCH" == rc/* ]]; then - STABILITY="rc" - VERSION_LABEL=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null || echo "${BRANCH#rc/}")-rc - fi - - if [ "$PLATFORM" = "crm-module" ]; then - printf '%s' "$VERSION_LABEL" > update.txt - fi - - if [ "$PLATFORM" = "waas-component" ]; then - MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1 || true) - if [ -n "$MANIFEST" ]; then - EXT_NAME=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "${{ github.event.repository.name }}") - EXT_TYPE=$(grep -oP ']+type="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "component") - EXT_ELEMENT=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || basename "$MANIFEST" .xml) - EXT_CLIENT=$(grep -oP ']+client="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "") - EXT_FOLDER=$(grep -oP ']+group="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "") - TARGET_PLATFORM=$(grep -oP '/dev/null | head -1 || true) - [ -n "$TARGET_PLATFORM" ] && TARGET_PLATFORM="${TARGET_PLATFORM}>" - [ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '' "/") - - CLIENT_TAG="" - if [ -n "$EXT_CLIENT" ]; then - CLIENT_TAG="${EXT_CLIENT}" - elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then - CLIENT_TAG="site" - fi - - FOLDER_TAG="" - if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then - FOLDER_TAG="${EXT_FOLDER}" - fi - - DOWNLOAD_URL="https://github.com/${REPO}/archive/refs/heads/${BRANCH}.zip" - - { - printf '%s\n' '' - printf '%s\n' '' - printf '%s\n' ' ' - printf '%s\n' " ${EXT_NAME}" - printf '%s\n' " ${EXT_NAME} ${STABILITY} build" - printf '%s\n' " ${EXT_ELEMENT}" - printf '%s\n' " ${EXT_TYPE}" - printf '%s\n' " ${VERSION_LABEL}" - [ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}" - [ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}" - printf '%s\n' ' ' - printf '%s\n' " ${STABILITY}" - printf '%s\n' ' ' - printf '%s\n' " https://github.com/${REPO}/tree/${BRANCH}" - printf '%s\n' ' ' - printf '%s\n' " ${DOWNLOAD_URL}" - printf '%s\n' ' ' - printf '%s\n' " ${TARGET_PLATFORM}" - printf '%s\n' ' Moko Consulting' - printf '%s\n' ' https://mokoconsulting.tech' - printf '%s\n' ' ' - printf '%s\n' '' - } > updates.xml - sed -i '/^[[:space:]]*$/d' updates.xml - fi - fi - - # Use Joomla-aware deploy for waas-component (routes files to correct Joomla dirs) - # Use standard SFTP deploy for everything else - 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 - # (both scripts handle dotfile skipping and .ftpignore natively) - # Remove temp files that should never be left behind - rm -f /tmp/deploy_key /tmp/sftp-config.json - - # Dev deploys fail silently — no issue creation. - # Demo and RS deploys create failure issues (production-facing). - - - name: Deployment summary - if: always() - run: | - if [ "${{ steps.source.outputs.skip }}" == "true" ]; then - echo "### ⏭️ Deployment Skipped" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "No \`src/\` directory found in this repository." >> "$GITHUB_STEP_SUMMARY" - elif [ "${{ job.status }}" == "success" ]; then - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "### ✅ Dev Deployment Successful" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "| Field | Value |" >> "$GITHUB_STEP_SUMMARY" - echo "|-------|-------|" >> "$GITHUB_STEP_SUMMARY" - echo "| Host | \`${{ steps.conn.outputs.host }}:${{ steps.conn.outputs.port }}\` |" >> "$GITHUB_STEP_SUMMARY" - echo "| Remote path | \`${{ steps.remote.outputs.path }}\` |" >> "$GITHUB_STEP_SUMMARY" - echo "| Source | \`src/\` |" >> "$GITHUB_STEP_SUMMARY" - echo "| Trigger | ${{ github.event_name }} |" >> "$GITHUB_STEP_SUMMARY" - echo "| Auth | ${{ steps.auth.outputs.method }} |" >> "$GITHUB_STEP_SUMMARY" - echo "| Clear remote | ${{ inputs.clear_remote || 'false' }} |" >> "$GITHUB_STEP_SUMMARY" - else - echo "### ❌ Dev Deployment Failed" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "Check the job log above for error details." >> "$GITHUB_STEP_SUMMARY" - fi diff --git a/.github/workflows/deploy-manual.yml b/.github/workflows/deploy-manual.yml deleted file mode 100644 index e127f0e..0000000 --- a/.github/workflows/deploy-manual.yml +++ /dev/null @@ -1,132 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: GitHub.Workflow -# INGROUP: MokoStandards.Deploy -# REPO: https://github.com/mokoconsulting-tech/MokoStandards -# PATH: /templates/workflows/joomla/deploy-manual.yml.template -# VERSION: 04.06.00 -# BRIEF: Manual SFTP deploy to dev server for Joomla repos -# NOTE: Joomla repos use update.xml for distribution. This is for manual -# dev server testing only — triggered via workflow_dispatch. - -name: 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 - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 - with: - php-version: '8.2' - extensions: json, ssh2 - tools: composer - coverage: none - - - name: Setup MokoStandards tools - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' - run: | - git clone --depth 1 --branch version/04 --quiet \ - "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ - /tmp/mokostandards 2>/dev/null || true - if [ -d "/tmp/mokostandards" ] && [ -f "/tmp/mokostandards/composer.json" ]; then - cd /tmp/mokostandards && 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 }} - SUFFIX: ${{ vars.DEV_FTP_SUFFIX }} - 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%/}" - [ -n "$SUFFIX" ] && REMOTE="${REMOTE}/${SUFFIX#/}" - 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/.github/workflows/enterprise-firewall-setup.yml b/.github/workflows/enterprise-firewall-setup.yml deleted file mode 100644 index 1a533fb..0000000 --- a/.github/workflows/enterprise-firewall-setup.yml +++ /dev/null @@ -1,758 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# This file is part of a Moko Consulting project. -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -# FILE INFORMATION -# DEFGROUP: GitHub.Workflow -# INGROUP: MokoStandards.Firewall -# REPO: https://github.com/mokoconsulting-tech/MokoStandards -# PATH: /templates/workflows/shared/enterprise-firewall-setup.yml.template -# VERSION: 04.06.00 -# BRIEF: Enterprise firewall configuration — generates outbound allow-rules including SFTP deployment server -# NOTE: Reads DEV_FTP_HOST / DEV_FTP_PORT variables to include SFTP egress rules alongside HTTPS rules. - -name: Enterprise Firewall Configuration - -# This workflow provides firewall configuration guidance for enterprise-ready sites -# It generates firewall rules for allowing outbound access to trusted domains -# including license providers, documentation sources, package registries, -# and the SFTP deployment server (DEV_FTP_HOST / DEV_FTP_PORT). -# -# Runs automatically when: -# - Coding agent workflows are triggered (pull requests with copilot/ prefix) -# - Manual workflow dispatch for custom configurations - -on: - workflow_dispatch: - inputs: - firewall_type: - description: 'Target firewall type' - required: true - type: choice - options: - - 'iptables' - - 'ufw' - - 'firewalld' - - 'aws-security-group' - - 'azure-nsg' - - 'gcp-firewall' - - 'cloudflare' - - 'all' - default: 'all' - output_format: - description: 'Output format' - required: true - type: choice - options: - - 'shell-script' - - 'json' - - 'yaml' - - 'markdown' - - 'all' - default: 'markdown' - - # Auto-run when coding agent creates or updates PRs - pull_request: - branches: - - 'copilot/**' - - 'agent/**' - types: [opened, synchronize, reopened] - - # Auto-run on push to coding agent branches - push: - branches: - - 'copilot/**' - - 'agent/**' - -permissions: - contents: read - actions: read - -jobs: - generate-firewall-rules: - name: Generate Firewall Rules - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: '3.11' - - - name: Apply Firewall Rules to Runner (Auto-run only) - if: github.event_name != 'workflow_dispatch' - env: - DEV_FTP_HOST: ${{ vars.DEV_FTP_HOST }} - DEV_FTP_PORT: ${{ vars.DEV_FTP_PORT }} - run: | - echo "🔥 Applying firewall rules for coding agent environment..." - echo "" - echo "This step ensures the GitHub Actions runner can access trusted domains" - echo "including license providers, package registries, and documentation sources." - echo "" - - # Note: GitHub Actions runners are ephemeral and run in controlled environments - # This step documents what domains are being accessed during the workflow - # Actual firewall configuration is managed by GitHub - - cat > /tmp/trusted-domains.txt << 'EOF' - # Trusted domains for coding agent environment - # License Providers - www.gnu.org - opensource.org - choosealicense.com - spdx.org - creativecommons.org - apache.org - fsf.org - - # Documentation & Standards - semver.org - keepachangelog.com - conventionalcommits.org - - # GitHub & Related - github.com - api.github.com - docs.github.com - raw.githubusercontent.com - ghcr.io - - # Package Registries - npmjs.com - registry.npmjs.org - pypi.org - files.pythonhosted.org - packagist.org - repo.packagist.org - rubygems.org - - # Platform-Specific - joomla.org - downloads.joomla.org - docs.joomla.org - php.net - getcomposer.org - dolibarr.org - wiki.dolibarr.org - docs.dolibarr.org - - # Moko Consulting - mokoconsulting.tech - - # SFTP Deployment Server (DEV_FTP_HOST) - ${DEV_FTP_HOST:-} - - # Google Services - drive.google.com - docs.google.com - sheets.google.com - accounts.google.com - storage.googleapis.com - fonts.googleapis.com - fonts.gstatic.com - - # GitHub Extended - upload.github.com - objects.githubusercontent.com - user-images.githubusercontent.com - codeload.github.com - pkg.github.com - - # Developer Reference - developer.mozilla.org - stackoverflow.com - git-scm.com - - # CDN & Infrastructure - cdn.jsdelivr.net - unpkg.com - cdnjs.cloudflare.com - img.shields.io - - # Container Registries - hub.docker.com - registry-1.docker.io - - # CI & Code Quality - codecov.io - sonarcloud.io - - # Terraform & Infrastructure - registry.terraform.io - releases.hashicorp.com - checkpoint-api.hashicorp.com - EOF - - echo "✓ Trusted domains documented for this runner" - echo "✓ GitHub Actions runners have network access to these domains" - echo "" - - # Test connectivity to key domains - echo "Testing connectivity to key domains..." - for domain in "github.com" "www.gnu.org" "npmjs.com" "pypi.org"; do - if curl -s --max-time 3 -o /dev/null -w "%{http_code}" "https://$domain" | grep -q "200\|301\|302"; then - echo " ✓ $domain is accessible" - else - echo " ⚠️ $domain connectivity check failed (may be expected)" - fi - done - - # Test SFTP server connectivity (TCP port check) - SFTP_HOST="${DEV_FTP_HOST:-}" - SFTP_PORT="${DEV_FTP_PORT:-22}" - if [ -n "$SFTP_HOST" ]; then - # Strip any embedded :port suffix - SFTP_HOST="${SFTP_HOST%%:*}" - echo "" - echo "Testing SFTP deployment server connectivity..." - if timeout 5 bash -c "echo >/dev/tcp/${SFTP_HOST}/${SFTP_PORT}" 2>/dev/null; then - echo " ✓ SFTP server ${SFTP_HOST}:${SFTP_PORT} is reachable" - else - echo " ⚠️ SFTP server ${SFTP_HOST}:${SFTP_PORT} is not reachable from runner (firewall rule needed)" - fi - else - echo "" - echo " ℹ️ DEV_FTP_HOST not configured — skipping SFTP connectivity check" - fi - - - name: Generate Firewall Configuration - id: generate - env: - DEV_FTP_HOST: ${{ vars.DEV_FTP_HOST }} - DEV_FTP_PORT: ${{ vars.DEV_FTP_PORT }} - run: | - cat > generate_firewall_config.py << 'PYTHON_EOF' - #!/usr/bin/env python3 - """ - Enterprise Firewall Configuration Generator - - Generates firewall rules for enterprise-ready deployments allowing - access to trusted domains including license providers, documentation - sources, package registries, and platform-specific sites. - """ - - import json - import os - import yaml - import sys - from typing import List, Dict - - # SFTP deployment server from org variables - _sftp_host_raw = os.environ.get("DEV_FTP_HOST", "").strip() - _sftp_port = os.environ.get("DEV_FTP_PORT", "").strip() or "22" - # Strip embedded :port suffix if present - _sftp_host = _sftp_host_raw.split(":")[0] if _sftp_host_raw else "" - if ":" in _sftp_host_raw and not _sftp_port: - _sftp_port = _sftp_host_raw.split(":")[1] - - SFTP_HOST = _sftp_host - SFTP_PORT = int(_sftp_port) if _sftp_port.isdigit() else 22 - - # Trusted domains from .github/copilot.yml - TRUSTED_DOMAINS = { - "license_providers": [ - "www.gnu.org", - "opensource.org", - "choosealicense.com", - "spdx.org", - "creativecommons.org", - "apache.org", - "fsf.org", - ], - "documentation_standards": [ - "semver.org", - "keepachangelog.com", - "conventionalcommits.org", - ], - "github_related": [ - "github.com", - "api.github.com", - "docs.github.com", - "raw.githubusercontent.com", - "ghcr.io", - ], - "package_registries": [ - "npmjs.com", - "registry.npmjs.org", - "pypi.org", - "files.pythonhosted.org", - "packagist.org", - "repo.packagist.org", - "rubygems.org", - ], - "standards_organizations": [ - "json-schema.org", - "w3.org", - "ietf.org", - ], - "platform_specific": [ - "joomla.org", - "downloads.joomla.org", - "docs.joomla.org", - "php.net", - "getcomposer.org", - "dolibarr.org", - "wiki.dolibarr.org", - "docs.dolibarr.org", - ], - "moko_consulting": [ - "mokoconsulting.tech", - ], - "google_services": [ - "drive.google.com", - "docs.google.com", - "sheets.google.com", - "accounts.google.com", - "storage.googleapis.com", - "fonts.googleapis.com", - "fonts.gstatic.com", - ], - "github_extended": [ - "upload.github.com", - "objects.githubusercontent.com", - "user-images.githubusercontent.com", - "codeload.github.com", - "pkg.github.com", - ], - "developer_reference": [ - "developer.mozilla.org", - "stackoverflow.com", - "git-scm.com", - ], - "cdn_and_infrastructure": [ - "cdn.jsdelivr.net", - "unpkg.com", - "cdnjs.cloudflare.com", - "img.shields.io", - ], - "container_registries": [ - "hub.docker.com", - "registry-1.docker.io", - ], - "ci_code_quality": [ - "codecov.io", - "sonarcloud.io", - ], - "terraform_infrastructure": [ - "registry.terraform.io", - "releases.hashicorp.com", - "checkpoint-api.hashicorp.com", - ], - } - - # Inject SFTP deployment server as a separate category (port 22, not 443) - if SFTP_HOST: - TRUSTED_DOMAINS["sftp_deployment_server"] = [SFTP_HOST] - print(f"ℹ️ SFTP deployment server: {SFTP_HOST}:{SFTP_PORT}") - - def generate_sftp_iptables_rules(host: str, port: int) -> str: - """Generate iptables rules specifically for SFTP egress""" - return ( - f"# Allow SFTP to deployment server {host}:{port}\n" - f"iptables -A OUTPUT -p tcp -d $(dig +short {host} | head -1)" - f" --dport {port} -j ACCEPT # SFTP deploy\n" - ) - - def generate_sftp_ufw_rules(host: str, port: int) -> str: - """Generate UFW rules for SFTP egress""" - return ( - f"# Allow SFTP to deployment server\n" - f"ufw allow out to $(dig +short {host} | head -1)" - f" port {port} proto tcp comment 'SFTP deploy to {host}'\n" - ) - - def generate_sftp_firewalld_rules(host: str, port: int) -> str: - """Generate firewalld rules for SFTP egress""" - return ( - f"# Allow SFTP to deployment server\n" - f"firewall-cmd --permanent --add-rich-rule='" - f"rule family=ipv4 destination address=$(dig +short {host} | head -1)" - f" port port={port} protocol=tcp accept' # SFTP deploy\n" - ) - - def generate_iptables_rules(domains: List[str]) -> str: - """Generate iptables firewall rules""" - rules = ["#!/bin/bash", "", "# Enterprise Firewall Rules - iptables", ""] - rules.append("# Allow outbound HTTPS to trusted domains") - rules.append("") - - for domain in domains: - rules.append(f"# Allow {domain}") - rules.append(f"iptables -A OUTPUT -p tcp -d $(dig +short {domain} | head -1) --dport 443 -j ACCEPT") - - rules.append("") - rules.append("# Allow DNS lookups") - rules.append("iptables -A OUTPUT -p udp --dport 53 -j ACCEPT") - rules.append("iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT") - - return "\n".join(rules) - - def generate_ufw_rules(domains: List[str]) -> str: - """Generate UFW firewall rules""" - rules = ["#!/bin/bash", "", "# Enterprise Firewall Rules - UFW", ""] - rules.append("# Allow outbound HTTPS to trusted domains") - rules.append("") - - for domain in domains: - rules.append(f"# Allow {domain}") - rules.append(f"ufw allow out to $(dig +short {domain} | head -1) port 443 proto tcp comment 'Allow {domain}'") - - rules.append("") - rules.append("# Allow DNS") - rules.append("ufw allow out 53/udp comment 'Allow DNS UDP'") - rules.append("ufw allow out 53/tcp comment 'Allow DNS TCP'") - - return "\n".join(rules) - - def generate_firewalld_rules(domains: List[str]) -> str: - """Generate firewalld rules""" - rules = ["#!/bin/bash", "", "# Enterprise Firewall Rules - firewalld", ""] - rules.append("# Add trusted domains to firewall") - rules.append("") - - for domain in domains: - rules.append(f"# Allow {domain}") - rules.append(f"firewall-cmd --permanent --add-rich-rule='rule family=ipv4 destination address=$(dig +short {domain} | head -1) port port=443 protocol=tcp accept'") - - rules.append("") - rules.append("# Reload firewall") - rules.append("firewall-cmd --reload") - - return "\n".join(rules) - - def generate_aws_security_group(domains: List[str]) -> Dict: - """Generate AWS Security Group rules (JSON format)""" - rules = { - "SecurityGroupRules": { - "Egress": [] - } - } - - for domain in domains: - rules["SecurityGroupRules"]["Egress"].append({ - "Description": f"Allow HTTPS to {domain}", - "IpProtocol": "tcp", - "FromPort": 443, - "ToPort": 443, - "CidrIp": "0.0.0.0/0", # In practice, resolve to specific IPs - "Tags": [{ - "Key": "Domain", - "Value": domain - }] - }) - - # Add DNS - rules["SecurityGroupRules"]["Egress"].append({ - "Description": "Allow DNS", - "IpProtocol": "udp", - "FromPort": 53, - "ToPort": 53, - "CidrIp": "0.0.0.0/0" - }) - - return rules - - def generate_markdown_documentation(domains_by_category: Dict[str, List[str]]) -> str: - """Generate markdown documentation""" - md = ["# Enterprise Firewall Configuration Guide", ""] - md.append("## Overview") - md.append("") - md.append("This document provides firewall configuration guidance for enterprise-ready deployments.") - md.append("It lists trusted domains that should be whitelisted for outbound access to ensure") - md.append("proper functionality of license validation, package management, and documentation access.") - md.append("") - - md.append("## Trusted Domains by Category") - md.append("") - - all_domains = [] - for category, domains in domains_by_category.items(): - category_name = category.replace("_", " ").title() - md.append(f"### {category_name}") - md.append("") - md.append("| Domain | Purpose |") - md.append("|--------|---------|") - - for domain in domains: - all_domains.append(domain) - purpose = get_domain_purpose(domain) - md.append(f"| `{domain}` | {purpose} |") - - md.append("") - - md.append("## Implementation Examples") - md.append("") - - md.append("### iptables Example") - md.append("") - md.append("```bash") - md.append("# Allow HTTPS to trusted domain") - md.append(f"iptables -A OUTPUT -p tcp -d $(dig +short {all_domains[0]}) --dport 443 -j ACCEPT") - md.append("```") - md.append("") - - md.append("### UFW Example") - md.append("") - md.append("```bash") - md.append("# Allow HTTPS to trusted domain") - md.append(f"ufw allow out to {all_domains[0]} port 443 proto tcp") - md.append("```") - md.append("") - - md.append("### AWS Security Group Example") - md.append("") - md.append("```json") - md.append("{") - md.append(' "IpPermissions": [{') - md.append(' "IpProtocol": "tcp",') - md.append(' "FromPort": 443,') - md.append(' "ToPort": 443,') - md.append(' "IpRanges": [{"CidrIp": "0.0.0.0/0", "Description": "HTTPS to trusted domains"}]') - md.append(" }]") - md.append("}") - md.append("```") - md.append("") - - md.append("## Ports Required") - md.append("") - md.append("| Port | Protocol | Purpose |") - md.append("|------|----------|---------|") - md.append("| 443 | TCP | HTTPS (secure web access) |") - md.append("| 80 | TCP | HTTP (redirects to HTTPS) |") - md.append("| 53 | UDP/TCP | DNS resolution |") - md.append("") - - md.append("## Security Considerations") - md.append("") - md.append("1. **DNS Resolution**: Ensure DNS queries are allowed (port 53 UDP/TCP)") - md.append("2. **Certificate Validation**: HTTPS requires ability to reach certificate authorities") - md.append("3. **Dynamic IPs**: Some domains use CDNs with dynamic IPs - consider using FQDNs in rules") - md.append("4. **Regular Updates**: Review and update whitelist as services change") - md.append("5. **Logging**: Enable logging for blocked connections to identify missing rules") - md.append("") - - md.append("## Compliance Notes") - md.append("") - md.append("- All listed domains provide read-only access to public information") - md.append("- License providers enable GPL compliance verification") - md.append("- Package registries support dependency security scanning") - md.append("- No authentication credentials are transmitted to these domains") - md.append("") - - return "\n".join(md) - - def get_domain_purpose(domain: str) -> str: - """Get human-readable purpose for a domain""" - purposes = { - "www.gnu.org": "GNU licenses and documentation", - "opensource.org": "Open Source Initiative resources", - "choosealicense.com": "GitHub license selection tool", - "spdx.org": "Software Package Data Exchange identifiers", - "creativecommons.org": "Creative Commons licenses", - "apache.org": "Apache Software Foundation licenses", - "fsf.org": "Free Software Foundation resources", - "semver.org": "Semantic versioning specification", - "keepachangelog.com": "Changelog format standards", - "conventionalcommits.org": "Commit message conventions", - "github.com": "GitHub platform access", - "api.github.com": "GitHub API access", - "docs.github.com": "GitHub documentation", - "raw.githubusercontent.com": "GitHub raw content access", - "npmjs.com": "npm package registry", - "pypi.org": "Python Package Index", - "packagist.org": "PHP Composer package registry", - "rubygems.org": "Ruby gems registry", - "joomla.org": "Joomla CMS platform", - "php.net": "PHP documentation and downloads", - "dolibarr.org": "Dolibarr ERP/CRM platform", - } - return purposes.get(domain, "Trusted resource") - - def main(): - # Use inputs if provided (manual dispatch), otherwise use defaults (auto-run) - firewall_type = "${{ github.event.inputs.firewall_type }}" or "all" - output_format = "${{ github.event.inputs.output_format }}" or "markdown" - - print(f"Running in {'manual' if '${{ github.event.inputs.firewall_type }}' else 'automatic'} mode") - print(f"Firewall type: {firewall_type}") - print(f"Output format: {output_format}") - print("") - - # Collect all domains - all_domains = [] - for domains in TRUSTED_DOMAINS.values(): - all_domains.extend(domains) - - # Remove duplicates and sort - all_domains = sorted(set(all_domains)) - - print(f"Generating firewall rules for {len(all_domains)} trusted domains...") - print("") - - # Exclude SFTP server from HTTPS rule generation (different port) - https_domains = [d for d in all_domains if d != SFTP_HOST] - - # Generate based on firewall type - if firewall_type in ["iptables", "all"]: - rules = generate_iptables_rules(https_domains) - if SFTP_HOST: - rules += "\n# ── SFTP Deployment Server ──────────────────────────────\n" - rules += generate_sftp_iptables_rules(SFTP_HOST, SFTP_PORT) - with open("firewall-rules-iptables.sh", "w") as f: - f.write(rules) - print("✓ Generated iptables rules: firewall-rules-iptables.sh") - - if firewall_type in ["ufw", "all"]: - rules = generate_ufw_rules(https_domains) - if SFTP_HOST: - rules += "\n# ── SFTP Deployment Server ──────────────────────────────\n" - rules += generate_sftp_ufw_rules(SFTP_HOST, SFTP_PORT) - with open("firewall-rules-ufw.sh", "w") as f: - f.write(rules) - print("✓ Generated UFW rules: firewall-rules-ufw.sh") - - if firewall_type in ["firewalld", "all"]: - rules = generate_firewalld_rules(https_domains) - if SFTP_HOST: - rules += "\n# ── SFTP Deployment Server ──────────────────────────────\n" - rules += generate_sftp_firewalld_rules(SFTP_HOST, SFTP_PORT) - with open("firewall-rules-firewalld.sh", "w") as f: - f.write(rules) - print("✓ Generated firewalld rules: firewall-rules-firewalld.sh") - - if firewall_type in ["aws-security-group", "all"]: - rules = generate_aws_security_group(all_domains) - with open("firewall-rules-aws-sg.json", "w") as f: - json.dump(rules, f, indent=2) - print("✓ Generated AWS Security Group rules: firewall-rules-aws-sg.json") - - if output_format in ["yaml", "all"]: - with open("trusted-domains.yml", "w") as f: - yaml.dump(TRUSTED_DOMAINS, f, default_flow_style=False) - print("✓ Generated YAML domain list: trusted-domains.yml") - - if output_format in ["json", "all"]: - with open("trusted-domains.json", "w") as f: - json.dump(TRUSTED_DOMAINS, f, indent=2) - print("✓ Generated JSON domain list: trusted-domains.json") - - if output_format in ["markdown", "all"]: - md = generate_markdown_documentation(TRUSTED_DOMAINS) - with open("FIREWALL_CONFIGURATION.md", "w") as f: - f.write(md) - print("✓ Generated documentation: FIREWALL_CONFIGURATION.md") - - print("") - print("Domain Categories:") - for category, domains in TRUSTED_DOMAINS.items(): - print(f" - {category}: {len(domains)} domains") - - print("") - print("Total unique domains: ", len(all_domains)) - - if __name__ == "__main__": - main() - PYTHON_EOF - - chmod +x generate_firewall_config.py - pip install PyYAML - python3 generate_firewall_config.py - - - name: Upload Firewall Configuration Artifacts - uses: actions/upload-artifact@v6 - with: - name: firewall-configurations - path: | - firewall-rules-*.sh - firewall-rules-*.json - trusted-domains.* - FIREWALL_CONFIGURATION.md - retention-days: 90 - - - name: Display Summary - run: | - echo "## Firewall Configuration" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - echo "**Mode**: Manual Execution" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Firewall rules have been generated for enterprise-ready deployments." >> $GITHUB_STEP_SUMMARY - else - echo "**Mode**: Automatic Execution (Coding Agent Active)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "This workflow ran automatically because a coding agent (GitHub Copilot) is active." >> $GITHUB_STEP_SUMMARY - echo "Firewall configuration has been validated for the coding agent environment." >> $GITHUB_STEP_SUMMARY - fi - - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Files Generated" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - if ls firewall-rules-* trusted-domains.* FIREWALL_CONFIGURATION.md 2>/dev/null; then - ls -lh firewall-rules-* trusted-domains.* FIREWALL_CONFIGURATION.md 2>/dev/null | awk '{print "- " $9 " (" $5 ")"}' >> $GITHUB_STEP_SUMMARY - else - echo "- Documentation generated" >> $GITHUB_STEP_SUMMARY - fi - echo "" >> $GITHUB_STEP_SUMMARY - - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - echo "### Download Artifacts" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Download the generated firewall configurations from the workflow artifacts." >> $GITHUB_STEP_SUMMARY - else - echo "### Trusted Domains Active" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "The coding agent has access to:" >> $GITHUB_STEP_SUMMARY - echo "- License providers (GPL, OSI, SPDX, Apache, etc.)" >> $GITHUB_STEP_SUMMARY - echo "- Package registries (npm, PyPI, Packagist, RubyGems)" >> $GITHUB_STEP_SUMMARY - echo "- Documentation sources (GitHub, Joomla, Dolibarr, PHP)" >> $GITHUB_STEP_SUMMARY - echo "- Standards organizations (W3C, IETF, JSON Schema)" >> $GITHUB_STEP_SUMMARY - fi - -# Usage Instructions: -# -# This workflow runs in two modes: -# -# 1. AUTOMATIC MODE (Coding Agent): -# - Triggers when coding agent branches (copilot/**, agent/**) are pushed or PR'd -# - Validates firewall configuration for the coding agent environment -# - Documents accessible domains for compliance -# - Ensures license sources and package registries are available -# -# 2. MANUAL MODE (Enterprise Configuration): -# - Manually trigger from the Actions tab -# - Select desired firewall type and output format -# - Download generated artifacts -# - Apply firewall rules to your enterprise environment -# -# Configuration: -# - Trusted domains are sourced from .github/copilot.yml -# - Modify copilot.yml to add/remove trusted domains -# - Changes automatically propagate to firewall rules -# -# Important Notes: -# - Review generated rules before applying to production -# - Some domains may use CDNs with dynamic IPs -# - Consider using FQDN-based rules where supported -# - Test thoroughly in staging environment first -# - Monitor logs for blocked connections -# - Update rules as domains/services change diff --git a/.github/workflows/repo_health.yml b/.github/workflows/repo_health.yml deleted file mode 100644 index 73308be..0000000 --- a/.github/workflows/repo_health.yml +++ /dev/null @@ -1,795 +0,0 @@ -# ============================================================================ -# 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: GitHub.Workflow -# INGROUP: MokoStandards.Validation -# REPO: https://github.com/mokoconsulting-tech/MokoStandards -# PATH: /.github/workflows/repo_health.yml -# VERSION: 04.06.00 -# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts. -# NOTE: Field is user-managed. -# ============================================================================ - -name: Repo Health - -concurrency: - group: repo-health-${{ github.repository }}-${{ github.ref }} - cancel-in-progress: true - -defaults: - run: - shell: bash - -on: - workflow_dispatch: - inputs: - profile: - description: 'Validation profile: all, release, scripts, or repo' - required: true - default: all - type: choice - options: - - all - - release - - scripts - - repo - pull_request: - push: - -permissions: - contents: read - -env: - # Release policy - Repository Variables Only - RELEASE_REQUIRED_REPO_VARS: RS_FTP_PATH_SUFFIX - RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX - - # Scripts governance policy - # Note: directories listed without a trailing slash. - SCRIPTS_REQUIRED_DIRS: - SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate - - # Repo health policy - # Files are listed as-is; directories must end with a trailing slash. - REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.github/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 (moved to top-level env) - DOCS_INDEX: docs/docs-index.md - SCRIPT_DIR: scripts - WORKFLOWS_DIR: .github/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 - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.GH_TOKEN }} - script: | - const actor = context.actor; - let permission = "unknown"; - let allowed = false; - let method = ""; - - // Hardcoded authorized users — always allowed - const authorizedUsers = ["jmiller-moko", "github-actions[bot]"]; - if (authorizedUsers.includes(actor)) { - allowed = true; - permission = "admin"; - method = "hardcoded allowlist"; - } else { - // Check via API for other actors - try { - const res = await github.rest.repos.getCollaboratorPermissionLevel({ - owner: context.repo.owner, - repo: context.repo.repo, - username: actor, - }); - permission = (res?.data?.permission || "unknown").toLowerCase(); - allowed = permission === "admin" || permission === "maintain"; - method = "repo collaborator API"; - } catch (error) { - core.warning(`Could not fetch permissions for '${actor}': ${error.message}`); - permission = "unknown"; - allowed = false; - method = "API error"; - } - } - - core.setOutput("permission", permission); - core.setOutput("allowed", allowed ? "true" : "false"); - - const lines = [ - "## 🔐 Access Authorization", - "", - "| Field | Value |", - "|-------|-------|", - `| **Actor** | \`${actor}\` |`, - `| **Repository** | \`${context.repo.owner}/${context.repo.repo}\` |`, - `| **Permission** | \`${permission}\` |`, - `| **Method** | ${method} |`, - `| **Authorized** | ${allowed} |`, - `| **Trigger** | \`${context.eventName}\` |`, - `| **Branch** | \`${context.ref.replace('refs/heads/', '')}\` |`, - "", - allowed - ? `✅ ${actor} authorized (${method})` - : `❌ ${actor} is NOT authorized. Requires admin or maintain role, or be in the hardcoded allowlist.`, - ]; - - await core.summary.addRaw(lines.join("\n")).write(); - - - 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 - - release_config: - name: Release configuration - 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: Guardrails release vars - env: - PROFILE_RAW: ${{ github.event.inputs.profile }} - RS_FTP_PATH_SUFFIX: ${{ vars.RS_FTP_PATH_SUFFIX }} - DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }} - run: | - set -euo pipefail - - profile="${PROFILE_RAW:-all}" - case "${profile}" in - all|release|scripts|repo) ;; - *) - printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" - exit 1 - ;; - esac - - if [ "${profile}" = 'scripts' ] || [ "${profile}" = 'repo' ]; then - { - printf '%s\n' '### Release configuration (Repository Variables)' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' 'Status: SKIPPED' - printf '%s\n' 'Reason: profile excludes release validation' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - exit 0 - fi - - IFS=',' read -r -a required <<< "${RELEASE_REQUIRED_REPO_VARS}" - IFS=',' read -r -a optional <<< "${RELEASE_OPTIONAL_REPO_VARS}" - - missing=() - missing_optional=() - - for k in "${required[@]}"; do - v="${!k:-}" - [ -z "${v}" ] && missing+=("${k}") - done - - for k in "${optional[@]}"; do - v="${!k:-}" - [ -z "${v}" ] && missing_optional+=("${k}") - done - - { - printf '%s\n' '### Release configuration (Repository Variables)' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' '| Variable | Status |' - printf '%s\n' '|---|---|' - printf '%s\n' "| RS_FTP_PATH_SUFFIX | ${RS_FTP_PATH_SUFFIX:-NOT SET} |" - printf '%s\n' "| DEV_FTP_SUFFIX | ${DEV_FTP_SUFFIX:-NOT SET} |" - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - - if [ "${#missing_optional[@]}" -gt 0 ]; then - { - printf '%s\n' '### Missing optional repository variables' - for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - if [ "${#missing[@]}" -gt 0 ]; then - { - printf '%s\n' '### Missing required repository variables' - for m in "${missing[@]}"; do printf '%s\n' "- ${m}"; done - printf '%s\n' 'ERROR: Guardrails failed. Missing required repository variables.' - } >> "${GITHUB_STEP_SUMMARY}" - exit 1 - fi - - { - printf '%s\n' '### Repository variables validation result' - printf '%s\n' 'Status: OK' - printf '%s\n' 'All required repository variables present.' - printf '%s\n' '' - printf '%s\n' '**Note**: Organization secrets (RS_FTP_HOST, RS_FTP_USER, etc.) are validated at deployment time, not in repository health checks.' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - - 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|release|scripts|repo) ;; - *) - printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" - exit 1 - ;; - esac - - if [ "${profile}" = 'release' ] || [ "${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 - - IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}" - 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|release|scripts|repo) ;; - *) - printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" - exit 1 - ;; - esac - - if [ "${profile}" = 'release' ] || [ "${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 - - # Source directory: src/ or htdocs/ (either is valid) - if [ -d "src" ]; then - SOURCE_DIR="src" - elif [ -d "htdocs" ]; then - SOURCE_DIR="htdocs" - else - missing_required+=("src/ or htdocs/ (source directory required)") - fi - - IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}" - IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}" - IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}" - IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES}" - - missing_required=() - missing_optional=() - - 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 - - # Optional entries: handle files and directories (trailing slash indicates dir) - 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=() - - # Look for remote branches matching origin/dev*. - # A plain origin/dev is considered invalid; we require 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 there are no dev/* branches, fail the guardrail. - if [ "${#dev_paths[@]}" -eq 0 ]; then - missing_required+=("dev/* branch (e.g. dev/01.00.00)") - fi - - # If a plain dev branch exists (origin/dev), flag it as invalid. - if [ "${#dev_branches[@]}" -gt 0 ]; then - missing_required+=("invalid branch dev (must be dev/)") - 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="$(python3 - <<'PY' - import json - import os - - profile = os.environ.get('PROFILE_RAW') or 'all' - - missing_required = os.environ.get('MISSING_REQUIRED', '').splitlines() if os.environ.get('MISSING_REQUIRED') else [] - missing_optional = os.environ.get('MISSING_OPTIONAL', '').splitlines() if os.environ.get('MISSING_OPTIONAL') else [] - content_warnings = os.environ.get('CONTENT_WARNINGS', '').splitlines() if os.environ.get('CONTENT_WARNINGS') else [] - - out = { - 'profile': profile, - 'missing_required': [x for x in missing_required if x], - 'missing_optional': [x for x in missing_optional if x], - 'content_warnings': [x for x in content_warnings if x], - } - - print(json.dumps(out, indent=2)) - PY - )" - - { - 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=() - - # XML manifest: find any XML file containing tag)") - else - # Check tag exists - if ! grep -qP '' "${MANIFEST}"; then - joomla_findings+=("XML manifest: tag missing") - fi - # Check extension type attribute - if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then - joomla_findings+=("XML manifest: type attribute missing or invalid") - fi - # Check tag - if ! grep -qP '' "${MANIFEST}"; then - joomla_findings+=("XML manifest: tag missing") - fi - # Check tag - if ! grep -qP '' "${MANIFEST}"; then - joomla_findings+=("XML manifest: tag missing") - fi - # Check for Joomla 5+ - if ! grep -qP ' missing (required for Joomla 5+)") - fi - fi - - # Language files: check for at least one .ini file - 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 - - # updates.xml must exist in root (Joomla update server) - if [ ! -f 'updates.xml' ]; then - joomla_findings+=("updates.xml missing in root (required for Joomla update server)") - fi - - # index.html files for directory listing protection - 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 - - 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 - # CODEOWNERS presence - if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then - : - else - extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)") - fi - - # Workflow pinning advisory: flag uses @main/@master - 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 - - # Docs index link integrity (docs/docs-index.md) - if [ -f "${DOCS_INDEX}" ]; then - missing_links="$(python3 - <<'PY' - import os - import re - - idx = os.environ.get('DOCS_INDEX', 'docs/docs-index.md') - base = os.getcwd() - - bad = [] - pat = re.compile(r'\[[^\]]+\]\(([^)]+)\)') - - with open(idx, 'r', encoding='utf-8') as f: - for line in f: - for m in pat.findall(line): - link = m.strip() - if link.startswith('http://') or link.startswith('https://') or link.startswith('#') or link.startswith('mailto:'): - continue - if link.startswith('/'): - rel = link.lstrip('/') - else: - rel = os.path.normpath(os.path.join(os.path.dirname(idx), link)) - rel = rel.split('#', 1)[0] - rel = rel.split('?', 1)[0] - if not rel: - continue - p = os.path.join(base, rel) - if not os.path.exists(p): - bad.append(rel) - - print('\n'.join(sorted(set(bad)))) - PY - )" - 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:' - while IFS= read -r l; do [ -n "${l}" ] && printf '%s\n' "- ${l}"; done <<< "${missing_links}" - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - fi - - # ShellCheck advisory - 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 header advisory for common source types - 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 - - # Git hygiene advisory: branches older than 180 days (remote) - 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 [...] - 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 variables | OK | Repository variables validation |' - 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}" diff --git a/.github/workflows/repository-cleanup.yml b/.github/workflows/repository-cleanup.yml deleted file mode 100644 index ea9219d..0000000 --- a/.github/workflows/repository-cleanup.yml +++ /dev/null @@ -1,525 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# This file is part of a Moko Consulting project. -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: GitHub.Workflow -# INGROUP: MokoStandards.Maintenance -# REPO: https://github.com/mokoconsulting-tech/MokoStandards -# PATH: /templates/workflows/shared/repository-cleanup.yml.template -# VERSION: 04.06.00 -# BRIEF: Recurring repository maintenance — labels, branches, workflows, logs, doc indexes -# NOTE: Synced via bulk-repo-sync to .github/workflows/repository-cleanup.yml in all governed repos. -# Runs on the 1st and 15th of each month at 6:00 AM UTC, and on manual dispatch. - -name: Repository Cleanup - -on: - schedule: - - cron: '0 6 1,15 * *' - workflow_dispatch: - inputs: - reset_labels: - description: 'Delete ALL existing labels and recreate the standard set' - type: boolean - default: false - clean_branches: - description: 'Delete old chore/sync-mokostandards-* branches' - type: boolean - default: true - clean_workflows: - description: 'Delete orphaned workflow runs (cancelled, stale)' - type: boolean - default: true - clean_logs: - description: 'Delete workflow run logs older than 30 days' - type: boolean - default: true - fix_templates: - description: 'Strip copyright comment blocks from issue templates' - type: boolean - default: true - rebuild_indexes: - description: 'Rebuild docs/ index files' - type: boolean - default: true - delete_closed_issues: - description: 'Delete issues that have been closed for more than 30 days' - type: boolean - default: false - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -permissions: - contents: write - issues: write - actions: write - -jobs: - cleanup: - name: Repository Maintenance - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.GH_TOKEN || github.token }} - fetch-depth: 0 - - - name: Check actor permission - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - run: | - ACTOR="${{ github.actor }}" - # Schedule triggers use github-actions[bot] - if [ "${{ github.event_name }}" = "schedule" ]; then - echo "✅ Scheduled run — authorized" - exit 0 - fi - AUTHORIZED_USERS="jmiller-moko github-actions[bot]" - for user in $AUTHORIZED_USERS; do - if [ "$ACTOR" = "$user" ]; then - echo "✅ ${ACTOR} authorized" - exit 0 - fi - done - PERMISSION=$(gh api "repos/${{ github.repository }}/collaborators/${ACTOR}/permission" \ - --jq '.permission' 2>/dev/null) - case "$PERMISSION" in - admin|maintain) echo "✅ ${ACTOR} has ${PERMISSION}" ;; - *) echo "❌ Admin or maintain required"; exit 1 ;; - esac - - # ── Determine which tasks to run ───────────────────────────────────── - # On schedule: run all tasks with safe defaults (labels NOT reset) - # On dispatch: use input toggles - - name: Set task flags - id: tasks - run: | - if [ "${{ github.event_name }}" = "schedule" ]; then - echo "reset_labels=false" >> $GITHUB_OUTPUT - echo "clean_branches=true" >> $GITHUB_OUTPUT - echo "clean_workflows=true" >> $GITHUB_OUTPUT - echo "clean_logs=true" >> $GITHUB_OUTPUT - echo "fix_templates=true" >> $GITHUB_OUTPUT - echo "rebuild_indexes=true" >> $GITHUB_OUTPUT - echo "delete_closed_issues=false" >> $GITHUB_OUTPUT - else - echo "reset_labels=${{ inputs.reset_labels }}" >> $GITHUB_OUTPUT - echo "clean_branches=${{ inputs.clean_branches }}" >> $GITHUB_OUTPUT - echo "clean_workflows=${{ inputs.clean_workflows }}" >> $GITHUB_OUTPUT - echo "clean_logs=${{ inputs.clean_logs }}" >> $GITHUB_OUTPUT - echo "fix_templates=${{ inputs.fix_templates }}" >> $GITHUB_OUTPUT - echo "rebuild_indexes=${{ inputs.rebuild_indexes }}" >> $GITHUB_OUTPUT - echo "delete_closed_issues=${{ inputs.delete_closed_issues }}" >> $GITHUB_OUTPUT - fi - - # ── DELETE RETIRED WORKFLOWS (always runs) ──────────────────────────── - - name: Delete retired workflow files - run: | - echo "## 🗑️ Retired Workflow Cleanup" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - RETIRED=( - ".github/workflows/build.yml" - ".github/workflows/code-quality.yml" - ".github/workflows/release-cycle.yml" - ".github/workflows/release-pipeline.yml" - ".github/workflows/branch-cleanup.yml" - ".github/workflows/auto-update-changelog.yml" - ".github/workflows/enterprise-issue-manager.yml" - ".github/workflows/flush-actions-cache.yml" - ".github/workflows/mokostandards-script-runner.yml" - ".github/workflows/unified-ci.yml" - ".github/workflows/unified-platform-testing.yml" - ".github/workflows/reusable-build.yml" - ".github/workflows/reusable-ci-validation.yml" - ".github/workflows/reusable-deploy.yml" - ".github/workflows/reusable-php-quality.yml" - ".github/workflows/reusable-platform-testing.yml" - ".github/workflows/reusable-project-detector.yml" - ".github/workflows/reusable-release.yml" - ".github/workflows/reusable-script-executor.yml" - ".github/workflows/rebuild-docs-indexes.yml" - ".github/workflows/setup-project-v2.yml" - ".github/workflows/sync-docs-to-project.yml" - ".github/workflows/release.yml" - ".github/workflows/sync-changelogs.yml" - ".github/workflows/version_branch.yml" - "update.json" - ".github/workflows/auto-version-branch.yml" - ".github/workflows/publish-to-mokodolibarr.yml" - ".github/workflows/ci.yml" - ".github/workflows/deploy-rs.yml" - "sftp-config.json" - "sftp-config.json.template" - "scripts/sftp-config" - ) - - DELETED=0 - for wf in "${RETIRED[@]}"; do - if [ -f "$wf" ]; then - git rm "$wf" 2>/dev/null || rm -f "$wf" - echo " Deleted: \`$(basename $wf)\`" >> $GITHUB_STEP_SUMMARY - DELETED=$((DELETED+1)) - fi - done - - if [ "$DELETED" -gt 0 ]; then - git config --local user.email "github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" - git add -A - git commit -m "chore: delete ${DELETED} retired workflow file(s) [skip ci]" \ - --author="github-actions[bot] " - git push - echo "✅ ${DELETED} retired workflow(s) deleted" >> $GITHUB_STEP_SUMMARY - else - echo "✅ No retired workflows found" >> $GITHUB_STEP_SUMMARY - fi - - # ── LABEL RESET ────────────────────────────────────────────────────── - - name: Reset labels to standard set - if: steps.tasks.outputs.reset_labels == 'true' - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - run: | - REPO="${{ github.repository }}" - echo "## 🏷️ Label Reset" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - gh api "repos/${REPO}/labels?per_page=100" --paginate --jq '.[].name' | while read -r label; do - ENCODED=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$label', safe=''))") - gh api -X DELETE "repos/${REPO}/labels/${ENCODED}" --silent 2>/dev/null || true - done - - while IFS='|' read -r name color description; do - [ -z "$name" ] && continue - gh api "repos/${REPO}/labels" \ - -f name="$name" -f color="$color" -f description="$description" \ - --silent 2>/dev/null || true - done << 'LABELS' - joomla|7F52FF|Joomla extension or component - dolibarr|FF6B6B|Dolibarr module or extension - generic|808080|Generic project or library - php|4F5D95|PHP code changes - javascript|F7DF1E|JavaScript code changes - typescript|3178C6|TypeScript code changes - python|3776AB|Python code changes - css|1572B6|CSS/styling changes - html|E34F26|HTML template changes - documentation|0075CA|Documentation changes - ci-cd|000000|CI/CD pipeline changes - docker|2496ED|Docker configuration changes - tests|00FF00|Test suite changes - security|FF0000|Security-related changes - dependencies|0366D6|Dependency updates - config|F9D0C4|Configuration file changes - build|FFA500|Build system changes - automation|8B4513|Automated processes or scripts - mokostandards|B60205|MokoStandards compliance - needs-review|FBCA04|Awaiting code review - work-in-progress|D93F0B|Work in progress, not ready for merge - breaking-change|D73A4A|Breaking API or functionality change - priority: critical|B60205|Critical priority, must be addressed immediately - priority: high|D93F0B|High priority - priority: medium|FBCA04|Medium priority - priority: low|0E8A16|Low priority - type: bug|D73A4A|Something isn't working - type: feature|A2EEEF|New feature or request - type: enhancement|84B6EB|Enhancement to existing feature - type: refactor|F9D0C4|Code refactoring - type: chore|FEF2C0|Maintenance tasks - type: version|0E8A16|Version-related change - status: pending|FBCA04|Pending action or decision - status: in-progress|0E8A16|Currently being worked on - status: blocked|B60205|Blocked by another issue or dependency - status: on-hold|D4C5F9|Temporarily on hold - status: wontfix|FFFFFF|This will not be worked on - size/xs|C5DEF5|Extra small change (1-10 lines) - size/s|6FD1E2|Small change (11-30 lines) - size/m|F9DD72|Medium change (31-100 lines) - size/l|FFA07A|Large change (101-300 lines) - size/xl|FF6B6B|Extra large change (301-1000 lines) - size/xxl|B60205|Extremely large change (1000+ lines) - health: excellent|0E8A16|Health score 90-100 - health: good|FBCA04|Health score 70-89 - health: fair|FFA500|Health score 50-69 - health: poor|FF6B6B|Health score below 50 - standards-update|B60205|MokoStandards sync update - standards-drift|FBCA04|Repository drifted from MokoStandards - sync-report|0075CA|Bulk sync run report - sync-failure|D73A4A|Bulk sync failure requiring attention - push-failure|D73A4A|File push failure requiring attention - health-check|0E8A16|Repository health check results - version-drift|FFA500|Version mismatch detected - deploy-failure|CC0000|Automated deploy failure tracking - template-validation-failure|D73A4A|Template workflow validation failure - version|0E8A16|Version bump or release - LABELS - - echo "✅ Standard labels created" >> $GITHUB_STEP_SUMMARY - - # ── BRANCH CLEANUP ─────────────────────────────────────────────────── - - name: Delete old sync branches - if: steps.tasks.outputs.clean_branches == 'true' - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - run: | - REPO="${{ github.repository }}" - CURRENT="chore/sync-mokostandards-v04.05" - echo "## 🌿 Branch Cleanup" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - FOUND=false - gh api "repos/${REPO}/branches?per_page=100" --jq '.[].name' | \ - grep "^chore/sync-mokostandards" | \ - grep -v "^${CURRENT}$" | while read -r branch; do - gh pr list --repo "$REPO" --head "$branch" --state open --json number --jq '.[].number' 2>/dev/null | while read -r pr; do - gh pr close "$pr" --repo "$REPO" --comment "Superseded by \`${CURRENT}\`" 2>/dev/null || true - echo " Closed PR #${pr}" >> $GITHUB_STEP_SUMMARY - done - gh api -X DELETE "repos/${REPO}/git/refs/heads/${branch}" --silent 2>/dev/null || true - echo " Deleted: \`${branch}\`" >> $GITHUB_STEP_SUMMARY - FOUND=true - done - - if [ "$FOUND" != "true" ]; then - echo "✅ No old sync branches found" >> $GITHUB_STEP_SUMMARY - fi - - # ── WORKFLOW RUN CLEANUP ───────────────────────────────────────────── - - name: Clean up workflow runs - if: steps.tasks.outputs.clean_workflows == 'true' - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - run: | - REPO="${{ github.repository }}" - echo "## 🔄 Workflow Run Cleanup" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - DELETED=0 - # Delete cancelled and stale workflow runs - for status in cancelled stale; do - gh api "repos/${REPO}/actions/runs?status=${status}&per_page=100" \ - --jq '.workflow_runs[].id' 2>/dev/null | while read -r run_id; do - gh api -X DELETE "repos/${REPO}/actions/runs/${run_id}" --silent 2>/dev/null || true - DELETED=$((DELETED+1)) - done - done - - echo "✅ Cleaned cancelled/stale workflow runs" >> $GITHUB_STEP_SUMMARY - - # ── LOG CLEANUP ────────────────────────────────────────────────────── - - name: Delete old workflow run logs - if: steps.tasks.outputs.clean_logs == 'true' - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - run: | - REPO="${{ github.repository }}" - CUTOFF=$(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-30d +%Y-%m-%dT%H:%M:%SZ) - echo "## 📋 Log Cleanup" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Deleting logs older than: ${CUTOFF}" >> $GITHUB_STEP_SUMMARY - - DELETED=0 - gh api "repos/${REPO}/actions/runs?created=<${CUTOFF}&per_page=100" \ - --jq '.workflow_runs[].id' 2>/dev/null | while read -r run_id; do - gh api -X DELETE "repos/${REPO}/actions/runs/${run_id}/logs" --silent 2>/dev/null || true - DELETED=$((DELETED+1)) - done - - echo "✅ Cleaned old workflow run logs" >> $GITHUB_STEP_SUMMARY - - # ── ISSUE TEMPLATE FIX ────────────────────────────────────────────── - - name: Strip copyright headers from issue templates - if: steps.tasks.outputs.fix_templates == 'true' - run: | - echo "## 📋 Issue Template Cleanup" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - FIXED=0 - for f in .github/ISSUE_TEMPLATE/*.md; do - [ -f "$f" ] || continue - if grep -q '^$/d' "$f" - echo " Cleaned: \`$(basename $f)\`" >> $GITHUB_STEP_SUMMARY - FIXED=$((FIXED+1)) - fi - done - - if [ "$FIXED" -gt 0 ]; then - git config --local user.email "github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" - git add .github/ISSUE_TEMPLATE/ - git commit -m "fix: strip copyright comment blocks from issue templates [skip ci]" \ - --author="github-actions[bot] " - git push - echo "✅ ${FIXED} template(s) cleaned and committed" >> $GITHUB_STEP_SUMMARY - else - echo "✅ No templates need cleaning" >> $GITHUB_STEP_SUMMARY - fi - - # ── REBUILD DOC INDEXES ───────────────────────────────────────────── - - name: Rebuild docs/ index files - if: steps.tasks.outputs.rebuild_indexes == 'true' - run: | - echo "## 📚 Documentation Index Rebuild" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ ! -d "docs" ]; then - echo "⏭️ No docs/ directory — skipping" >> $GITHUB_STEP_SUMMARY - exit 0 - fi - - UPDATED=0 - # Generate index.md for each docs/ subdirectory - find docs -type d | while read -r dir; do - INDEX="${dir}/index.md" - FILES=$(find "$dir" -maxdepth 1 -name "*.md" ! -name "index.md" -printf "- [%f](./%f)\n" 2>/dev/null | sort) - if [ -z "$FILES" ]; then - continue - fi - - cat > "$INDEX" << INDEXEOF - # $(basename "$dir") - - ## Documents - - ${FILES} - - --- - *Auto-generated by repository-cleanup workflow* - INDEXEOF - # Dedent - sed -i 's/^ //' "$INDEX" - UPDATED=$((UPDATED+1)) - done - - if [ "$UPDATED" -gt 0 ]; then - git config --local user.email "github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" - git add docs/ - if ! git diff --cached --quiet; then - git commit -m "docs: rebuild documentation indexes [skip ci]" \ - --author="github-actions[bot] " - git push - echo "✅ ${UPDATED} index file(s) rebuilt and committed" >> $GITHUB_STEP_SUMMARY - else - echo "✅ All indexes already up to date" >> $GITHUB_STEP_SUMMARY - fi - else - echo "✅ No indexes to rebuild" >> $GITHUB_STEP_SUMMARY - fi - - # ── VERSION DRIFT DETECTION ────────────────────────────────────────── - - name: Check for version drift - run: | - echo "## 📦 Version Drift Check" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ ! -f "README.md" ]; then - echo "⏭️ No README.md — skipping" >> $GITHUB_STEP_SUMMARY - exit 0 - fi - - README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md 2>/dev/null | head -1) - if [ -z "$README_VERSION" ]; then - echo "⚠️ No VERSION found in README.md FILE INFORMATION block" >> $GITHUB_STEP_SUMMARY - exit 0 - fi - - echo "**README version:** \`${README_VERSION}\`" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - DRIFT=0 - CHECKED=0 - - # Check all files with FILE INFORMATION blocks - while IFS= read -r -d '' file; do - FILE_VERSION=$(grep -oP '^\s*\*?\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' "$file" 2>/dev/null | head -1) - [ -z "$FILE_VERSION" ] && continue - CHECKED=$((CHECKED+1)) - if [ "$FILE_VERSION" != "$README_VERSION" ]; then - echo " ⚠️ \`${file}\`: \`${FILE_VERSION}\` (expected \`${README_VERSION}\`)" >> $GITHUB_STEP_SUMMARY - DRIFT=$((DRIFT+1)) - fi - done < <(find . -maxdepth 4 -type f \( -name "*.php" -o -name "*.md" -o -name "*.yml" \) ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" -print0 2>/dev/null) - - echo "" >> $GITHUB_STEP_SUMMARY - if [ "$DRIFT" -gt 0 ]; then - echo "⚠️ **${DRIFT}** file(s) out of ${CHECKED} have version drift" >> $GITHUB_STEP_SUMMARY - echo "Run \`sync-version-on-merge\` workflow or update manually" >> $GITHUB_STEP_SUMMARY - else - echo "✅ All ${CHECKED} file(s) match README version \`${README_VERSION}\`" >> $GITHUB_STEP_SUMMARY - fi - - # ── PROTECT CUSTOM WORKFLOWS ──────────────────────────────────────── - - name: Ensure custom workflow directory exists - run: | - echo "## 🔧 Custom Workflows" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ ! -d ".github/workflows/custom" ]; then - mkdir -p .github/workflows/custom - cat > .github/workflows/custom/README.md << 'CWEOF' - # Custom Workflows - - Place repo-specific workflows here. Files in this directory are: - - **Never overwritten** by MokoStandards bulk sync - - **Never deleted** by the repository-cleanup workflow - - Safe for custom CI, notifications, or repo-specific automation - - Synced workflows live in `.github/workflows/` (parent directory). - CWEOF - sed -i 's/^ //' .github/workflows/custom/README.md - git config --local user.email "github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" - git add .github/workflows/custom/ - if ! git diff --cached --quiet; then - git commit -m "chore: create .github/workflows/custom/ for repo-specific workflows [skip ci]" \ - --author="github-actions[bot] " - git push - echo "✅ Created \`.github/workflows/custom/\` directory" >> $GITHUB_STEP_SUMMARY - fi - else - CUSTOM_COUNT=$(find .github/workflows/custom -name "*.yml" -o -name "*.yaml" 2>/dev/null | wc -l) - echo "✅ Custom workflow directory exists (${CUSTOM_COUNT} workflow(s))" >> $GITHUB_STEP_SUMMARY - fi - - # ── DELETE CLOSED ISSUES ────────────────────────────────────────────── - - name: Delete old closed issues - if: steps.tasks.outputs.delete_closed_issues == 'true' - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - run: | - REPO="${{ github.repository }}" - CUTOFF=$(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-30d +%Y-%m-%dT%H:%M:%SZ) - echo "## 🗑️ Closed Issue Cleanup" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Deleting issues closed before: ${CUTOFF}" >> $GITHUB_STEP_SUMMARY - - DELETED=0 - gh api "repos/${REPO}/issues?state=closed&since=1970-01-01T00:00:00Z&per_page=100&sort=updated&direction=asc" \ - --jq ".[] | select(.closed_at < \"${CUTOFF}\") | .number" 2>/dev/null | while read -r num; do - # Lock and close with "not_planned" to mark as cleaned up - gh api "repos/${REPO}/issues/${num}/lock" -X PUT -f lock_reason="resolved" --silent 2>/dev/null || true - echo " Locked issue #${num}" >> $GITHUB_STEP_SUMMARY - DELETED=$((DELETED+1)) - done - - if [ "$DELETED" -eq 0 ] 2>/dev/null; then - echo "✅ No old closed issues found" >> $GITHUB_STEP_SUMMARY - else - echo "✅ Locked ${DELETED} old closed issue(s)" >> $GITHUB_STEP_SUMMARY - fi - - - name: Summary - if: always() - run: | - echo "" >> $GITHUB_STEP_SUMMARY - echo "---" >> $GITHUB_STEP_SUMMARY - echo "*Run by @${{ github.actor }} — trigger: ${{ github.event_name }}*" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/standards-compliance.yml b/.github/workflows/standards-compliance.yml deleted file mode 100644 index 79aaedd..0000000 --- a/.github/workflows/standards-compliance.yml +++ /dev/null @@ -1,2614 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# SPDX-License-Identifier: GPL-3.0-or-later -# FILE INFORMATION -# DEFGROUP: GitHub.Workflow -# INGROUP: MokoStandards.Compliance -# REPO: https://github.com/mokoconsulting-tech/MokoStandards -# PATH: /.github/workflows/standards-compliance.yml -# VERSION: 04.06.00 -# BRIEF: MokoStandards compliance validation workflow -# NOTE: Validates repository structure, documentation, and coding standards - -name: Standards Compliance - -# ╔════════════════════════════════════════════════════════════════════════╗ -# ║ MOKOSTANDARDS COMPLIANCE WORKFLOW ║ -# ╠════════════════════════════════════════════════════════════════════════╣ -# ║ ║ -# ║ 28 checks across 4 priority tiers: ║ -# ║ ║ -# ║ TIER 1 — CRITICAL (must pass) ║ -# ║ secret-scanning, license-compliance, repository-structure, ║ -# ║ coding-standards, version-consistency ║ -# ║ ║ -# ║ TIER 2 — IMPORTANT (should pass) ║ -# ║ workflow-validation, documentation-quality, readme-completeness, ║ -# ║ git-hygiene, script-integrity ║ -# ║ ║ -# ║ TIER 3 — QUALITY (code metrics) ║ -# ║ line-length, file-naming, insecure-patterns, complexity, ║ -# ║ duplication, dead-code ║ -# ║ ║ -# ║ TIER 4 — SUPPLEMENTARY (informational) ║ -# ║ file-size, binary, todo, deps, links, api-docs, accessibility, ║ -# ║ performance, enterprise, health, terraform ║ -# ║ ║ -# ║ File size: warning >15MB, critical >20MB ║ -# ║ Exempt: .mmdb, .woff2, .woff, .ttf, .otf ║ -# ║ ║ -# ╚════════════════════════════════════════════════════════════════════════╝ - -env: - WORKFLOW_VERSION: "04.04.01" - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -# MokoStandards Policy Compliance: -# - File formatting: Enforces organizational coding standards -# - Reference: docs/policy/file-formatting.md - -# ┌─────────────────────────────────────────────────────────────────────────┐ -# │ WORKFLOW FLOW DIAGRAM │ -# └─────────────────────────────────────────────────────────────────────────┘ -# -# TRIGGER: Push/PR to main/dev/rc branches -# │ -# ▼ -# ┌──────────────────────────────────────────────────────────────┐ -# │ PARALLEL VALIDATION CHECKS │ -# └──────────────────────────────────────────────────────────────┘ -# │ -# ├─────────────┬──────────────┬──────────────┬────────────┐ -# ▼ ▼ ▼ ▼ ▼ -# ┌─────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ ┌──────────┐ -# │Repository │File Header │Code Style│ │ Docs │ │ License │ -# │Structure│ │ Validation│ │ Check │ │ Check │ │ Check │ -# └─────────┘ └──────────┘ └──────────┘ └─────────┘ └──────────┘ -# │ │ │ │ │ -# ▼ ▼ ▼ ▼ ▼ -# ┌─────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ ┌──────────┐ -# │ Check │ │ Verify │ │ Run │ │ Check │ │ Verify │ -# │Required │ │Copyright │ │ Linters │ │README │ │SPDX-ID │ -# │ Dirs │ │ Header │ │(Python, │ │ Exists │ │ Present │ -# │ │ │ Format │ │PHP,YAML) │ │ │ │ │ -# └─────────┘ └──────────┘ └──────────┘ └─────────┘ └──────────┘ -# │ │ │ │ │ -# └─────────────┴──────────────┴──────────────┴────────────┘ -# │ -# ▼ -# ┌──────────────────┐ -# │ All Checks Pass?│ -# └──────────────────┘ -# │ │ -# YES │ │ NO -# ▼ ▼ -# ┌──────────┐ ┌──────────────┐ -# │ SUCCESS │ │ CREATE ISSUE │ -# │ Summary │ │ with Failure │ -# └──────────┘ │ Details │ -# └──────────────┘ - -on: - push: - branches: [main, dev/**, rc/**, version/**] - pull_request: - branches: [main, dev/**, rc/**] - workflow_dispatch: - -permissions: - contents: read - pull-requests: write - issues: write - -jobs: - # ════════════════════════════════════════════════════════════════════════ - # TIER 1 — CRITICAL (must pass, blocks merge) - # ════════════════════════════════════════════════════════════════════════ - secret-scanning: - name: Secret Scanning - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Scan for Secrets - run: | - set -x - echo "## 🔒 Secret Scanning" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Scanning for hardcoded secrets and credentials." >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Define secret patterns - VIOLATIONS=0 - - # Check for common secret patterns - echo "### Secret Patterns" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Helper: scan with a pattern, show results with file:line, return count - scan_pattern() { - local label="$1" icon="$2" tmpfile="$3" - local count=0 - if [ -f "$tmpfile" ]; then - count=$(wc -l < "$tmpfile") - fi - if [ "$count" -gt 0 ]; then - echo "${icon} **${label}**: ${count} finding(s)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "View locations" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| File | Line | Match |" >> $GITHUB_STEP_SUMMARY - echo "|------|------|-------|" >> $GITHUB_STEP_SUMMARY - head -20 "$tmpfile" | while IFS= read -r line; do - FILE=$(echo "$line" | cut -d: -f1 | sed 's|^\./||') - LINENO=$(echo "$line" | cut -d: -f2) - MATCH=$(echo "$line" | cut -d: -f3- | head -c 80 | sed 's/|/\\|/g') - echo "| \`${FILE}\` | ${LINENO} | \`${MATCH}\` |" >> $GITHUB_STEP_SUMMARY - done - if [ "$count" -gt 20 ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "*... and $((count - 20)) more*" >> $GITHUB_STEP_SUMMARY - fi - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - VIOLATIONS=$((VIOLATIONS + count)) - fi - } - - # Pattern 1: password/secret assignments - grep -r -n -E "(password|passwd|pwd|secret|api[_-]?key|token).*=.*['\"]" . \ - --include="*.php" --include="*.py" --include="*.js" --include="*.ts" \ - --exclude-dir=".git" --exclude-dir="vendor" --exclude-dir="node_modules" 2>/dev/null | \ - grep -v -E '(test|example|sample|getenv|getString|getArgument|config\[|/\.\*/|^\s*//|^\s*\*|CREDENTIAL_PATTERNS|SecurityValidator|SECRET_PATTERN|===|!==|ApiClient|str_contains|gen_wrappers)' | \ - grep -v "= ''" | grep -v '= ""' | grep -v '\$this->config' | \ - grep -v 'type="password"' | grep -v 'type="text"' | grep -v 'name="password"' | grep -v 'name="secretkey"' | \ - grep -v '/dev/null > /tmp/secrets2.txt || true - scan_pattern "Private keys" "❌" /tmp/secrets2.txt - - # Pattern 3: AWS keys - grep -r -n -E "AKIA[0-9A-Z]{16}" . \ - --include="*.php" --include="*.py" --include="*.js" --include="*.txt" --include="*.env" \ - --exclude-dir=".git" --exclude-dir="vendor" --exclude-dir="node_modules" 2>/dev/null > /tmp/secrets3.txt || true - scan_pattern "AWS access keys" "❌" /tmp/secrets3.txt - - # Pattern 4: GitHub tokens - grep -r -n -E "gh[ps]_[a-zA-Z0-9]{36}" . \ - --include="*.php" --include="*.py" --include="*.js" --include="*.txt" --include="*.env" \ - --exclude-dir=".git" --exclude-dir="vendor" --exclude-dir="node_modules" 2>/dev/null > /tmp/secrets4.txt || true - scan_pattern "GitHub tokens" "❌" /tmp/secrets4.txt - - echo "" >> $GITHUB_STEP_SUMMARY - - if [ "$VIOLATIONS" -gt 0 ]; then - echo "**Total Violations**: $VIOLATIONS" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "View detected secrets (file paths only)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - cat /tmp/secrets*.txt 2>/dev/null | cut -d: -f1 | sort -u >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Action Required**: Remove hardcoded secrets immediately!" >> $GITHUB_STEP_SUMMARY - echo "Use environment variables or secrets management instead." >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "✅ No hardcoded secrets detected" >> $GITHUB_STEP_SUMMARY - fi - - license-compliance: - name: License Header Validation - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Check SPDX Headers - run: | - set -x - echo "### SPDX License Header Check" >> $GITHUB_STEP_SUMMARY - - # Count source files with and without SPDX headers - TOTAL_PHP=0 - WITH_SPDX_PHP=0 - - if find . -name "*.php" -type f ! -path "./vendor/*" | head -1 | grep -q .; then - TOTAL_PHP=$(find . -name "*.php" -type f ! -path "./vendor/*" | wc -l) - WITH_SPDX_PHP=$(find . -name "*.php" -type f ! -path "./vendor/*" -exec grep -l "SPDX-License-Identifier" {} \; | wc -l) - fi - - if [ "$TOTAL_PHP" -gt 0 ]; then - PERCENT=$((WITH_SPDX_PHP * 100 / TOTAL_PHP)) - echo "- PHP files: $WITH_SPDX_PHP/$TOTAL_PHP ($PERCENT%) with SPDX headers" >> $GITHUB_STEP_SUMMARY - - if [ "$PERCENT" -lt 80 ]; then - echo "⚠️ Less than 80% of PHP files have SPDX headers" >> $GITHUB_STEP_SUMMARY - else - echo "✅ Good SPDX header coverage" >> $GITHUB_STEP_SUMMARY - fi - fi - - - name: Validate License File - run: | - set -x - echo "" >> $GITHUB_STEP_SUMMARY - echo "### License File Validation" >> $GITHUB_STEP_SUMMARY - - if [ ! -f "LICENSE" ]; then - echo "❌ LICENSE file not found" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### ❌ Validation Failed: LICENSE File Missing" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Error:** LICENSE file is required for all MokoStandards-compliant repositories" >> $GITHUB_STEP_SUMMARY - echo "**Action Required:** Add LICENSE file with appropriate open-source license (GPL-3.0-or-later recommended)" >> $GITHUB_STEP_SUMMARY - echo "" - echo "❌ ERROR: LICENSE file not found - This is a critical requirement" - exit 1 - fi - - # Check license type - if grep -qi "GNU GENERAL PUBLIC LICENSE" LICENSE; then - VERSION=$(grep -i "Version 3" LICENSE || echo "") - if [ -n "$VERSION" ]; then - echo "✅ GPL-3.0-or-later license detected" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ GPL license detected but version unclear" >> $GITHUB_STEP_SUMMARY - fi - elif grep -qi "MIT License" LICENSE; then - echo "✅ MIT license detected" >> $GITHUB_STEP_SUMMARY - elif grep -qi "Apache License" LICENSE; then - echo "✅ Apache license detected" >> $GITHUB_STEP_SUMMARY - else - echo "ℹ️ License type could not be automatically detected" >> $GITHUB_STEP_SUMMARY - fi - - repository-structure: - name: Repository Structure Validation - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Check Required Directories - run: | - set -x - echo "## 📁 Repository Structure Validation" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - MISSING=0 - PRESENT=0 - TOTAL=2 - - echo "### Required Directories" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Directory | Status | Files | Size | Notes |" >> $GITHUB_STEP_SUMMARY - echo "|-----------|--------|-------|------|-------|" >> $GITHUB_STEP_SUMMARY - - # Check required directories - for dir in docs .github; do - if [ -d "$dir" ]; then - FILE_COUNT=$(find "$dir" -type f 2>/dev/null | wc -l) - DIR_SIZE=$(du -sh "$dir" 2>/dev/null | cut -f1) - echo "| $dir/ | ✅ Pass | $FILE_COUNT files | $DIR_SIZE | Complete |" >> $GITHUB_STEP_SUMMARY - PRESENT=$((PRESENT + 1)) - else - echo "| $dir/ | ❌ **Missing** | - | - | **Action Required** |" >> $GITHUB_STEP_SUMMARY - MISSING=$((MISSING + 1)) - fi - done - - echo "" >> $GITHUB_STEP_SUMMARY - PERCENT=$((PRESENT * 100 / TOTAL)) - echo "**Compliance Score:** $PERCENT% ($PRESENT/$TOTAL directories present)" >> $GITHUB_STEP_SUMMARY - - if [ "$MISSING" -gt 0 ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "### 🔴 Critical Issues: $MISSING" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Remediation Steps:**" >> $GITHUB_STEP_SUMMARY - [ ! -d "docs" ] && echo "- Create docs directory: \`mkdir docs && echo '# Documentation' > docs/README.md\`" >> $GITHUB_STEP_SUMMARY - [ ! -d ".github" ] && echo "- Create .github directory: \`mkdir -p .github/workflows\`" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "📚 Reference: [MokoStandards Repository Structure](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/docs/policy/core-structure.md)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### ❌ Validation Failed: Required Directories Missing" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Status:** Repository structure does not meet MokoStandards requirements" >> $GITHUB_STEP_SUMMARY - echo "**Missing:** $MISSING required director(y|ies)" >> $GITHUB_STEP_SUMMARY - echo "**Compliance:** $PERCENT% ($PRESENT/$TOTAL directories present)" >> $GITHUB_STEP_SUMMARY - echo "" - echo "❌ ERROR: Required directories missing - See job summary for remediation steps" - exit 1 - fi - - - name: Check Required Files - run: | - set -x - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Required Files" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - MISSING=0 - PRESENT=0 - TOTAL=5 - - echo "| File | Status | Size | Last Modified | Notes |" >> $GITHUB_STEP_SUMMARY - echo "|------|--------|------|---------------|-------|" >> $GITHUB_STEP_SUMMARY - - # Check required files (CHANGELOG handled separately via find -iname to support src/ChangeLog.md) - for file in README.md LICENSE CONTRIBUTING.md SECURITY.md .editorconfig; do - if [ -f "$file" ]; then - FILE_SIZE=$(wc -c < "$file" 2>/dev/null | awk '{printf "%.1f KB", $1/1024}') - LAST_MOD=$(stat -c %y "$file" 2>/dev/null | cut -d' ' -f1 || echo "Unknown") - CONTENT_CHECK="" - - # Basic content validation - case "$file" in - "README.md") - LINES=$(wc -l < "$file") - [ "$LINES" -lt 10 ] && CONTENT_CHECK="⚠️ Too short" - ;; - "LICENSE") - [ $(wc -c < "$file") -lt 100 ] && CONTENT_CHECK="⚠️ Incomplete?" - ;; - esac - - echo "| $file | ✅ Pass | $FILE_SIZE | $LAST_MOD | Complete $CONTENT_CHECK |" >> $GITHUB_STEP_SUMMARY - PRESENT=$((PRESENT + 1)) - else - echo "| $file | ❌ **Missing** | - | - | **Required** |" >> $GITHUB_STEP_SUMMARY - MISSING=$((MISSING + 1)) - fi - done - - echo "" >> $GITHUB_STEP_SUMMARY - PERCENT=$((PRESENT * 100 / TOTAL)) - echo "**Compliance Score:** $PERCENT% ($PRESENT/$TOTAL files present)" >> $GITHUB_STEP_SUMMARY - - if [ "$MISSING" -gt 0 ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "### 🔴 Critical Issues: $MISSING" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Remediation Steps:**" >> $GITHUB_STEP_SUMMARY - [ ! -f "README.md" ] && echo "- Create README.md: Use [template](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/templates/docs/required/README.md)" >> $GITHUB_STEP_SUMMARY - [ ! -f "LICENSE" ] && echo "- Add LICENSE file: Choose from [OSI-approved licenses](https://opensource.org/licenses)" >> $GITHUB_STEP_SUMMARY - [ ! -f "CONTRIBUTING.md" ] && echo "- Create CONTRIBUTING.md: Use [template](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/templates/docs/required/CONTRIBUTING.md)" >> $GITHUB_STEP_SUMMARY - [ ! -f "SECURITY.md" ] && echo "- Create SECURITY.md: Use [template](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/templates/docs/required/SECURITY.md)" >> $GITHUB_STEP_SUMMARY - [ ! -f ".editorconfig" ] && echo "- Add .editorconfig: Use [template](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/templates/.editorconfig)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "📚 Reference: [MokoStandards File Requirements](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/docs/policy/file-header-standards.md)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### ❌ Validation Failed: Required Files Missing" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Status:** Repository files do not meet MokoStandards requirements" >> $GITHUB_STEP_SUMMARY - echo "**Missing:** $MISSING required file(s)" >> $GITHUB_STEP_SUMMARY - echo "**Compliance:** $PERCENT% ($PRESENT/$TOTAL files present)" >> $GITHUB_STEP_SUMMARY - echo "" - echo "❌ ERROR: Required files missing - See job summary for remediation steps" - exit 1 - fi - - coding-standards: - name: Coding Standards Check - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Check for Tab Characters - run: | - set -x - echo "### Tab Character Detection" >> $GITHUB_STEP_SUMMARY - - # Policy: Tabs are DEFAULT. Only check for tabs in files that REQUIRE spaces. - # Languages requiring spaces: YAML, Python, Haskell, F#, CoffeeScript, Nim, JSON, RST - TABS_IN_SPACES_FILES=$(find . -type f \ - \( -name "*.yml" -o -name "*.yaml" \ - -o -name "*.py" \ - -o -name "*.hs" -o -name "*.lhs" \ - -o -name "*.fs" -o -name "*.fsx" -o -name "*.fsi" \ - -o -name "*.coffee" -o -name "*.litcoffee" \ - -o -name "*.nim" -o -name "*.nims" -o -name "*.nimble" \ - -o -name "*.json" \ - -o -name "*.rst" \) \ - ! -path "./vendor/*" \ - ! -path "./node_modules/*" \ - ! -path "./.git/*" \ - -exec grep -l $'\t' {} \; 2>/dev/null | head -10) - - if [ -n "$TABS_IN_SPACES_FILES" ]; then - echo "⚠️ Tab characters found in files that require spaces:" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "$TABS_IN_SPACES_FILES" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "These languages require spaces (tabs will break): YAML, Python, Haskell, F#, CoffeeScript, Nim, JSON, RST" >> $GITHUB_STEP_SUMMARY - echo "All other files (including .md, .ps1, LICENSE, etc.) may use tabs per MokoStandards policy" >> $GITHUB_STEP_SUMMARY - else - echo "✅ No tabs found in files requiring spaces" >> $GITHUB_STEP_SUMMARY - echo "Note: Tabs are allowed in most files (policy default). Only checked files requiring spaces." >> $GITHUB_STEP_SUMMARY - fi - - - name: Check File Encoding - run: | - set -x - echo "" >> $GITHUB_STEP_SUMMARY - echo "### File Encoding Check" >> $GITHUB_STEP_SUMMARY - - # Check for UTF-8 encoding (ASCII is a subset of UTF-8 and is acceptable) - NON_UTF8=$(find . -type f \( -name "*.php" -o -name "*.js" -o -name "*.md" \) \ - ! -path "./vendor/*" \ - ! -path "./node_modules/*" \ - ! -path "./.git/*" \ - -exec file {} \; | grep -v "UTF-8" | grep -v "ASCII" | head -5) - - if [ -n "$NON_UTF8" ]; then - echo "⚠️ Non-UTF-8 files detected:" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "$NON_UTF8" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - else - echo "✅ All source files appear to be UTF-8 encoded" >> $GITHUB_STEP_SUMMARY - fi - - - name: Check Line Endings - run: | - set -x - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Line Ending Check" >> $GITHUB_STEP_SUMMARY - - # Check for CRLF line endings - CRLF_FILES=$(find . -type f \( -name "*.php" -o -name "*.js" -o -name "*.md" \) \ - ! -path "./vendor/*" \ - ! -path "./node_modules/*" \ - ! -path "./.git/*" \ - -exec file {} \; | grep "CRLF" | head -5) - - if [ -n "$CRLF_FILES" ]; then - echo "⚠️ Files with CRLF line endings found:" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "$CRLF_FILES" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "MokoStandards requires LF line endings" >> $GITHUB_STEP_SUMMARY - else - echo "✅ Line endings are consistent (LF)" >> $GITHUB_STEP_SUMMARY - fi - - version-consistency: - name: Version Consistency Check - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Set up PHP - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 - with: - php-version: '8.1' - extensions: json - tools: composer - coverage: none - - - name: Setup MokoStandards tools - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' - run: | - git clone --depth 1 --branch version/04 --quiet \ - "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ - /tmp/mokostandards 2>/dev/null || true - if [ -d "/tmp/mokostandards" ] && [ -f "/tmp/mokostandards/composer.json" ]; then - cd /tmp/mokostandards - composer install --no-dev --no-interaction --quiet 2>/dev/null || true - fi - - - name: Run Version Consistency Check - id: version_check - run: | - set -x - echo "## 🔢 Version Consistency Validation" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Use MokoStandards tools (no Composer needed on the governed repo) - if [ -f "/tmp/mokostandards/api/validate/check_version_consistency.php" ]; then - php /tmp/mokostandards/api/validate/check_version_consistency.php --path . --verbose 2>&1 | tee /tmp/version-check.log - EXIT_CODE=${PIPESTATUS[0]} - elif [ -f "api/validate/check_version_consistency.php" ]; then - php api/validate/check_version_consistency.php --path . --verbose 2>&1 | tee /tmp/version-check.log - EXIT_CODE=${PIPESTATUS[0]} - else - echo "⏭️ MokoStandards tools not available — skipping version check" >> $GITHUB_STEP_SUMMARY - exit 0 - fi - - echo '```' >> $GITHUB_STEP_SUMMARY - cat /tmp/version-check.log >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - - if [ "$EXIT_CODE" -eq 0 ]; then - echo "✅ All version numbers are consistent" >> $GITHUB_STEP_SUMMARY - else - echo "❌ Version drift detected" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - - # ════════════════════════════════════════════════════════════════════════ - # TIER 2 — IMPORTANT (should pass) - # ════════════════════════════════════════════════════════════════════════ - workflow-validation: - name: Workflow Configuration Check - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Check Required Workflows - run: | - set -x - echo "### GitHub Actions Workflows" >> $GITHUB_STEP_SUMMARY - - WORKFLOWS_DIR=".github/workflows" - - if [ ! -d "$WORKFLOWS_DIR" ]; then - echo "❌ No workflows directory found" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### ❌ Validation Failed: Workflows Directory Missing" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Error:** .github/workflows directory is required for CI/CD automation" >> $GITHUB_STEP_SUMMARY - echo "**Action Required:** Create .github/workflows directory and add GitHub Actions workflows" >> $GITHUB_STEP_SUMMARY - echo "" - echo "❌ ERROR: .github/workflows directory not found" - exit 1 - fi - - # Check for recommended workflows - CI_FOUND=false - for wf in ci.yml build.yml ci-dolibarr.yml ci-joomla.yml; do - if [ -f "$WORKFLOWS_DIR/$wf" ]; then - echo "✅ CI workflow present ($wf)" >> $GITHUB_STEP_SUMMARY - CI_FOUND=true - break - fi - done - if [ "$CI_FOUND" = "false" ]; then - echo "⚠️ No CI workflow found (ci.yml, build.yml, ci-dolibarr.yml, or ci-joomla.yml)" >> $GITHUB_STEP_SUMMARY - fi - - if [ -f "$WORKFLOWS_DIR/codeql-analysis.yml" ]; then - echo "✅ CodeQL security scanning present" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ CodeQL workflow not found" >> $GITHUB_STEP_SUMMARY - fi - - # Check for MokoStandards-synced workflows - for wf in deploy-dev.yml deploy-demo.yml deploy-rs.yml sync-version-on-merge.yml auto-release.yml standards-compliance.yml enterprise-firewall-setup.yml; do - if [ -f "$WORKFLOWS_DIR/$wf" ]; then - echo "✅ ${wf}" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ ${wf} not found (synced from MokoStandards)" >> $GITHUB_STEP_SUMMARY - fi - done - - - name: Validate Workflow Syntax - run: | - set -x - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Workflow YAML Syntax" >> $GITHUB_STEP_SUMMARY - - INVALID=0 - for workflow in $(find .github/workflows -maxdepth 1 -type f \( -name "*.yml" -o -name "*.yaml" \) 2>/dev/null); do - if [ -f "$workflow" ]; then - if python3 -c "import yaml, sys; yaml.safe_load(open(sys.argv[1]))" "$workflow" 2>/dev/null; then - echo "✅ $(basename $workflow)" >> $GITHUB_STEP_SUMMARY - else - echo "❌ $(basename $workflow) - invalid YAML" >> $GITHUB_STEP_SUMMARY - INVALID=$((INVALID + 1)) - fi - fi - done - - if [ "$INVALID" -gt 0 ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "### ❌ Validation Failed: Invalid Workflow YAML Syntax" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Error:** $INVALID workflow file(s) have invalid YAML syntax" >> $GITHUB_STEP_SUMMARY - echo "**Action Required:** Fix YAML syntax errors in the marked workflow files" >> $GITHUB_STEP_SUMMARY - echo "**Tool:** Run \`python3 -c \"import yaml; yaml.safe_load(open('.github/workflows/FILE.yml'))\"\` locally" >> $GITHUB_STEP_SUMMARY - echo "" - echo "❌ ERROR: $INVALID workflow file(s) with invalid YAML syntax" - exit 1 - fi - - echo "" >> $GITHUB_STEP_SUMMARY - echo "### ✅ All Workflow Files Have Valid YAML Syntax" >> $GITHUB_STEP_SUMMARY - echo "" - echo "✅ SUCCESS: All workflow files passed YAML validation" - - - name: Validate CodeQL Configuration - if: hashFiles('.github/workflows/codeql-analysis.yml') != '' - run: | - set -e - echo "" >> $GITHUB_STEP_SUMMARY - echo "### CodeQL Language Configuration" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Inline validation (rewritten from Python to bash for PHP-only architecture) - CODEQL_FILE=".github/workflows/codeql-analysis.yml" - - if [ ! -f "$CODEQL_FILE" ]; then - echo "⚠️ CodeQL workflow file not found" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### ⚠️ CodeQL Workflow Not Found" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Status:** CodeQL workflow file not present - skipping language validation" >> $GITHUB_STEP_SUMMARY - echo "" - echo "⚠️ INFO: CodeQL workflow not found - Skipping validation" - exit 0 - fi - - echo "**CodeQL Configuration Analysis**" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Extract configured languages from workflow - LANGUAGES=$(grep -A5 "language:" "$CODEQL_FILE" | grep -oP "(?<=')[^']+(?=')" | tr '\n' ' ' || echo "") - - # Check if this is a configuration-only scan (no languages specified) - if grep -q "category.*language:config" "$CODEQL_FILE"; then - echo "**Scan Type:** Configuration-only (no language matrix)" >> $GITHUB_STEP_SUMMARY - echo "**Status:** ✅ Valid configuration for PHP-only repository" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "This CodeQL workflow scans YAML, JSON, shell scripts for security issues." >> $GITHUB_STEP_SUMMARY - echo "PHP security is handled by SecurityValidator enterprise library." >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "✅ SUCCESS: CodeQL configuration-only scan properly configured" - exit 0 - fi - - if [ -z "$LANGUAGES" ]; then - echo "❌ No languages configured in CodeQL workflow" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### ❌ Validation Failed: CodeQL Languages Not Configured" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Error:** CodeQL workflow exists but has no languages configured" >> $GITHUB_STEP_SUMMARY - echo "**Action Required:** Configure appropriate languages in codeql-analysis.yml" >> $GITHUB_STEP_SUMMARY - echo "" - echo "❌ ERROR: No languages configured in CodeQL workflow" - exit 1 - fi - - echo "**Configured Languages:** $LANGUAGES" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Validate language presence in repository - INVALID_LANGS="" - VALID_LANGS="" - - for LANG in $LANGUAGES; do - case "$LANG" in - python) - # Check for Python files (should be none in v04.00.04) - if find . -name "*.py" -type f ! -path "./.git/*" | grep -q .; then - VALID_LANGS="$VALID_LANGS python" - echo "✅ Python: Found Python files" >> $GITHUB_STEP_SUMMARY - else - INVALID_LANGS="$INVALID_LANGS python" - echo "❌ Python: No Python files found (PHP-only repository)" >> $GITHUB_STEP_SUMMARY - fi - ;; - javascript|typescript) - # Check for JS/TS files - if find . \( -name "*.js" -o -name "*.ts" -o -name "*.json" \) -type f ! -path "./.git/*" ! -path "./node_modules/*" | grep -q .; then - VALID_LANGS="$VALID_LANGS $LANG" - echo "✅ $LANG: Found JavaScript/TypeScript/JSON files" >> $GITHUB_STEP_SUMMARY - else - INVALID_LANGS="$INVALID_LANGS $LANG" - echo "⚠️ $LANG: No JavaScript/TypeScript files found" >> $GITHUB_STEP_SUMMARY - fi - ;; - java) - if find . -name "*.java" -type f ! -path "./.git/*" | grep -q .; then - VALID_LANGS="$VALID_LANGS java" - echo "✅ Java: Found Java files" >> $GITHUB_STEP_SUMMARY - else - INVALID_LANGS="$INVALID_LANGS java" - echo "⚠️ Java: No Java files found" >> $GITHUB_STEP_SUMMARY - fi - ;; - go) - if find . -name "*.go" -type f ! -path "./.git/*" | grep -q .; then - VALID_LANGS="$VALID_LANGS go" - echo "✅ Go: Found Go files" >> $GITHUB_STEP_SUMMARY - else - INVALID_LANGS="$INVALID_LANGS go" - echo "⚠️ Go: No Go files found" >> $GITHUB_STEP_SUMMARY - fi - ;; - cpp|c) - if find . \( -name "*.cpp" -o -name "*.c" -o -name "*.h" \) -type f ! -path "./.git/*" | grep -q .; then - VALID_LANGS="$VALID_LANGS $LANG" - echo "✅ $LANG: Found C/C++ files" >> $GITHUB_STEP_SUMMARY - else - INVALID_LANGS="$INVALID_LANGS $LANG" - echo "⚠️ $LANG: No C/C++ files found" >> $GITHUB_STEP_SUMMARY - fi - ;; - ruby) - if find . -name "*.rb" -type f ! -path "./.git/*" | grep -q .; then - VALID_LANGS="$VALID_LANGS ruby" - echo "✅ Ruby: Found Ruby files" >> $GITHUB_STEP_SUMMARY - else - INVALID_LANGS="$INVALID_LANGS ruby" - echo "⚠️ Ruby: No Ruby files found" >> $GITHUB_STEP_SUMMARY - fi - ;; - *) - echo "⚠️ $LANG: Unknown language, skipping validation" >> $GITHUB_STEP_SUMMARY - ;; - esac - done - - echo "" >> $GITHUB_STEP_SUMMARY - - # Report results - if [ -n "$INVALID_LANGS" ]; then - echo "**⚠️ Warning:** Some configured languages may not have corresponding files:" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "Invalid languages: $INVALID_LANGS" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Note:** This is informational. CodeQL will skip languages without source files." >> $GITHUB_STEP_SUMMARY - echo "For PHP repository (v04.00.04), JavaScript language covers JSON/YAML/shell scripts." >> $GITHUB_STEP_SUMMARY - else - echo "✅ **All configured CodeQL languages have corresponding source files**" >> $GITHUB_STEP_SUMMARY - fi - - # Always succeed - this is informational only - echo "" >> $GITHUB_STEP_SUMMARY - echo "### ✅ CodeQL Configuration Validation Complete" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Status:** CodeQL language configuration reviewed successfully" >> $GITHUB_STEP_SUMMARY - echo "" - echo "✅ SUCCESS: CodeQL validation complete" - exit 0 - - documentation-quality: - name: Documentation Quality Check - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Validate README.md - run: | - set -x - echo "## 📚 Documentation Quality Check" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### README.md Analysis" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ ! -f "README.md" ]; then - echo "❌ **Critical:** README.md not found" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### ❌ Validation Failed: README.md Missing" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Error:** README.md is required for all MokoStandards-compliant repositories" >> $GITHUB_STEP_SUMMARY - echo "**Action Required:** Create README.md with project description, setup instructions, and usage examples" >> $GITHUB_STEP_SUMMARY - echo "" - echo "❌ ERROR: README.md not found - This is a critical requirement" - exit 1 - fi - - # Detailed content analysis - SIZE=$(wc -c < README.md) - LINES=$(wc -l < README.md) - WORDS=$(wc -w < README.md) - HEADINGS=$(grep -c "^#" README.md || echo 0) - LINKS=$(grep -c "\[.*\](.*)" README.md || echo 0) - CODE_BLOCKS=$(grep -c '```' README.md || echo 0) - - echo "| Metric | Value | Status | Recommendation |" >> $GITHUB_STEP_SUMMARY - echo "|--------|-------|--------|----------------|" >> $GITHUB_STEP_SUMMARY - - # Size check - SIZE_STATUS="✅ Good" - SIZE_REC="Adequate length" - if [ "$SIZE" -lt 500 ]; then - SIZE_STATUS="⚠️ Warning" - SIZE_REC="Add more content (min 500 bytes)" - elif [ "$SIZE" -gt 50000 ]; then - SIZE_STATUS="⚠️ Warning" - SIZE_REC="Consider splitting into multiple docs" - fi - echo "| Size | $SIZE bytes | $SIZE_STATUS | $SIZE_REC |" >> $GITHUB_STEP_SUMMARY - - # Line count - LINES_STATUS="✅ Good" - LINES_REC="Good size" - if [ "$LINES" -lt 20 ]; then - LINES_STATUS="⚠️ Warning" - LINES_REC="Add more sections (min 20 lines)" - fi - echo "| Lines | $LINES | $LINES_STATUS | $LINES_REC |" >> $GITHUB_STEP_SUMMARY - - # Word count - WORDS_STATUS="✅ Good" - WORDS_REC="Good detail" - if [ "$WORDS" -lt 100 ]; then - WORDS_STATUS="⚠️ Warning" - WORDS_REC="Add more description (min 100 words)" - fi - echo "| Words | $WORDS | $WORDS_STATUS | $WORDS_REC |" >> $GITHUB_STEP_SUMMARY - - # Headings - HEADINGS_STATUS="✅ Good" - HEADINGS_REC="Well structured" - if [ "$HEADINGS" -lt 3 ]; then - HEADINGS_STATUS="⚠️ Warning" - HEADINGS_REC="Add more sections (min 3 headings)" - fi - echo "| Headings | $HEADINGS | $HEADINGS_STATUS | $HEADINGS_REC |" >> $GITHUB_STEP_SUMMARY - - # Links - LINKS_STATUS="✅ Good" - LINKS_REC="Includes references" - if [ "$LINKS" -lt 1 ]; then - LINKS_STATUS="ℹ️ Info" - LINKS_REC="Consider adding useful links" - fi - echo "| Links | $LINKS | $LINKS_STATUS | $LINKS_REC |" >> $GITHUB_STEP_SUMMARY - - # Code blocks - CODE_STATUS="✅ Good" - CODE_REC="Includes examples" - if [ "$CODE_BLOCKS" -eq 0 ]; then - CODE_STATUS="ℹ️ Info" - CODE_REC="Consider adding code examples" - fi - echo "| Code blocks | $CODE_BLOCKS | $CODE_STATUS | $CODE_REC |" >> $GITHUB_STEP_SUMMARY - - echo "" >> $GITHUB_STEP_SUMMARY - - # Check for key sections - echo "**Section Coverage:**" >> $GITHUB_STEP_SUMMARY - MISSING_COUNT=0 - grep -qi "install\|setup\|getting started" README.md && echo "- ✅ Installation/Setup instructions" >> $GITHUB_STEP_SUMMARY || { echo "- ⚠️ Missing: Installation/Setup" >> $GITHUB_STEP_SUMMARY; MISSING_COUNT=$((MISSING_COUNT + 1)); } - grep -qi "usage\|example\|how to" README.md && echo "- ✅ Usage examples" >> $GITHUB_STEP_SUMMARY || { echo "- ⚠️ Missing: Usage examples" >> $GITHUB_STEP_SUMMARY; MISSING_COUNT=$((MISSING_COUNT + 1)); } - grep -qi "license" README.md && echo "- ✅ License information" >> $GITHUB_STEP_SUMMARY || { echo "- ⚠️ Missing: License information" >> $GITHUB_STEP_SUMMARY; MISSING_COUNT=$((MISSING_COUNT + 1)); } - grep -qi "contribut" README.md && echo "- ✅ Contributing guidelines" >> $GITHUB_STEP_SUMMARY || echo "- ℹ️ Optional: Contributing section" >> $GITHUB_STEP_SUMMARY - - if [ "$MISSING_COUNT" -gt 0 ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "**⚠️ $MISSING_COUNT important sections missing**" >> $GITHUB_STEP_SUMMARY - fi - - - name: Validate CHANGELOG.md - run: | - set -x - echo "" >> $GITHUB_STEP_SUMMARY - echo "### CHANGELOG.md Analysis" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Locate changelog case-insensitively; accepted at root, src/, or docs/ - CHANGELOG_PATH=$(find . -maxdepth 3 \( -path ./.git -o -path ./node_modules \) -prune \ - -o -iname "changelog.md" -print | head -1 | sed 's|^\./||') - - if [ -z "$CHANGELOG_PATH" ]; then - echo "❌ **Critical:** CHANGELOG.md not found (checked root, src/, docs/)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### ❌ Validation Failed: CHANGELOG.md Missing" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Error:** CHANGELOG.md is required for all MokoStandards-compliant repositories" >> $GITHUB_STEP_SUMMARY - echo "**Action Required:** Create CHANGELOG.md following [Keep a Changelog](https://keepachangelog.com/) format" >> $GITHUB_STEP_SUMMARY - echo "" - echo "❌ ERROR: CHANGELOG.md not found - This is a critical requirement" - exit 1 - fi - - echo "📄 Found: $CHANGELOG_PATH" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Analyze changelog structure - VERSIONS=$(grep -c "## \[" "$CHANGELOG_PATH" || echo 0) - UNRELEASED=$(grep -c "## \[Unreleased\]" "$CHANGELOG_PATH" || echo 0) - DATES=$(grep -c "[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}" "$CHANGELOG_PATH" || echo 0) - SIZE=$(wc -c < "$CHANGELOG_PATH") - - echo "| Metric | Value | Status | Notes |" >> $GITHUB_STEP_SUMMARY - echo "|--------|-------|--------|-------|" >> $GITHUB_STEP_SUMMARY - - # Check format - if grep -qi "## \[.*\]" "$CHANGELOG_PATH"; then - echo "| Format | Keep a Changelog | ✅ Pass | Standard format |" >> $GITHUB_STEP_SUMMARY - else - echo "| Format | Custom | ⚠️ Warning | Consider [Keep a Changelog](https://keepachangelog.com/) |" >> $GITHUB_STEP_SUMMARY - fi - - # Version count - VERSIONS_STATUS="✅ Good" - VERSIONS_NOTE="Well maintained" - if [ "$VERSIONS" -lt 1 ]; then - VERSIONS_STATUS="⚠️ Warning" - VERSIONS_NOTE="Add version entries" - fi - echo "| Versions | $VERSIONS | $VERSIONS_STATUS | $VERSIONS_NOTE |" >> $GITHUB_STEP_SUMMARY - - # Unreleased section - if [ "$UNRELEASED" -gt 0 ]; then - echo "| Unreleased | Yes | ✅ Good | Active development tracked |" >> $GITHUB_STEP_SUMMARY - else - echo "| Unreleased | No | ℹ️ Info | Consider adding [Unreleased] section |" >> $GITHUB_STEP_SUMMARY - fi - - # Dates - DATES_STATUS="✅ Good" - if [ "$DATES" -lt 1 ]; then - DATES_STATUS="⚠️ Warning" - DATES_NOTE="Add release dates" - else - DATES_NOTE="Dates present" - fi - echo "| Release dates | $DATES | $DATES_STATUS | $DATES_NOTE |" >> $GITHUB_STEP_SUMMARY - - # Check for standard sections - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Changelog Sections:**" >> $GITHUB_STEP_SUMMARY - grep -qi "### Added" "$CHANGELOG_PATH" && echo "- ✅ Added section" >> $GITHUB_STEP_SUMMARY || echo "- ℹ️ Added section (optional)" >> $GITHUB_STEP_SUMMARY - grep -qi "### Changed" "$CHANGELOG_PATH" && echo "- ✅ Changed section" >> $GITHUB_STEP_SUMMARY || echo "- ℹ️ Changed section (optional)" >> $GITHUB_STEP_SUMMARY - grep -qi "### Fixed" "$CHANGELOG_PATH" && echo "- ✅ Fixed section" >> $GITHUB_STEP_SUMMARY || echo "- ℹ️ Fixed section (optional)" >> $GITHUB_STEP_SUMMARY - - echo "" >> $GITHUB_STEP_SUMMARY - echo "📚 Reference: [Keep a Changelog](https://keepachangelog.com/)" >> $GITHUB_STEP_SUMMARY - - - name: Check Documentation Index - run: | - set -x - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Documentation Index" >> $GITHUB_STEP_SUMMARY - - if [ -f "docs/index.md" ] || [ -f "docs/README.md" ]; then - echo "✅ Documentation index found" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ No documentation index (docs/index.md or docs/README.md)" >> $GITHUB_STEP_SUMMARY - fi - - readme-completeness: - name: README Completeness Check - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Check README Sections - run: | - set -x - echo "## 📄 README Completeness Check" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ ! -f "README.md" ]; then - echo "❌ README.md not found" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - # Required sections - REQUIRED_SECTIONS=("Installation" "Usage" "Contributing" "License") - MISSING=0 - PRESENT=0 - - echo "### Required Sections" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - for section in "${REQUIRED_SECTIONS[@]}"; do - if grep -qi "##.*$section" README.md; then - echo "✅ $section" >> $GITHUB_STEP_SUMMARY - PRESENT=$((PRESENT + 1)) - else - echo "❌ $section" >> $GITHUB_STEP_SUMMARY - MISSING=$((MISSING + 1)) - fi - done - - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Completeness**: $PRESENT/${#REQUIRED_SECTIONS[@]} required sections present" >> $GITHUB_STEP_SUMMARY - - if [ "$MISSING" -gt 0 ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Action Required**: Add missing sections to README.md" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - # ============================================================================ - # PHASE 3: Future Enhancements - # ============================================================================ - - git-hygiene: - name: Git Repository Hygiene - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - fetch-depth: 0 - - - name: Check .gitignore - run: | - set -x - echo "### .gitignore Validation" >> $GITHUB_STEP_SUMMARY - - if [ ! -f ".gitignore" ]; then - echo "⚠️ .gitignore file not found" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### ⚠️ Warning: .gitignore Not Found" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Status:** .gitignore file is recommended but not required" >> $GITHUB_STEP_SUMMARY - echo "**Recommendation:** Add .gitignore to exclude build artifacts, dependencies, and temporary files" >> $GITHUB_STEP_SUMMARY - echo "" - echo "⚠️ WARNING: .gitignore file not found - Continuing validation" - exit 0 - fi - - # Check for common exclusions - MISSING="" - grep -q "vendor/" .gitignore || MISSING="${MISSING}vendor/ " - grep -q "node_modules/" .gitignore || MISSING="${MISSING}node_modules/ " - - if [ -n "$MISSING" ]; then - echo "⚠️ .gitignore may be missing common exclusions: $MISSING" >> $GITHUB_STEP_SUMMARY - else - echo "✅ .gitignore appears complete" >> $GITHUB_STEP_SUMMARY - fi - - - name: Check for Large Files - run: | - set -x - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Large File Detection" >> $GITHUB_STEP_SUMMARY - - # Find files larger than 1MB - LARGE_FILES=$(find . -type f -size +1M ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" | head -5) - - if [ -n "$LARGE_FILES" ]; then - echo "⚠️ Large files detected (>1MB):" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "$LARGE_FILES" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "Consider using Git LFS for large binary files" >> $GITHUB_STEP_SUMMARY - else - echo "✅ No unusually large files detected" >> $GITHUB_STEP_SUMMARY - fi - - script-integrity: - name: Script Integrity Validation - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: '3.x' - - - name: Validate Script Integrity - id: script_check - run: | - set -x - echo "## 🔐 Script Integrity Validation" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ -f "api/.script-registry.json" ]; then - echo "### Critical Scripts" >> $GITHUB_STEP_SUMMARY - php api/maintenance/update_sha_hashes.php \ - --dry-run --verbose | tee /tmp/script-validation.log - - EXIT_CODE=$? - - echo "" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - cat /tmp/script-validation.log >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - - if [ "$EXIT_CODE" -eq 0 ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "✅ All critical scripts validated successfully!" >> $GITHUB_STEP_SUMMARY - exit 0 - else - echo "" >> $GITHUB_STEP_SUMMARY - echo "❌ Script integrity violations detected" >> $GITHUB_STEP_SUMMARY - echo "**Action Required:** Review validation report and update registry" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - else - echo "ℹ️ Script registry not found - skipping integrity check" >> $GITHUB_STEP_SUMMARY - exit 0 - fi - - - # ════════════════════════════════════════════════════════════════════════ - # TIER 3 — QUALITY (code quality metrics) - # ════════════════════════════════════════════════════════════════════════ - line-length-validation: - name: Line Length Check - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Check Line Lengths - run: | - set -x - echo "## 📏 Line Length Validation" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Line length standards: - # - General source code: 120 characters (hard limit) - # - YAML workflows: 180 characters (exception for GitHub Actions) - # - Markdown files: No limit (content-focused) - - echo "### Line Length Standards" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| File Type | Soft Limit | Hard Limit |" >> $GITHUB_STEP_SUMMARY - echo "|-----------|------------|------------|" >> $GITHUB_STEP_SUMMARY - echo "| General source code | 80 chars | 120 chars |" >> $GITHUB_STEP_SUMMARY - echo "| YAML workflows | 80 chars | 180 chars |" >> $GITHUB_STEP_SUMMARY - echo "| Markdown files | N/A | No limit |" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Check YAML files (using yamllint which is already configured) - echo "### YAML Files (180 char limit)" >> $GITHUB_STEP_SUMMARY - - YAML_VIOLATIONS=0 - if command -v yamllint >/dev/null 2>&1; then - # Install yamllint if not present - : - else - pip install yamllint >/dev/null 2>&1 - fi - - # Run yamllint and count line-length warnings - YAML_OUTPUT=$(yamllint .github/workflows/*.yml 2>&1 | grep "line too long" || true) - if [ -n "$YAML_OUTPUT" ]; then - YAML_VIOLATIONS=$(echo "$YAML_OUTPUT" | wc -l) - echo "⚠️ Found $YAML_VIOLATIONS lines exceeding 180 characters in YAML files" >> $GITHUB_STEP_SUMMARY - echo "
View warnings (informational only)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "$YAML_OUTPUT" | head -20 >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - else - echo "✅ All YAML files comply with 180 character limit" >> $GITHUB_STEP_SUMMARY - fi - echo "" >> $GITHUB_STEP_SUMMARY - - # Check source code files (PHP, Python, JavaScript, etc.) for 120 char limit - echo "### Source Code Files (120 char limit)" >> $GITHUB_STEP_SUMMARY - - LONG_LINES=$(find . -type f \ - \( -name "*.php" -o -name "*.py" -o -name "*.js" -o -name "*.ts" \ - -o -name "*.go" -o -name "*.rs" -o -name "*.java" -o -name "*.c" \ - -o -name "*.cpp" -o -name "*.h" -o -name "*.sh" \) \ - ! -path "./vendor/*" \ - ! -path "./node_modules/*" \ - ! -path "./.git/*" \ - ! -path "./build/*" \ - ! -path "./dist/*" \ - -exec awk 'length > 120 { print FILENAME ":" NR ": " length " chars" }' {} \; 2>/dev/null | head -20) - - if [ -n "$LONG_LINES" ]; then - LINE_COUNT=$(echo "$LONG_LINES" | wc -l) - echo "⚠️ Found $LINE_COUNT source code lines exceeding 120 characters" >> $GITHUB_STEP_SUMMARY - echo "
View violations (informational)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "$LONG_LINES" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - else - echo "✅ All source code files comply with 120 character limit" >> $GITHUB_STEP_SUMMARY - fi - echo "" >> $GITHUB_STEP_SUMMARY - - # Confirm Markdown files are not checked - echo "### Markdown Files" >> $GITHUB_STEP_SUMMARY - echo "✅ Markdown files have no line length limit per coding standards" >> $GITHUB_STEP_SUMMARY - echo "Rationale: Content-focused format, URLs, tables, and natural prose flow" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Summary - echo "### Summary" >> $GITHUB_STEP_SUMMARY - echo "This check is **informational only** and does not block merges." >> $GITHUB_STEP_SUMMARY - echo "Line length standards help maintain code readability." >> $GITHUB_STEP_SUMMARY - echo "Exceptions documented in: \`docs/policy/coding-style-guide.md\`" >> $GITHUB_STEP_SUMMARY - - file-naming-standards: - name: File Naming Standards - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Check File Naming - run: | - set -x - echo "## 📝 File Naming Standards" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - VIOLATIONS=0 - - # Check PHP files (should be PascalCase for classes) - INVALID_PHP=$(find . -name "*.php" ! -path "./vendor/*" ! -path "./.git/*" ! -regex ".*/[A-Z][a-zA-Z0-9]*\.php" ! -name "index.php" ! -name "functions.php" | wc -l || echo 0) - - # Check config files (should be kebab-case) - INVALID_CONFIG=$(find . -name "*.yml" -o -name "*.yaml" -o -name "*.json" ! -path "./vendor/*" ! -path "./.git/*" ! -path "./node_modules/*" | grep -E "[A-Z_]" | wc -l || echo 0) - - echo "### Naming Violations" >> $GITHUB_STEP_SUMMARY - echo "- **PHP files not PascalCase**: $INVALID_PHP" >> $GITHUB_STEP_SUMMARY - echo "- **Config files not kebab-case**: $INVALID_CONFIG" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - VIOLATIONS=$((INVALID_PHP + INVALID_CONFIG)) - - if [ "$VIOLATIONS" -gt 0 ]; then - echo "⚠️ Found $VIOLATIONS naming convention violation(s)" >> $GITHUB_STEP_SUMMARY - echo "**Recommendation**: Follow naming conventions for consistency" >> $GITHUB_STEP_SUMMARY - else - echo "✅ File naming conventions followed" >> $GITHUB_STEP_SUMMARY - fi - - insecure-patterns: - name: Insecure Code Pattern Detection - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Scan for Insecure Patterns - run: | - set -x - echo "## 🔒 Insecure Code Pattern Detection" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - VIOLATIONS=0 - - # PHP: SQL injection patterns - if grep -r -n "\\$_\(GET\|POST\|REQUEST\).*mysql_query\|mysqli_query" . --include="*.php" ! -path "./vendor/*" 2>/dev/null > /tmp/sql_inject.txt; then - COUNT=$(wc -l < /tmp/sql_inject.txt) - echo "⚠️ Found $COUNT potential SQL injection pattern(s)" >> $GITHUB_STEP_SUMMARY - VIOLATIONS=$((VIOLATIONS + COUNT)) - fi - - # PHP: eval/exec usage - if grep -r -n "eval\|exec\|system\|passthru\|shell_exec" . --include="*.php" ! -path "./vendor/*" 2>/dev/null > /tmp/exec.txt; then - COUNT=$(wc -l < /tmp/exec.txt) - echo "⚠️ Found $COUNT dangerous function call(s)" >> $GITHUB_STEP_SUMMARY - VIOLATIONS=$((VIOLATIONS + COUNT)) - fi - - # Python: eval usage - if grep -r -n "eval(" . --include="*.py" 2>/dev/null > /tmp/py_eval.txt; then - COUNT=$(wc -l < /tmp/py_eval.txt) - echo "⚠️ Found $COUNT Python eval() usage(s)" >> $GITHUB_STEP_SUMMARY - VIOLATIONS=$((VIOLATIONS + COUNT)) - fi - - echo "" >> $GITHUB_STEP_SUMMARY - - if [ "$VIOLATIONS" -gt 0 ]; then - echo "**Total Violations**: $VIOLATIONS" >> $GITHUB_STEP_SUMMARY - echo "**Recommendation**: Review and secure flagged patterns" >> $GITHUB_STEP_SUMMARY - else - echo "✅ No insecure patterns detected" >> $GITHUB_STEP_SUMMARY - fi - - code-complexity: - name: Code Complexity Analysis - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Setup PHP - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 - with: - php-version: '8.1' - - - name: Analyze Complexity - run: | - set -x - echo "## 📊 Code Complexity Analysis" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - PHP_COUNT=$(find . -name "*.php" ! -path "./vendor/*" ! -path "./.git/*" | wc -l) - - if [ "$PHP_COUNT" -gt 0 ]; then - # Install phploc - wget https://phar.phpunit.de/phploc.phar 2>/dev/null - chmod +x phploc.phar - - echo "### PHP Code Metrics" >> $GITHUB_STEP_SUMMARY - if ./phploc.phar --exclude vendor --exclude .git . 2>&1 | tee /tmp/phploc.txt; then - COMPLEXITY=$(grep "Cyclomatic Complexity" /tmp/phploc.txt | grep "Average" | awk '{print $NF}' || echo "N/A") - echo "**Average Cyclomatic Complexity**: $COMPLEXITY" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ "$COMPLEXITY" != "N/A" ] && [ $(echo "$COMPLEXITY > 10" | bc -l) -eq 1 ]; then - echo "⚠️ Average complexity exceeds recommended threshold (10)" >> $GITHUB_STEP_SUMMARY - echo "**Recommendation**: Refactor complex functions" >> $GITHUB_STEP_SUMMARY - else - echo "✅ Code complexity within acceptable limits" >> $GITHUB_STEP_SUMMARY - fi - fi - else - echo "ℹ️ No PHP files found for complexity analysis" >> $GITHUB_STEP_SUMMARY - fi - - code-duplication: - name: Code Duplication Detection - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Setup PHP - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 - with: - php-version: '8.1' - - - name: Detect Duplicates - run: | - set -x - echo "## 🔁 Code Duplication Detection" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Check if PHP files exist - PHP_COUNT=$(find . -name "*.php" ! -path "./vendor/*" ! -path "./.git/*" | wc -l) - - if [ "$PHP_COUNT" -gt 0 ]; then - echo "### PHP Code Duplication" >> $GITHUB_STEP_SUMMARY - - # Install phpcpd - wget https://phar.phpunit.de/phpcpd.phar 2>/dev/null - chmod +x phpcpd.phar - - # Run duplication detection - if ./phpcpd.phar --exclude vendor --exclude .git . 2>&1 | tee /tmp/phpcpd.txt; then - DUPLICATION=$(grep "Found" /tmp/phpcpd.txt | grep -oE "[0-9]+\.[0-9]+%" | head -1 || echo "0.00%") - echo "📊 **Duplication Rate**: $DUPLICATION" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - DUPLICATION_NUM=$(echo "$DUPLICATION" | sed 's/%//') - if [ $(echo "$DUPLICATION_NUM > 5.0" | bc -l) -eq 1 ]; then - echo "⚠️ Code duplication exceeds 5% threshold" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "View duplication details" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - cat /tmp/phpcpd.txt >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - else - echo "✅ Code duplication within acceptable limits (<5%)" >> $GITHUB_STEP_SUMMARY - fi - else - echo "✅ No significant code duplication detected" >> $GITHUB_STEP_SUMMARY - fi - else - echo "ℹ️ No PHP files found for duplication analysis" >> $GITHUB_STEP_SUMMARY - fi - - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Note**: This is an informational check to encourage DRY principles." >> $GITHUB_STEP_SUMMARY - - dead-code-detection: - name: Dead Code Detection - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Setup Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: '3.x' - - - name: Detect Dead Code - run: | - set -x - echo "## 🗑️ Dead Code Detection" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - PY_COUNT=$(find . -name "*.py" ! -path "./vendor/*" ! -path "./.git/*" ! -path "./venv/*" | wc -l) - - if [ "$PY_COUNT" -gt 0 ]; then - pip install vulture 2>/dev/null - echo "### Python Dead Code" >> $GITHUB_STEP_SUMMARY - - if vulture . --exclude vendor,venv,.git 2>&1 | tee /tmp/vulture.txt; then - DEAD_COUNT=$(wc -l < /tmp/vulture.txt || echo 0) - if [ "$DEAD_COUNT" -gt 0 ]; then - echo "⚠️ Found $DEAD_COUNT potential dead code item(s)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "View dead code" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - head -50 /tmp/vulture.txt >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - else - echo "✅ No dead code detected" >> $GITHUB_STEP_SUMMARY - fi - fi - else - echo "ℹ️ No Python files found for dead code analysis" >> $GITHUB_STEP_SUMMARY - fi - - - # ════════════════════════════════════════════════════════════════════════ - # TIER 4 — SUPPLEMENTARY (informational) - # ════════════════════════════════════════════════════════════════════════ - file-size-limits: - name: File Size Limits - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Check File Sizes - run: | - set -x - echo "## 📦 File Size Validation" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Exempt file types (allowed to be large) - EXEMPT="! -name *.mmdb ! -name *.woff2 ! -name *.woff ! -name *.ttf ! -name *.otf" - - # Find large files (>15MB warning, >20MB critical) - LARGE_FILES=$(find . -type f -size +15M $EXEMPT ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" 2>/dev/null | wc -l) - HUGE_FILES=$(find . -type f -size +20M $EXEMPT ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" 2>/dev/null | wc -l) - - echo "### Size Thresholds" >> $GITHUB_STEP_SUMMARY - echo "- **Warning**: Files >15MB" >> $GITHUB_STEP_SUMMARY - echo "- **Critical**: Files >20MB" >> $GITHUB_STEP_SUMMARY - echo "- **Exempt**: .mmdb, .woff2, .woff, .ttf, .otf" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ "$HUGE_FILES" -gt 0 ]; then - echo "❌ **Critical**: Found $HUGE_FILES file(s) exceeding 20MB" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "View files >20MB" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - find . -type f -size +20M $EXEMPT ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" -exec ls -lh {} + 2>/dev/null | awk '{print $5, $9}' >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Action Required**: Remove or optimize files >20MB" >> $GITHUB_STEP_SUMMARY - exit 1 - elif [ "$LARGE_FILES" -gt 0 ]; then - echo "⚠️ **Warning**: Found $LARGE_FILES file(s) between 15MB and 20MB" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "View files >15MB" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - find . -type f -size +15M $EXEMPT ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" -exec ls -lh {} + 2>/dev/null | awk '{print $5, $9}' >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Recommendation**: Consider optimizing large files" >> $GITHUB_STEP_SUMMARY - else - echo "✅ All files within acceptable size limits" >> $GITHUB_STEP_SUMMARY - fi - - binary-file-detection: - name: Binary File Detection - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Detect Binary Files - run: | - set -x - echo "## 🔍 Binary File Detection" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Find binary files excluding allowed types - BINARIES=$(find . -type f ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" \ - ! -name "*.png" ! -name "*.jpg" ! -name "*.jpeg" ! -name "*.gif" ! -name "*.svg" ! -name "*.ico" \ - ! -name "*.woff" ! -name "*.woff2" ! -name "*.ttf" ! -name "*.eot" \ - -exec file {} \; | grep -v "text" | grep -v "empty" | wc -l || echo 0) - - if [ "$BINARIES" -gt 0 ]; then - echo "⚠️ Found $BINARIES non-image binary file(s)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "View binary files" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - find . -type f ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" \ - ! -name "*.png" ! -name "*.jpg" ! -name "*.jpeg" ! -name "*.gif" ! -name "*.svg" ! -name "*.ico" \ - ! -name "*.woff" ! -name "*.woff2" ! -name "*.ttf" ! -name "*.eot" \ - -exec file {} \; | grep -v "text" | grep -v "empty" | cut -d: -f1 >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Recommendation**: Source control should primarily contain text files" >> $GITHUB_STEP_SUMMARY - else - echo "✅ No unexpected binary files detected" >> $GITHUB_STEP_SUMMARY - fi - - # ============================================================================ - # PHASE 4: Nice to Have Checks - # ============================================================================ - - todo-fixme-tracking: - name: TODO/FIXME Tracking - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Track Technical Debt - run: | - set -x - echo "## 📝 TODO/FIXME Tracking" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Tracking technical debt markers in source code." >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Search for technical debt markers - PATTERNS="TODO|FIXME|HACK|XXX" - EXTENSIONS="*.php *.py *.js *.ts *.go *.rs *.java *.c *.cpp *.h *.hpp *.sh" - - echo "### Technical Debt Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - TOTAL_COUNT=0 - for ext in $EXTENSIONS; do - COUNT=$(find . -type f -name "$ext" ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" -exec grep -n -E "($PATTERNS)" {} + 2>/dev/null | wc -l || echo 0) - TOTAL_COUNT=$((TOTAL_COUNT + COUNT)) - done - - if [ "$TOTAL_COUNT" -gt 0 ]; then - echo "⚠️ Found **$TOTAL_COUNT** technical debt item(s)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "View technical debt items" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - for ext in $EXTENSIONS; do - find . -type f -name "$ext" ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" -exec grep -n -H -E "($PATTERNS)" {} + 2>/dev/null | head -100 || true - done >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - else - echo "✅ No technical debt markers found" >> $GITHUB_STEP_SUMMARY - fi - - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Note**: This is an informational check. Technical debt items don't block compliance." >> $GITHUB_STEP_SUMMARY - - dependency-vulnerabilities: - name: Dependency Vulnerability Scanning - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Setup PHP - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 - with: - php-version: '8.1' - - - name: Setup Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: '3.x' - - - name: Scan Dependencies - run: | - set -x - echo "## 🛡️ Dependency Vulnerability Scanning" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - VULNERABILITIES=0 - - # PHP Dependencies - if [ -f "composer.json" ]; then - echo "### PHP Dependencies (composer)" >> $GITHUB_STEP_SUMMARY - if composer audit --no-dev 2>&1 | tee /tmp/php_audit.txt; then - echo "✅ No PHP vulnerabilities detected" >> $GITHUB_STEP_SUMMARY - else - VULN_COUNT=$(grep -c "vulnerability" /tmp/php_audit.txt || echo 0) - echo "⚠️ Found $VULN_COUNT PHP vulnerability/vulnerabilities" >> $GITHUB_STEP_SUMMARY - VULNERABILITIES=$((VULNERABILITIES + VULN_COUNT)) - fi - echo "" >> $GITHUB_STEP_SUMMARY - fi - - # Python Dependencies - if [ -f "requirements.txt" ]; then - echo "### Python Dependencies" >> $GITHUB_STEP_SUMMARY - pip install pip-audit 2>&1 > /dev/null - if pip-audit -r requirements.txt 2>&1 | tee /tmp/py_audit.txt; then - echo "✅ No Python vulnerabilities detected" >> $GITHUB_STEP_SUMMARY - else - VULN_COUNT=$(grep -c "vulnerability" /tmp/py_audit.txt || echo 0) - echo "⚠️ Found $VULN_COUNT Python vulnerability/vulnerabilities" >> $GITHUB_STEP_SUMMARY - VULNERABILITIES=$((VULNERABILITIES + VULN_COUNT)) - fi - echo "" >> $GITHUB_STEP_SUMMARY - fi - - # NPM Dependencies - if [ -f "package.json" ]; then - echo "### NPM Dependencies" >> $GITHUB_STEP_SUMMARY - if npm audit --production 2>&1 | tee /tmp/npm_audit.txt; then - echo "✅ No NPM vulnerabilities detected" >> $GITHUB_STEP_SUMMARY - else - VULN_COUNT=$(grep -c "vulnerability" /tmp/npm_audit.txt || echo 0) - echo "⚠️ Found $VULN_COUNT NPM vulnerability/vulnerabilities" >> $GITHUB_STEP_SUMMARY - VULNERABILITIES=$((VULNERABILITIES + VULN_COUNT)) - fi - echo "" >> $GITHUB_STEP_SUMMARY - fi - - if [ "$VULNERABILITIES" -gt 0 ]; then - echo "**Total Vulnerabilities**: $VULNERABILITIES" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Action Required**: Update vulnerable dependencies" >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "✅ No dependency vulnerabilities detected" >> $GITHUB_STEP_SUMMARY - fi - - unused-dependencies: - name: Unused Dependencies Check - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Setup PHP - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 - with: - php-version: '8.1' - - - name: Check Unused Dependencies - run: | - set -x - echo "## 📦 Unused Dependencies Check" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ -f "composer.json" ]; then - echo "### PHP Dependencies" >> $GITHUB_STEP_SUMMARY - - # Install composer-unused - composer global require icanhazstring/composer-unused 2>/dev/null || true - - if composer global exec composer-unused 2>&1 | tee /tmp/unused.txt; then - UNUSED_COUNT=$(grep "unused" /tmp/unused.txt | wc -l || echo 0) - if [ "$UNUSED_COUNT" -gt 0 ]; then - echo "⚠️ Found $UNUSED_COUNT unused dependency/dependencies" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "View unused dependencies" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - cat /tmp/unused.txt >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - else - echo "✅ No unused dependencies detected" >> $GITHUB_STEP_SUMMARY - fi - else - echo "✅ All dependencies appear to be in use" >> $GITHUB_STEP_SUMMARY - fi - else - echo "ℹ️ No composer.json found" >> $GITHUB_STEP_SUMMARY - fi - - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Recommendation**: Remove unused dependencies to reduce attack surface" >> $GITHUB_STEP_SUMMARY - - broken-link-detection: - name: Broken Link Detection - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Check Internal Links - run: | - set -x - echo "## 🔗 Broken Link Detection" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Checking internal links in markdown files." >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - BROKEN_LINKS=0 - CHECKED_LINKS=0 - - # Find all markdown files - MD_FILES=$(find . -name "*.md" ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*") - - for file in $MD_FILES; do - # Extract markdown links [text](path) - while IFS= read -r line; do - # Extract path from [text](path) - link=$(echo "$line" | sed -n 's/.*\](\([^)]*\)).*/\1/p') - - # Skip external links (http/https) - if echo "$link" | grep -qE "^https?://"; then - continue - fi - - # Skip anchors only - if echo "$link" | grep -qE "^#"; then - continue - fi - - CHECKED_LINKS=$((CHECKED_LINKS + 1)) - - # Get directory of the markdown file - basedir=$(dirname "$file") - - # Resolve relative path - if [ -n "$link" ]; then - # Remove anchor if present - clean_link=$(echo "$link" | sed 's/#.*//') - - # Check if file exists - if [ ! -e "$basedir/$clean_link" ] && [ ! -e "$clean_link" ]; then - echo "Broken link in $file: $link" >> /tmp/broken_links.txt - BROKEN_LINKS=$((BROKEN_LINKS + 1)) - fi - fi - done < <(grep -o '\[.*\](.*)' "$file" 2>/dev/null || true) - done - - echo "### Link Validation Results" >> $GITHUB_STEP_SUMMARY - echo "- **Links Checked**: $CHECKED_LINKS" >> $GITHUB_STEP_SUMMARY - echo "- **Broken Links**: $BROKEN_LINKS" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ "$BROKEN_LINKS" -gt 0 ]; then - echo "⚠️ Found $BROKEN_LINKS broken internal link(s)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "View broken links" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - cat /tmp/broken_links.txt 2>/dev/null >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Recommendation**: Fix or remove broken links to maintain documentation quality" >> $GITHUB_STEP_SUMMARY - else - if [ "$CHECKED_LINKS" -gt 0 ]; then - echo "✅ All internal links are valid" >> $GITHUB_STEP_SUMMARY - else - echo "ℹ️ No internal links found to check" >> $GITHUB_STEP_SUMMARY - fi - fi - - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Note**: This check validates internal file references only. External URLs are not validated." >> $GITHUB_STEP_SUMMARY - - # ============================================================================ - # PHASE 2: Medium Priority Checks - # ============================================================================ - - api-documentation: - name: API Documentation Coverage - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Check Documentation - run: | - set -x - echo "## 📚 API Documentation Coverage" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Count public functions/classes - PUBLIC_METHODS=$(grep -r "public function" . --include="*.php" ! -path "./vendor/*" | wc -l || echo 0) - DOCUMENTED=$(grep -B5 -r "public function" . --include="*.php" ! -path "./vendor/*" | grep -c "/\*\*" || echo 0) - - if [ "$PUBLIC_METHODS" -gt 0 ]; then - COVERAGE=$((DOCUMENTED * 100 / PUBLIC_METHODS)) - echo "**Documentation Coverage**: $COVERAGE% ($DOCUMENTED/$PUBLIC_METHODS)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ "$COVERAGE" -lt 80 ]; then - echo "⚠️ Documentation coverage below 80% threshold" >> $GITHUB_STEP_SUMMARY - echo "**Recommendation**: Add PHPDoc blocks to public methods" >> $GITHUB_STEP_SUMMARY - else - echo "✅ Good documentation coverage" >> $GITHUB_STEP_SUMMARY - fi - else - echo "ℹ️ No public methods found for documentation check" >> $GITHUB_STEP_SUMMARY - fi - - accessibility-check: - name: Accessibility Check - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Check Accessibility - run: | - set -x - echo "## ♿ Accessibility Check" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - HTML_COUNT=$(find . -name "*.html" ! -path "./vendor/*" ! -path "./.git/*" ! -path "./node_modules/*" | wc -l || echo 0) - MD_IMG_COUNT=$(find . -name "*.md" ! -path "./vendor/*" ! -path "./.git/*" -exec grep -l "!\[" {} + 2>/dev/null | wc -l || echo 0) - - if [ "$HTML_COUNT" -gt 0 ] || [ "$MD_IMG_COUNT" -gt 0 ]; then - # Check for images without alt text - MISSING_ALT=0 - - if [ "$HTML_COUNT" -gt 0 ]; then - MISSING_ALT=$(grep -r "> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ "$MISSING_ALT" -gt 0 ]; then - echo "⚠️ Found images without alt text" >> $GITHUB_STEP_SUMMARY - echo "**Recommendation**: Add descriptive alt text for accessibility" >> $GITHUB_STEP_SUMMARY - else - echo "✅ All images have alt text" >> $GITHUB_STEP_SUMMARY - fi - else - echo "ℹ️ No HTML files found for accessibility check" >> $GITHUB_STEP_SUMMARY - fi - - performance-metrics: - name: Performance Metrics - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Check Performance Metrics - run: | - set -x - echo "## ⚡ Performance Metrics" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Check if JavaScript bundles exist - if [ -f "package.json" ]; then - echo "### Bundle Analysis" >> $GITHUB_STEP_SUMMARY - - # Check for common bundle files - BUNDLE_SIZE=0 - if [ -d "dist" ]; then - BUNDLE_SIZE=$(du -sb dist/ 2>/dev/null | cut -f1 || echo 0) - elif [ -d "build" ]; then - BUNDLE_SIZE=$(du -sb build/ 2>/dev/null | cut -f1 || echo 0) - fi - - if [ "$BUNDLE_SIZE" -gt 0 ]; then - BUNDLE_MB=$((BUNDLE_SIZE / 1024 / 1024)) - echo "**Bundle Size**: ${BUNDLE_MB}MB" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ "$BUNDLE_MB" -gt 5 ]; then - echo "⚠️ Bundle size exceeds 5MB threshold" >> $GITHUB_STEP_SUMMARY - echo "**Recommendation**: Optimize bundle size" >> $GITHUB_STEP_SUMMARY - else - echo "✅ Bundle size within acceptable limits" >> $GITHUB_STEP_SUMMARY - fi - else - echo "ℹ️ No build artifacts found" >> $GITHUB_STEP_SUMMARY - fi - else - echo "ℹ️ Not a JavaScript project" >> $GITHUB_STEP_SUMMARY - fi - - enterprise-readiness: - name: Enterprise Readiness Check - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Set up PHP - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 - with: - php-version: '8.1' - extensions: json, mbstring - tools: composer - coverage: none - - - name: Install API Package - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' - run: | - if [ -f "composer.json" ]; then - composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader - else - echo "No composer.json — pulling MokoStandards tools" - if [ ! -d "/tmp/mokostandards" ]; then - git clone --depth 1 --branch version/04 --quiet \ - "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ - /tmp/mokostandards 2>/dev/null || true - if [ -f "/tmp/mokostandards/composer.json" ]; then - cd /tmp/mokostandards && composer install --no-dev --no-interaction --quiet 2>/dev/null || true - cd - - fi - fi - fi - - - name: Check Enterprise Readiness - id: enterprise_check - run: | - echo "" >> $GITHUB_STEP_SUMMARY - - SCRIPT="" - if [ -f "api/validate/check_enterprise_readiness.php" ]; then - SCRIPT="api/validate/check_enterprise_readiness.php" - elif [ -f "/tmp/mokostandards/api/validate/check_enterprise_readiness.php" ]; then - SCRIPT="/tmp/mokostandards/api/validate/check_enterprise_readiness.php" - fi - - if [ -n "$SCRIPT" ]; then - php "$SCRIPT" --verbose | tee /tmp/enterprise-check.log - EXIT_CODE=$? - - echo "" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - cat /tmp/enterprise-check.log >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - - if [ "$EXIT_CODE" -eq 0 ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "✅ Repository meets enterprise readiness criteria!" >> $GITHUB_STEP_SUMMARY - exit 0 - else - echo "" >> $GITHUB_STEP_SUMMARY - echo "⚠️ Enterprise readiness issues detected" >> $GITHUB_STEP_SUMMARY - echo "**Note:** This is informational - review recommendations to improve" >> $GITHUB_STEP_SUMMARY - exit 0 # Non-blocking - fi - else - echo "ℹ️ Enterprise readiness check script not found - skipping" >> $GITHUB_STEP_SUMMARY - exit 0 - fi - - repository-health: - name: Repository Health Check - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Set up PHP - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 - with: - php-version: '8.1' - extensions: json, mbstring - tools: composer - coverage: none - - - name: Install API Package - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' - run: | - if [ -f "composer.json" ]; then - composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader - else - echo "No composer.json — pulling MokoStandards tools" - if [ ! -d "/tmp/mokostandards" ]; then - git clone --depth 1 --branch version/04 --quiet \ - "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ - /tmp/mokostandards 2>/dev/null || true - if [ -f "/tmp/mokostandards/composer.json" ]; then - cd /tmp/mokostandards && composer install --no-dev --no-interaction --quiet 2>/dev/null || true - cd - - fi - fi - fi - - - name: Check Repository Health - id: health_check - run: | - echo "" >> $GITHUB_STEP_SUMMARY - - SCRIPT="" - if [ -f "api/validate/check_repo_health.php" ]; then - SCRIPT="api/validate/check_repo_health.php" - elif [ -f "/tmp/mokostandards/api/validate/check_repo_health.php" ]; then - SCRIPT="/tmp/mokostandards/api/validate/check_repo_health.php" - fi - - if [ -n "$SCRIPT" ]; then - php "$SCRIPT" --verbose | tee /tmp/health-check.log - EXIT_CODE=$? - - echo "" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - cat /tmp/health-check.log >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - - if [ "$EXIT_CODE" -eq 0 ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "✅ Repository health check passed!" >> $GITHUB_STEP_SUMMARY - exit 0 - else - echo "" >> $GITHUB_STEP_SUMMARY - echo "⚠️ Repository health issues detected" >> $GITHUB_STEP_SUMMARY - echo "**Note:** This is informational - review recommendations to improve" >> $GITHUB_STEP_SUMMARY - exit 0 # Non-blocking - fi - else - echo "ℹ️ Repository health check script not found - skipping" >> $GITHUB_STEP_SUMMARY - exit 0 - fi - - terraform-validation: - name: Terraform Configuration Validation - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Setup Terraform - uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0 - with: - terraform_version: "1.0" - - - name: Validate Terraform Files - run: | - set -x - echo "## 🏗️ Terraform Configuration Validation" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Check if terraform files exist - TF_COUNT=$(find . -name "*.tf" -type f | wc -l || echo 0) - - if [ "$TF_COUNT" -eq 0 ]; then - echo "ℹ️ No Terraform files found in repository" >> $GITHUB_STEP_SUMMARY - exit 0 - fi - - echo "**Terraform Files Found**: $TF_COUNT" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Validation Results - VALIDATION_PASSED=true - WARNINGS=0 - ERRORS=0 - - # 1. Check .github/config.tf location (not root override files) - echo "### Override Configuration Check" >> $GITHUB_STEP_SUMMARY - LEGACY_OVERRIDES=$(find . -maxdepth 1 -name "*override*.tf" -o -name "MokoStandards.override.tf" 2>/dev/null | wc -l || echo 0) - if [ "$LEGACY_OVERRIDES" -gt 0 ]; then - echo "⚠️ Found legacy override files in root directory" >> $GITHUB_STEP_SUMMARY - echo "**Expected Location**: .github/config.tf" >> $GITHUB_STEP_SUMMARY - echo "**Legacy files found**: $LEGACY_OVERRIDES" >> $GITHUB_STEP_SUMMARY - WARNINGS=$((WARNINGS + 1)) - else - if [ -f ".github/config.tf" ]; then - echo "✅ Override configuration in correct location (.github/config.tf)" >> $GITHUB_STEP_SUMMARY - else - echo "ℹ️ No override configuration found" >> $GITHUB_STEP_SUMMARY - fi - fi - echo "" >> $GITHUB_STEP_SUMMARY - - # 2. Terraform Syntax Validation - echo "### Terraform Syntax Validation" >> $GITHUB_STEP_SUMMARY - SYNTAX_ERRORS=0 - - # Find all directories with terraform files - for dir in $(find . -name "*.tf" -type f -exec dirname {} \; | sort -u); do - cd "$dir" || continue - echo "Validating: $dir" >> $GITHUB_STEP_SUMMARY - - # Initialize without backend - terraform init -backend=false > /dev/null 2>&1 || true - - # Validate - if terraform validate -no-color > /tmp/tf_validate.txt 2>&1; then - echo " ✅ Syntax valid" >> $GITHUB_STEP_SUMMARY - else - echo " ❌ Syntax errors found" >> $GITHUB_STEP_SUMMARY - cat /tmp/tf_validate.txt >> $GITHUB_STEP_SUMMARY - SYNTAX_ERRORS=$((SYNTAX_ERRORS + 1)) - VALIDATION_PASSED=false - fi - cd - > /dev/null - done - echo "" >> $GITHUB_STEP_SUMMARY - - if [ "$SYNTAX_ERRORS" -eq 0 ]; then - echo "✅ All Terraform files have valid syntax" >> $GITHUB_STEP_SUMMARY - else - echo "❌ Found $SYNTAX_ERRORS directories with syntax errors" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + SYNTAX_ERRORS)) - fi - echo "" >> $GITHUB_STEP_SUMMARY - - # 3. Terraform Formatting Check - echo "### Terraform Formatting Check" >> $GITHUB_STEP_SUMMARY - FORMAT_ISSUES=0 - - for tf_file in $(find . -name "*.tf" -type f); do - if ! terraform fmt -check=true -no-color "$tf_file" > /dev/null 2>&1; then - FORMAT_ISSUES=$((FORMAT_ISSUES + 1)) - fi - done - - if [ "$FORMAT_ISSUES" -eq 0 ]; then - echo "✅ All Terraform files properly formatted" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ Found $FORMAT_ISSUES files with formatting issues" >> $GITHUB_STEP_SUMMARY - echo "**Fix**: Run \`terraform fmt -recursive\`" >> $GITHUB_STEP_SUMMARY - WARNINGS=$((WARNINGS + 1)) - fi - echo "" >> $GITHUB_STEP_SUMMARY - - # 4. Check for file_metadata blocks - echo "### File Metadata Validation" >> $GITHUB_STEP_SUMMARY - MISSING_METADATA=0 - - for tf_file in $(find . -name "*.tf" -type f); do - if ! grep -q "file_metadata" "$tf_file"; then - MISSING_METADATA=$((MISSING_METADATA + 1)) - fi - done - - if [ "$MISSING_METADATA" -eq 0 ]; then - echo "✅ All Terraform files contain file_metadata block" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ Found $MISSING_METADATA files missing file_metadata block" >> $GITHUB_STEP_SUMMARY - echo "**Reference**: docs/policy/terraform-file-standards.md" >> $GITHUB_STEP_SUMMARY - WARNINGS=$((WARNINGS + 1)) - fi - echo "" >> $GITHUB_STEP_SUMMARY - - # 5. Version Consistency Check - echo "### Version Consistency Check" >> $GITHUB_STEP_SUMMARY - VERSION_MISMATCHES=0 - EXPECTED_VERSION="04.00.04" - - for tf_file in $(find . -name "*.tf" -type f); do - if grep -q "version.*=" "$tf_file"; then - if ! grep -q "version.*=.*\"$EXPECTED_VERSION\"" "$tf_file"; then - VERSION_MISMATCHES=$((VERSION_MISMATCHES + 1)) - fi - fi - done - - if [ "$VERSION_MISMATCHES" -eq 0 ]; then - echo "✅ All Terraform file versions match $EXPECTED_VERSION" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ Found $VERSION_MISMATCHES files with version mismatches" >> $GITHUB_STEP_SUMMARY - echo "**Expected Version**: $EXPECTED_VERSION" >> $GITHUB_STEP_SUMMARY - WARNINGS=$((WARNINGS + 1)) - fi - echo "" >> $GITHUB_STEP_SUMMARY - - # 6. Copyright Header Check - echo "### Copyright Header Check" >> $GITHUB_STEP_SUMMARY - MISSING_COPYRIGHT=0 - - for tf_file in $(find . -name "*.tf" -type f); do - if ! grep -q "Copyright (C)" "$tf_file"; then - MISSING_COPYRIGHT=$((MISSING_COPYRIGHT + 1)) - fi - done - - if [ "$MISSING_COPYRIGHT" -eq 0 ]; then - echo "✅ All Terraform files have copyright headers" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ Found $MISSING_COPYRIGHT files missing copyright headers" >> $GITHUB_STEP_SUMMARY - echo "**Reference**: docs/policy/terraform-file-standards.md" >> $GITHUB_STEP_SUMMARY - WARNINGS=$((WARNINGS + 1)) - fi - echo "" >> $GITHUB_STEP_SUMMARY - - # Summary - echo "---" >> $GITHUB_STEP_SUMMARY - echo "### Validation Summary" >> $GITHUB_STEP_SUMMARY - echo "**Total Files**: $TF_COUNT" >> $GITHUB_STEP_SUMMARY - echo "**Errors**: $ERRORS" >> $GITHUB_STEP_SUMMARY - echo "**Warnings**: $WARNINGS" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ "$VALIDATION_PASSED" = true ] && [ "$ERRORS" -eq 0 ]; then - echo "✅ **Terraform Validation: PASSED**" >> $GITHUB_STEP_SUMMARY - exit 0 - elif [ "$ERRORS" -gt 0 ]; then - echo "❌ **Terraform Validation: FAILED**" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Note**: This is an informational check and does not block merges" >> $GITHUB_STEP_SUMMARY - exit 0 # Informational only - else - echo "⚠️ **Terraform Validation: PASSED WITH WARNINGS**" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Note**: This is an informational check and does not block merges" >> $GITHUB_STEP_SUMMARY - exit 0 # Informational only - fi - - summary: - name: Compliance Summary - runs-on: ubuntu-latest - needs: [ - repository-structure, documentation-quality, coding-standards, line-length-validation, license-compliance, git-hygiene, workflow-validation, version-consistency, script-integrity, enterprise-readiness, repository-health, - todo-fixme-tracking, file-size-limits, secret-scanning, broken-link-detection, - dependency-vulnerabilities, code-duplication, unused-dependencies, readme-completeness, - code-complexity, api-documentation, insecure-patterns, binary-file-detection, - dead-code-detection, file-naming-standards, accessibility-check, performance-metrics, terraform-validation - ] - if: always() - - steps: - - name: Generate Compliance Report - run: | - set -x - echo "# 📊 MokoStandards Compliance Report" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Calculate overall status - REPO_STATUS="${{ needs.repository-structure.result }}" - DOCS_STATUS="${{ needs.documentation-quality.result }}" - CODE_STATUS="${{ needs.coding-standards.result }}" - LINE_LENGTH_STATUS="${{ needs.line-length-validation.result }}" - LICENSE_STATUS="${{ needs.license-compliance.result }}" - GIT_STATUS="${{ needs.git-hygiene.result }}" - WORKFLOW_STATUS="${{ needs.workflow-validation.result }}" - VERSION_STATUS="${{ needs.version-consistency.result }}" - SCRIPT_STATUS="${{ needs.script-integrity.result }}" - ENTERPRISE_STATUS="${{ needs.enterprise-readiness.result }}" - HEALTH_STATUS="${{ needs.repository-health.result }}" - TERRAFORM_STATUS="${{ needs.terraform-validation.result }}" - - PASSED=0 - FAILED=0 - WARNINGS=0 - TOTAL=28 - - # Critical checks (must pass) - [ "$REPO_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) - [ "$DOCS_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) - [ "$CODE_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) - [ "$LICENSE_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) - [ "$GIT_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) - [ "$WORKFLOW_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) - [ "$VERSION_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) - [ "$SCRIPT_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) - - # Informational checks (don't fail build) - if [ "$ENTERPRISE_STATUS" = "success" ]; then - PASSED=$((PASSED + 1)) - else - WARNINGS=$((WARNINGS + 1)) - fi - - if [ "$HEALTH_STATUS" = "success" ]; then - PASSED=$((PASSED + 1)) - else - WARNINGS=$((WARNINGS + 1)) - fi - - if [ "$TERRAFORM_STATUS" = "success" ]; then - PASSED=$((PASSED + 1)) - else - WARNINGS=$((WARNINGS + 1)) - fi - - # Adjust total to only count critical checks for compliance percentage - CRITICAL_TOTAL=8 - CRITICAL_PASSED=$((PASSED - WARNINGS)) - COMPLIANCE_PERCENT=$((CRITICAL_PASSED * 100 / CRITICAL_TOTAL)) - - # Overall status badge - if [ "$COMPLIANCE_PERCENT" -eq 100 ]; then - echo "## ✅ Overall Status: **COMPLIANT** ($COMPLIANCE_PERCENT%)" >> $GITHUB_STEP_SUMMARY - elif [ "$COMPLIANCE_PERCENT" -ge 80 ]; then - echo "## ⚠️ Overall Status: **MOSTLY COMPLIANT** ($COMPLIANCE_PERCENT%)" >> $GITHUB_STEP_SUMMARY - elif [ "$COMPLIANCE_PERCENT" -ge 50 ]; then - echo "## ⚠️ Overall Status: **PARTIALLY COMPLIANT** ($COMPLIANCE_PERCENT%)" >> $GITHUB_STEP_SUMMARY - else - echo "## ❌ Overall Status: **NON-COMPLIANT** ($COMPLIANCE_PERCENT%)" >> $GITHUB_STEP_SUMMARY - fi - - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Critical Checks:** $CRITICAL_PASSED/$CRITICAL_TOTAL passed" >> $GITHUB_STEP_SUMMARY - echo "**Total Checks:** $PASSED/$TOTAL passed" >> $GITHUB_STEP_SUMMARY - if [ "$WARNINGS" -gt 0 ]; then - echo "**Informational:** $WARNINGS warning(s)" >> $GITHUB_STEP_SUMMARY - fi - echo "" >> $GITHUB_STEP_SUMMARY - - # Progress bar - FILLED=$((COMPLIANCE_PERCENT / 5)) - EMPTY=$((20 - FILLED)) - BAR="" - for i in $(seq 1 $FILLED); do BAR="${BAR}█"; done - for i in $(seq 1 $EMPTY); do BAR="${BAR}░"; done - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "$BAR $COMPLIANCE_PERCENT%" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Detailed breakdown - echo "## Validation Results" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Area | Status | Result | Priority |" >> $GITHUB_STEP_SUMMARY - echo "|------|--------|--------|----------|" >> $GITHUB_STEP_SUMMARY - - # Repository Structure - if [ "$REPO_STATUS" = "success" ]; then - echo "| 📁 Repository Structure | ✅ Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY - else - echo "| 📁 Repository Structure | ❌ Fail | **Action Required** | 🔴 Critical |" >> $GITHUB_STEP_SUMMARY - fi - - # Documentation Quality - if [ "$DOCS_STATUS" = "success" ]; then - echo "| 📚 Documentation Quality | ✅ Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY - else - echo "| 📚 Documentation Quality | ❌ Fail | **Action Required** | 🔴 Critical |" >> $GITHUB_STEP_SUMMARY - fi - - # Coding Standards - if [ "$CODE_STATUS" = "success" ]; then - echo "| 💻 Coding Standards | ✅ Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY - else - echo "| 💻 Coding Standards | ⚠️ Warning | Review Recommended | 🟡 Medium |" >> $GITHUB_STEP_SUMMARY - fi - - # License Compliance - if [ "$LICENSE_STATUS" = "success" ]; then - echo "| ⚖️ License Compliance | ✅ Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY - else - echo "| ⚖️ License Compliance | ❌ Fail | **Action Required** | 🔴 Critical |" >> $GITHUB_STEP_SUMMARY - fi - - # Git Hygiene - if [ "$GIT_STATUS" = "success" ]; then - echo "| 🧹 Git Repository Hygiene | ✅ Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY - else - echo "| 🧹 Git Repository Hygiene | ⚠️ Warning | Review Recommended | 🟡 Medium |" >> $GITHUB_STEP_SUMMARY - fi - - # Workflow Configuration - if [ "$WORKFLOW_STATUS" = "success" ]; then - echo "| ⚙️ Workflow Configuration | ✅ Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY - else - echo "| ⚙️ Workflow Configuration | ⚠️ Warning | Review Recommended | 🟡 Medium |" >> $GITHUB_STEP_SUMMARY - fi - - # Version Consistency - if [ "$VERSION_STATUS" = "success" ]; then - echo "| 🔢 Version Consistency | ✅ Pass | All versions match | - |" >> $GITHUB_STEP_SUMMARY - else - echo "| 🔢 Version Consistency | ❌ Fail | **Action Required** | 🔴 Critical |" >> $GITHUB_STEP_SUMMARY - fi - - # Script Integrity - if [ "$SCRIPT_STATUS" = "success" ]; then - echo "| 🔐 Script Integrity | ✅ Pass | SHA hashes validated | - |" >> $GITHUB_STEP_SUMMARY - else - echo "| 🔐 Script Integrity | ❌ Fail | **Action Required** | 🔴 Critical |" >> $GITHUB_STEP_SUMMARY - fi - - # Enterprise Readiness (Informational) - if [ "$ENTERPRISE_STATUS" = "success" ]; then - echo "| 🏢 Enterprise Readiness | ✅ Pass | Ready for enterprise | ℹ️ Info |" >> $GITHUB_STEP_SUMMARY - else - echo "| 🏢 Enterprise Readiness | ℹ️ Info | Review suggestions | ℹ️ Info |" >> $GITHUB_STEP_SUMMARY - fi - - # Repository Health (Informational) - if [ "$HEALTH_STATUS" = "success" ]; then - echo "| 🏥 Repository Health | ✅ Pass | Health check passed | ℹ️ Info |" >> $GITHUB_STEP_SUMMARY - else - echo "| 🏥 Repository Health | ℹ️ Info | Review recommendations | ℹ️ Info |" >> $GITHUB_STEP_SUMMARY - fi - - echo "" >> $GITHUB_STEP_SUMMARY - - # Action items summary - if [ "$FAILED" -gt 0 ]; then - echo "## ⚡ Action Items" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**$FAILED validation area(s) require attention:**" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - [ "$REPO_STATUS" != "success" ] && echo "- 🔴 **Critical:** Fix repository structure issues" >> $GITHUB_STEP_SUMMARY - [ "$DOCS_STATUS" != "success" ] && echo "- 🔴 **Critical:** Improve documentation quality" >> $GITHUB_STEP_SUMMARY - [ "$LICENSE_STATUS" != "success" ] && echo "- 🔴 **Critical:** Resolve license compliance issues" >> $GITHUB_STEP_SUMMARY - [ "$CODE_STATUS" != "success" ] && echo "- 🟡 **Medium:** Review coding standards violations" >> $GITHUB_STEP_SUMMARY - [ "$GIT_STATUS" != "success" ] && echo "- 🟡 **Medium:** Address git repository hygiene items" >> $GITHUB_STEP_SUMMARY - [ "$WORKFLOW_STATUS" != "success" ] && echo "- 🟡 **Medium:** Review workflow configuration" >> $GITHUB_STEP_SUMMARY - - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Next Steps:**" >> $GITHUB_STEP_SUMMARY - echo "1. Review detailed results in individual job outputs above" >> $GITHUB_STEP_SUMMARY - echo "2. Follow remediation steps provided for each failure" >> $GITHUB_STEP_SUMMARY - echo "3. Re-run this workflow after making corrections" >> $GITHUB_STEP_SUMMARY - echo "4. Reach 100% compliance before merging" >> $GITHUB_STEP_SUMMARY - else - echo "## 🎉 Excellent!" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Your repository is **fully compliant** with MokoStandards!" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Achievements:**" >> $GITHUB_STEP_SUMMARY - echo "- ✅ All required directories and files present" >> $GITHUB_STEP_SUMMARY - echo "- ✅ Documentation meets quality standards" >> $GITHUB_STEP_SUMMARY - echo "- ✅ Coding standards followed" >> $GITHUB_STEP_SUMMARY - echo "- ✅ License compliance verified" >> $GITHUB_STEP_SUMMARY - echo "- ✅ Git repository well-maintained" >> $GITHUB_STEP_SUMMARY - echo "- ✅ Workflows properly configured" >> $GITHUB_STEP_SUMMARY - fi - - echo "" >> $GITHUB_STEP_SUMMARY - echo "---" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "📚 **Resources:**" >> $GITHUB_STEP_SUMMARY - echo "- [MokoStandards Documentation](https://github.com/mokoconsulting-tech/MokoStandards)" >> $GITHUB_STEP_SUMMARY - echo "- [Repository Structure Guide](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/docs/policy/core-structure.md)" >> $GITHUB_STEP_SUMMARY - echo "- [Documentation Standards](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/docs/policy/document-formatting.md)" >> $GITHUB_STEP_SUMMARY - echo "- [Coding Standards](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/docs/policy/coding-style-guide.md)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "_Generated by MokoStandards Compliance Workflow v${WORKFLOW_VERSION}_" >> $GITHUB_STEP_SUMMARY - - # Create tracking issue for non-compliance if on push - if [ "$COMPLIANCE_PERCENT" -lt 100 ] && [ "${{ github.event_name }}" = "push" ]; then - echo "Creating tracking issue for standards violations..." - fi - - # Exit with error if not fully compliant - if [ "$COMPLIANCE_PERCENT" -lt 100 ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "### ❌ Standards Compliance Failed" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Overall Compliance:** $COMPLIANCE_PERCENT%" >> $GITHUB_STEP_SUMMARY - echo "**Status:** Repository does not meet 100% compliance requirement" >> $GITHUB_STEP_SUMMARY - echo "**Action Required:** Review and fix all validation failures above" >> $GITHUB_STEP_SUMMARY - echo "" - echo "❌ ERROR: Standards compliance at $COMPLIANCE_PERCENT% - 100% required" - exit 1 - fi - - echo "" >> $GITHUB_STEP_SUMMARY - echo "### ✅ Full Standards Compliance Achieved" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Overall Compliance:** 100%" >> $GITHUB_STEP_SUMMARY - echo "**Status:** Repository meets all MokoStandards requirements" >> $GITHUB_STEP_SUMMARY - echo "" - echo "✅ SUCCESS: Repository is fully MokoStandards compliant" - - - name: Create or reopen tracking issue for standards violations - if: failure() - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - run: | - REPO="${{ github.repository }}" - RUN_URL="${{ github.server_url }}/${REPO}/actions/runs/${{ github.run_id }}" - DATE=$(date -u '+%Y-%m-%d') - SHA="${{ github.sha }}" - ACTOR="${{ github.actor }}" - BRANCH="${{ github.ref_name }}" - - # Collect failed checks - FAILED="" - [ "${{ needs.repository-structure.result }}" != "success" ] && FAILED="${FAILED}\n- Repository Structure" - [ "${{ needs.documentation-quality.result }}" != "success" ] && FAILED="${FAILED}\n- Documentation Quality" - [ "${{ needs.coding-standards.result }}" != "success" ] && FAILED="${FAILED}\n- Coding Standards" - [ "${{ needs.license-compliance.result }}" != "success" ] && FAILED="${FAILED}\n- License Compliance" - [ "${{ needs.git-hygiene.result }}" != "success" ] && FAILED="${FAILED}\n- Git Hygiene" - [ "${{ needs.workflow-validation.result }}" != "success" ] && FAILED="${FAILED}\n- Workflow Validation" - [ "${{ needs.version-consistency.result }}" != "success" ] && FAILED="${FAILED}\n- Version Consistency" - [ "${{ needs.script-integrity.result }}" != "success" ] && FAILED="${FAILED}\n- Script Integrity" - [ "${{ needs.secret-scanning.result }}" != "success" ] && FAILED="${FAILED}\n- Secret Scanning" - [ "${{ needs.line-length-validation.result }}" != "success" ] && FAILED="${FAILED}\n- Line Length" - [ "${{ needs.file-size-limits.result }}" != "success" ] && FAILED="${FAILED}\n- File Size Limits" - [ "${{ needs.readme-completeness.result }}" != "success" ] && FAILED="${FAILED}\n- README Completeness" - - if [ -z "$FAILED" ]; then - echo "No failed checks to report" - exit 0 - fi - - TITLE="[Standards] Compliance violations — ${DATE}" - BODY="## Standards Compliance Violations - - | Field | Value | - |-------|-------| - | **Branch** | \`${BRANCH}\` | - | **Commit** | \`${SHA:0:7}\` | - | **Actor** | @${ACTOR} | - | **Run** | [View workflow](${RUN_URL}) | - - ### Failed Checks - $(printf '%b' "$FAILED") - - ### Required Actions - 1. Review the [workflow run](${RUN_URL}) for details - 2. Fix each failed check - 3. Push to trigger a new scan - - --- - *Auto-created by standards-compliance workflow*" - - BODY=$(echo "$BODY" | sed 's/^ //') - LABEL="standards-violation" - - gh label create "$LABEL" --repo "$REPO" --color "D73A4A" --description "Standards compliance failure" --force 2>/dev/null || true - - EXISTING=$(gh api "repos/${REPO}/issues?labels=${LABEL}&state=all&per_page=1&sort=created&direction=desc" \ - --jq '.[0].number' 2>/dev/null) - - if [ -n "$EXISTING" ] && [ "$EXISTING" != "null" ]; then - gh api "repos/${REPO}/issues/${EXISTING}" -X PATCH \ - -f title="$TITLE" -f body="$BODY" -f state="open" --silent - echo "Updated issue #${EXISTING}" - else - gh issue create --repo "$REPO" --title "$TITLE" --body "$BODY" \ - --label "$LABEL" --assignee "jmiller-moko" - fi - -# CUSTOMIZATION: -# -# 1. Adjust severity of checks (convert warnings to errors or vice versa) -# 2. Add project-specific validation rules -# 3. Integrate with custom linting tools -# 4. Add notification steps for compliance failures -# 5. Customize required files/directories for your project type - diff --git a/.github/workflows/sync-version-on-merge.yml b/.github/workflows/sync-version-on-merge.yml deleted file mode 100644 index 60715f6..0000000 --- a/.github/workflows/sync-version-on-merge.yml +++ /dev/null @@ -1,133 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# This file is part of a Moko Consulting project. -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: GitHub.Workflow -# INGROUP: MokoStandards.Automation -# REPO: https://github.com/mokoconsulting-tech/MokoStandards -# PATH: /templates/workflows/shared/sync-version-on-merge.yml.template -# VERSION: 04.06.00 -# BRIEF: Auto-bump patch version on every push to main and propagate to all file headers -# NOTE: Synced via bulk-repo-sync to .github/workflows/sync-version-on-merge.yml in all governed repos. -# README.md is the single source of truth for the repository version. - -name: Sync Version from README - -on: - push: - branches: - - main - - master - workflow_dispatch: - inputs: - dry_run: - description: 'Dry run (preview only, no commit)' - type: boolean - default: false - -permissions: - contents: write - issues: write - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - sync-version: - name: Propagate README version - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.GH_TOKEN || github.token }} - fetch-depth: 0 - - - name: Set up PHP - uses: shivammathur/setup-php@fcafdd6392932010c2bd5094439b8e33be2a8a09 # v2.37.0 - with: - php-version: '8.1' - tools: composer - - - name: Setup MokoStandards tools - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' - run: | - git clone --depth 1 --branch version/04 --quiet \ - "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ - /tmp/mokostandards - cd /tmp/mokostandards - composer install --no-dev --no-interaction --quiet - - - name: Auto-bump patch version - if: ${{ github.event_name == 'push' && github.actor != 'github-actions[bot]' }} - run: | - if git diff --name-only HEAD~1 HEAD 2>/dev/null | grep -q '^README\.md$'; then - echo "README.md changed in this push — skipping auto-bump" - exit 0 - fi - - RESULT=$(php /tmp/mokostandards/api/cli/version_bump.php --path .) || { - echo "⚠️ Could not bump version — skipping" - exit 0 - } - echo "Auto-bumping patch: $RESULT" - git config --local user.email "github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" - git add README.md - git commit -m "chore(version): auto-bump patch ${RESULT} [skip ci]" \ - --author="github-actions[bot] " - git push - - - name: Extract version from README.md - id: readme_version - run: | - git pull --ff-only 2>/dev/null || true - VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null) - if [ -z "$VERSION" ]; then - echo "⚠️ No VERSION in README.md — skipping propagation" - echo "skip=true" >> $GITHUB_OUTPUT - exit 0 - fi - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "skip=false" >> $GITHUB_OUTPUT - echo "✅ README.md version: $VERSION" - - - name: Run version sync - if: ${{ steps.readme_version.outputs.skip != 'true' && inputs.dry_run != true }} - run: | - php /tmp/mokostandards/api/maintenance/update_version_from_readme.php \ - --path . \ - --create-issue \ - --repo "${{ github.repository }}" - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - - - name: Commit updated files - if: ${{ steps.readme_version.outputs.skip != 'true' && inputs.dry_run != true }} - run: | - git pull --ff-only 2>/dev/null || true - if git diff --quiet; then - echo "ℹ️ No version changes needed — already up to date" - exit 0 - fi - VERSION="${{ steps.readme_version.outputs.version }}" - git config --local user.email "github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" - git add -A - git commit -m "chore(version): sync badges and headers to ${VERSION} [skip ci]" \ - --author="github-actions[bot] " - git push - - - name: Summary - run: | - VERSION="${{ steps.readme_version.outputs.version }}" - echo "## 📦 Version Sync — ${VERSION}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Source:** \`README.md\` FILE INFORMATION block" >> $GITHUB_STEP_SUMMARY - echo "**Version:** \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/update-server.yml b/.github/workflows/update-server.yml deleted file mode 100644 index fff9ec6..0000000 --- a/.github/workflows/update-server.yml +++ /dev/null @@ -1,249 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: GitHub.Workflow -# INGROUP: MokoStandards.Joomla -# REPO: https://github.com/mokoconsulting-tech/MokoStandards -# PATH: /templates/workflows/joomla/update-server.yml.template -# VERSION: 04.06.00 -# BRIEF: Update Joomla update server XML feed with stable/rc/dev entries -# -# Writes updates.xml with multiple entries: -# - stable on push to main (from auto-release) -# - rc on push to rc/** -# - development on push to dev/** -# -# Joomla filters by user's "Minimum Stability" setting. - -name: Update Joomla Update Server XML Feed - -on: - push: - branches: - - 'dev/**' - - 'alpha/**' - - 'beta/**' - - 'rc/**' - paths: - - 'src/**' - - 'htdocs/**' - workflow_dispatch: - inputs: - stability: - description: 'Stability tag' - required: true - default: 'development' - type: choice - options: - - development - - alpha - - beta - - rc - - stable - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -permissions: - contents: write - -jobs: - update-xml: - name: Update updates.xml - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.GH_TOKEN || github.token }} - fetch-depth: 0 - - - name: Setup MokoStandards tools - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' - run: | - git clone --depth 1 --branch version/04 --quiet \ - "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ - /tmp/mokostandards 2>/dev/null || true - if [ -d "/tmp/mokostandards" ] && [ -f "/tmp/mokostandards/composer.json" ]; then - cd /tmp/mokostandards && composer install --no-dev --no-interaction --quiet 2>/dev/null || true - fi - - - name: Generate updates.xml entry - run: | - BRANCH="${{ github.ref_name }}" - REPO="${{ github.repository }}" - VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null || echo "0.0.0") - - # Determine stability from branch or input - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - STABILITY="${{ inputs.stability }}" - elif [[ "$BRANCH" == rc/* ]]; then - STABILITY="rc" - elif [[ "$BRANCH" == beta/* ]]; then - STABILITY="beta" - elif [[ "$BRANCH" == alpha/* ]]; then - STABILITY="alpha" - elif [[ "$BRANCH" == dev/* ]]; then - STABILITY="development" - else - STABILITY="stable" - fi - - # Parse manifest - MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1) - if [ -z "$MANIFEST" ]; then - echo "No Joomla manifest found — skipping" - exit 0 - fi - - EXT_NAME=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "${{ github.event.repository.name }}") - EXT_TYPE=$(grep -oP ']+type="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "component") - EXT_ELEMENT=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || basename "$MANIFEST" .xml) - EXT_CLIENT=$(grep -oP ']+client="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "") - EXT_FOLDER=$(grep -oP ']+group="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "") - TARGET_PLATFORM=$(grep -oP '' "$MANIFEST" 2>/dev/null | head -1 || echo "") - PHP_MINIMUM=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "") - - [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(basename "$MANIFEST" .xml) - [ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '' "/") - - CLIENT_TAG="" - [ -n "$EXT_CLIENT" ] && CLIENT_TAG="${EXT_CLIENT}" - [ -z "$CLIENT_TAG" ] && ([ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]) && CLIENT_TAG="site" - - FOLDER_TAG="" - [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ] && FOLDER_TAG="${EXT_FOLDER}" - - PHP_TAG="" - [ -n "$PHP_MINIMUM" ] && PHP_TAG="${PHP_MINIMUM}" - - # Version suffix for non-stable - DISPLAY_VERSION="$VERSION" - case "$STABILITY" in - development) DISPLAY_VERSION="${VERSION}-dev" ;; - alpha) DISPLAY_VERSION="${VERSION}-alpha" ;; - beta) DISPLAY_VERSION="${VERSION}-beta" ;; - rc) DISPLAY_VERSION="${VERSION}-rc" ;; - esac - - MAJOR=$(echo "$VERSION" | awk -F. '{print $1}') - - # Each stability level has its own release tag - case "$STABILITY" in - development) RELEASE_TAG="development" ;; - alpha) RELEASE_TAG="alpha" ;; - beta) RELEASE_TAG="beta" ;; - rc) RELEASE_TAG="release-candidate" ;; - *) RELEASE_TAG="v${MAJOR}" ;; - esac - - PACKAGE_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.zip" - DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}" - INFO_URL="https://github.com/${REPO}" - - # ── Build install-ready ZIP ───────────────────────────────── - SOURCE_DIR="src" - [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" - if [ -d "$SOURCE_DIR" ]; then - cd "$SOURCE_DIR" - zip -r "/tmp/${PACKAGE_NAME}" . - cd .. - - SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1) - - # Ensure draft release exists for this major - gh release view "$RELEASE_TAG" --json tagName > /dev/null 2>&1 || \ - gh release create "$RELEASE_TAG" --title "${RELEASE_TAG} (${DISPLAY_VERSION})" --notes "${STABILITY} release" --prerelease --target main 2>/dev/null || true - - # Upload ZIP to the major release - gh release upload "$RELEASE_TAG" "/tmp/${PACKAGE_NAME}" --clobber 2>/dev/null || true - - echo "Package: ${PACKAGE_NAME} (SHA: ${SHA256})" >> $GITHUB_STEP_SUMMARY - else - SHA256="" - fi - - # ── Build the new entry ─────────────────────────────────────── - NEW_ENTRY="" - NEW_ENTRY="${NEW_ENTRY} \n" - NEW_ENTRY="${NEW_ENTRY} ${EXT_NAME}\n" - NEW_ENTRY="${NEW_ENTRY} ${EXT_NAME} (${STABILITY})\n" - NEW_ENTRY="${NEW_ENTRY} ${EXT_ELEMENT}\n" - NEW_ENTRY="${NEW_ENTRY} ${EXT_TYPE}\n" - NEW_ENTRY="${NEW_ENTRY} ${DISPLAY_VERSION}\n" - [ -n "$CLIENT_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${CLIENT_TAG}\n" - [ -n "$FOLDER_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${FOLDER_TAG}\n" - NEW_ENTRY="${NEW_ENTRY} \n" - NEW_ENTRY="${NEW_ENTRY} ${STABILITY}\n" - NEW_ENTRY="${NEW_ENTRY} \n" - NEW_ENTRY="${NEW_ENTRY} ${INFO_URL}\n" - NEW_ENTRY="${NEW_ENTRY} \n" - NEW_ENTRY="${NEW_ENTRY} ${DOWNLOAD_URL}\n" - NEW_ENTRY="${NEW_ENTRY} \n" - [ -n "$SHA256" ] && NEW_ENTRY="${NEW_ENTRY} sha256:${SHA256}\n" - NEW_ENTRY="${NEW_ENTRY} ${TARGET_PLATFORM}\n" - [ -n "$PHP_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${PHP_TAG}\n" - NEW_ENTRY="${NEW_ENTRY} Moko Consulting\n" - NEW_ENTRY="${NEW_ENTRY} https://mokoconsulting.tech\n" - NEW_ENTRY="${NEW_ENTRY} " - - # ── Write new entry to temp file ─────────────────────────────── - printf '%b' "$NEW_ENTRY" > /tmp/new_entry.xml - - # ── Merge into updates.xml ───────────────────────────────────── - if [ ! -f "updates.xml" ]; then - printf '%s\n' '' > updates.xml - printf '%s\n' '' >> updates.xml - cat /tmp/new_entry.xml >> updates.xml - printf '\n%s\n' '' >> updates.xml - else - # Remove existing entry for this stability, insert new one - printf 'import re\nstability = "%s"\n' "${STABILITY}" > /tmp/merge_xml.py - printf 'with open("updates.xml") as f: content = f.read()\n' >> /tmp/merge_xml.py - printf 'with open("/tmp/new_entry.xml") as f: new_entry = f.read()\n' >> /tmp/merge_xml.py - printf 'pattern = r" .*?" + re.escape(stability) + r".*?\\n?"\n' >> /tmp/merge_xml.py - printf 'content = re.sub(pattern, "", content, flags=re.DOTALL)\n' >> /tmp/merge_xml.py - printf 'content = content.replace("", new_entry + "\\n")\n' >> /tmp/merge_xml.py - printf 'content = re.sub(r"\\n{3,}", "\\n\\n", content)\n' >> /tmp/merge_xml.py - printf 'with open("updates.xml", "w") as f: f.write(content)\n' >> /tmp/merge_xml.py - python3 /tmp/merge_xml.py 2>/dev/null || { - # Fallback: rebuild keeping other stability entries - { - printf '%s\n' '' - printf '%s\n' '' - for TAG in stable rc development; do - [ "$TAG" = "${STABILITY}" ] && continue - if grep -q "${TAG}" updates.xml 2>/dev/null; then - sed -n "//,/<\/update>/{ /${TAG}<\/tag>/p; }" updates.xml - fi - done - cat /tmp/new_entry.xml - printf '\n%s\n' '' - } > /tmp/updates_new.xml - mv /tmp/updates_new.xml updates.xml - } - fi - - # Commit - git config --local user.email "github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" - git add updates.xml - git diff --cached --quiet || { - git commit -m "chore: update updates.xml (${STABILITY}: ${DISPLAY_VERSION}) [skip ci]" \ - --author="github-actions[bot] " - git push - } - - echo "## Joomla Update Server" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY - echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Version | \`${DISPLAY_VERSION}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Element | \`${EXT_ELEMENT}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Download | [ZIP](${DOWNLOAD_URL}) |" >> $GITHUB_STEP_SUMMARY -- 2.52.0 From 8cdaa57c22f27b37e46423b2e1d738ec7753c437 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sun, 26 Apr 2026 21:54:25 -0500 Subject: [PATCH 008/136] chore: Gitea-only workflows + remove GitHub update server [skip ci] --- .gitea/workflows/release.yml | 47 ------------------------------ .gitea/workflows/update-server.yml | 22 +------------- 2 files changed, 1 insertion(+), 68 deletions(-) diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index c3daa98..07d1b24 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -447,53 +447,6 @@ jobs: fi echo "Uploaded ${ZIP_NAME}" - # ── GitHub Mirror (BACKUP) ─────────────────────────────────────── - - name: "GitHub: Mirror release (stable/rc only)" - if: ${{ (steps.meta.outputs.stability == 'stable' || steps.meta.outputs.stability == 'rc') && secrets.GH_TOKEN != '' }} - continue-on-error: true - env: - EXT_ELEMENT: ${{ steps.meta.outputs.ext_element }} - run: | - TAG="${{ steps.meta.outputs.tag_name }}" - VERSION="${{ steps.bump.outputs.version }}" - STABILITY="${{ steps.meta.outputs.stability }}" - ZIP_NAME="${{ steps.bump.outputs.zip_name }}" - SHA256="${{ steps.zip.outputs.sha256 }}" - TOKEN="${{ secrets.GH_TOKEN }}" - GH_REPO="mokoconsulting-tech/${GITEA_REPO}" - GH_API="https://api.github.com/repos/${GH_REPO}" - - echo "=== GitHub Mirror ===" - IS_PRE="true" - [ "$STABILITY" = "stable" ] && IS_PRE="false" - - # Clean up existing - EXISTING=$(curl -sf -H "Authorization: token ${TOKEN}" \ - "${GH_API}/releases/tags/${TAG}" 2>/dev/null | jq -r '.id // empty') - [ -n "$EXISTING" ] && curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${GH_API}/releases/${EXISTING}" || true - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${GH_API}/git/refs/tags/${TAG}" 2>/dev/null || true - - RELEASE_ID=$(curl -sS -X POST -H "Authorization: token ${TOKEN}" \ - -H "Content-Type: application/json" \ - "${GH_API}/releases" \ - -d "$(jq -n \ - --arg tag "$TAG" \ - --arg target "${{ github.sha }}" \ - --arg name "${EXT_ELEMENT} ${VERSION} ${STABILITY^} (mirror)" \ - --arg body "Mirror of Gitea release. SHA-256: \`${SHA256}\`" \ - --argjson pre "$IS_PRE" \ - '{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: $pre, draft: false}' - )" | jq -r '.id') - - if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "null" ]; then - curl -sf -X POST \ - -H "Authorization: token ${TOKEN}" \ - -H "Content-Type: application/octet-stream" \ - "https://uploads.github.com/repos/${GH_REPO}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \ - --data-binary "@build/${ZIP_NAME}" - echo "GitHub mirror uploaded: ${ZIP_NAME}" - fi - # ── Update updates.xml ────────────────────────────────────────── - name: "Update updates.xml with SHA and sync to main" run: | diff --git a/.gitea/workflows/update-server.yml b/.gitea/workflows/update-server.yml index 4b0caa6..73f4e2a 100644 --- a/.gitea/workflows/update-server.yml +++ b/.gitea/workflows/update-server.yml @@ -82,7 +82,7 @@ jobs: env: MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN }}"}}' + COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_TOKEN }}"}}}' run: | if ! command -v composer &> /dev/null; then sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 @@ -407,26 +407,6 @@ jobs: echo "WARNING: could not get updates.xml SHA from main" >> $GITHUB_STEP_SUMMARY fi - # -- Mirror to GitHub (stable and rc only) -------------------------------- - - name: Mirror release to GitHub - if: >- - (steps.update.outputs.stability == 'stable' || steps.update.outputs.stability == 'rc') && - secrets.GH_TOKEN != '' - continue-on-error: true - env: - GH_TOKEN: ${{ secrets.GH_TOKEN }} - run: | - GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" - STABILITY="${{ steps.update.outputs.stability }}" - echo "GitHub mirror sync for ${STABILITY} — ${GH_REPO}" >> $GITHUB_STEP_SUMMARY - # Mirror packages if they exist - for PKG in /tmp/*.zip /tmp/*.tar.gz; do - if [ -f "$PKG" ]; then - _RELID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/${RELEASE_TAG}" 2>/dev/null | jq -r ".id // empty") - [ -n "$_RELID" ] && curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" -H "Content-Type: application/octet-stream" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/${_RELID}/assets?name=$(basename $PKG)" --data-binary "@$PKG" > /dev/null 2>&1 || true - fi - done - - name: SFTP deploy to dev server if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev' env: -- 2.52.0 From af9072f5c4c65ab727a51f52e8e96f5e1e1cf499 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Mon, 11 May 2026 21:16:48 +0000 Subject: [PATCH 009/136] chore: sync update-server.yml from Template-Joomla [skip ci] --- .gitea/workflows/update-server.yml | 99 +++++++++++++----------------- 1 file changed, 44 insertions(+), 55 deletions(-) diff --git a/.gitea/workflows/update-server.yml b/.gitea/workflows/update-server.yml index 73f4e2a..e6a1924 100644 --- a/.gitea/workflows/update-server.yml +++ b/.gitea/workflows/update-server.yml @@ -270,37 +270,34 @@ jobs: SHA256="" fi - # -- Build the new entry ----------------------------------------- + # -- Build the new entry (canonical format matching release.yml) -- NEW_ENTRY="" NEW_ENTRY="${NEW_ENTRY} \n" NEW_ENTRY="${NEW_ENTRY} ${EXT_NAME}\n" - NEW_ENTRY="${NEW_ENTRY} ${EXT_NAME} (${STABILITY})\n" + NEW_ENTRY="${NEW_ENTRY} ${EXT_NAME} ${STABILITY} build.\n" NEW_ENTRY="${NEW_ENTRY} ${EXT_ELEMENT}\n" NEW_ENTRY="${NEW_ENTRY} ${EXT_TYPE}\n" - NEW_ENTRY="${NEW_ENTRY} ${DISPLAY_VERSION}\n" - NEW_ENTRY="${NEW_ENTRY} $(date +%Y-%m-%d)\n" [ -n "$CLIENT_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${CLIENT_TAG}\n" [ -n "$FOLDER_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${FOLDER_TAG}\n" - NEW_ENTRY="${NEW_ENTRY} \n" - NEW_ENTRY="${NEW_ENTRY} ${STABILITY}\n" - NEW_ENTRY="${NEW_ENTRY} \n" - NEW_ENTRY="${NEW_ENTRY} ${INFO_URL}\n" + NEW_ENTRY="${NEW_ENTRY} ${VERSION}\n" + NEW_ENTRY="${NEW_ENTRY} $(date +%Y-%m-%d)\n" + NEW_ENTRY="${NEW_ENTRY} https://git.mokoconsulting.tech/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${RELEASE_TAG}\n" NEW_ENTRY="${NEW_ENTRY} \n" - NEW_ENTRY="${NEW_ENTRY} ${DOWNLOAD_URL}\n" + NEW_ENTRY="${NEW_ENTRY} ${DOWNLOAD_URL}\n" NEW_ENTRY="${NEW_ENTRY} \n" [ -n "$SHA256" ] && NEW_ENTRY="${NEW_ENTRY} ${SHA256}\n" - NEW_ENTRY="${NEW_ENTRY} ${TARGET_PLATFORM}\n" - [ -n "$PHP_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${PHP_TAG}\n" + NEW_ENTRY="${NEW_ENTRY} ${STABILITY}\n" NEW_ENTRY="${NEW_ENTRY} Moko Consulting\n" NEW_ENTRY="${NEW_ENTRY} https://mokoconsulting.tech\n" + NEW_ENTRY="${NEW_ENTRY} \n" + [ -n "$PHP_MINIMUM" ] && NEW_ENTRY="${NEW_ENTRY} ${PHP_MINIMUM}\n" NEW_ENTRY="${NEW_ENTRY} " # -- Write new entry to temp file -------------------------------- printf '%b' "$NEW_ENTRY" > /tmp/new_entry.xml - # -- Merge into updates.xml (only update this stability channel) - - # Cascading update: each stability level updates itself and all lower levels - # stable → all | rc → rc,beta,alpha,dev | beta → beta,alpha,dev | alpha → alpha,dev | dev → dev + # -- Merge into updates.xml ---------------------------------------- + # Cascade: stable→all | rc→rc+lower | beta→beta+lower | alpha→alpha+dev | dev→dev CASCADE_MAP="stable:development,alpha,beta,rc,stable rc:development,alpha,beta,rc beta:development,alpha,beta alpha:development,alpha development:development" TARGETS="" for entry in $CASCADE_MAP; do @@ -313,62 +310,54 @@ jobs: done [ -z "$TARGETS" ] && TARGETS="${STABILITY}" + echo "Cascade: ${STABILITY} → ${TARGETS}" + + # Create updates.xml if missing if [ ! -f "updates.xml" ]; then printf '%s\n' "" > updates.xml - printf '%s\n' "" >> updates.xml - printf '%s\n' "" >> updates.xml - printf '%s\n' '' >> updates.xml - cat /tmp/new_entry.xml >> updates.xml - printf '\n%s\n' '' >> updates.xml - else - # Replace each cascading channel with the new entry (different tag) - export PY_TARGETS="$TARGETS" - python3 << PYEOF + printf '%s\n' "" >> updates.xml + printf '%s\n' "" >> updates.xml + printf '%s\n' "" >> updates.xml + fi + + # Update existing blocks or create missing ones + export PY_TARGETS="$TARGETS" PY_VERSION="$VERSION" PY_DATE="$(date +%Y-%m-%d)" + python3 << 'PYEOF' import re, os + targets = os.environ["PY_TARGETS"].split(",") - stability = "${STABILITY}" + version = os.environ["PY_VERSION"] + date = os.environ["PY_DATE"] + with open("updates.xml") as f: content = f.read() with open("/tmp/new_entry.xml") as f: new_entry_template = f.read() + for tag in targets: tag = tag.strip() - # Build entry with this tag + # Build entry with this tag's name new_entry = re.sub(r"[^<]*", f"{tag}", new_entry_template) - # Remove existing entry for this tag - pattern = r" .*?" + re.escape(tag) + r".*?\n?" - content = re.sub(pattern, "", content, flags=re.DOTALL) - # Insert before - content = content.replace("", new_entry + "\n") + + # Try to find existing block (handles both single-line and multi-line ) + block_pattern = r"((?:(?!).)*?" + re.escape(tag) + r".*?)" + match = re.search(block_pattern, content, re.DOTALL) + + if match: + # Update in place — replace entire block + content = content.replace(match.group(1), new_entry.strip()) + print(f" UPDATED: {tag} → {version}") + else: + # Create — insert before + content = content.replace("", "\n" + new_entry.strip() + "\n\n") + print(f" CREATED: {tag} → {version}") + + # Clean up excessive blank lines content = re.sub(r"\n{3,}", "\n\n", content) + with open("updates.xml", "w") as f: f.write(content) PYEOF - if [ $? -ne 0 ]; then - # Fallback: rebuild keeping other stability entries - { - printf '%s\n' "" - printf '%s\n' "" - printf '%s\n' "" - printf '%s\n' '' - for TAG in stable rc development; do - [ "$TAG" = "${STABILITY}" ] && continue - if grep -q "${TAG}" updates.xml 2>/dev/null; then - sed -n "//,/<\/update>/{ /${TAG}<\/tag>/p; }" updates.xml - fi - done - cat /tmp/new_entry.xml - printf '\n%s\n' '' - } > /tmp/updates_new.xml - mv /tmp/updates_new.xml updates.xml - fi - fi # Commit git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" -- 2.52.0 From f5a628d793def2ce1d6f39227cecf1f97948abd8 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Mon, 11 May 2026 21:27:17 +0000 Subject: [PATCH 010/136] chore: sync auto-release.yml from Template-Joomla [skip ci] --- .gitea/workflows/auto-release.yml | 949 ++++++++++++++++++++++++++++++ 1 file changed, 949 insertions(+) create mode 100644 .gitea/workflows/auto-release.yml diff --git a/.gitea/workflows/auto-release.yml b/.gitea/workflows/auto-release.yml new file mode 100644 index 0000000..279bc5e --- /dev/null +++ b/.gitea/workflows/auto-release.yml @@ -0,0 +1,949 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Release +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# PATH: /templates/workflows/joomla/auto-release.yml.template +# VERSION: 04.06.00 +# BRIEF: Joomla build & release — ZIP package, updates.xml, SHA-256 checksum +# +# +========================================================================+ +# | BUILD & RELEASE PIPELINE (JOOMLA) | +# +========================================================================+ +# | | +# | Triggers on push to main (skips bot commits + [skip ci]): | +# | | +# | Every push: | +# | 1. Read version from README.md | +# | 3. Set platform version (Joomla ) | +# | 4. Update [VERSION: XX.YY.ZZ] badges in markdown files | +# | 5. Write updates.xml (Joomla update server XML) | +# | 6. Create git tag vXX.YY.ZZ | +# | 7a. Patch: update existing Gitea Release for this minor | +# | 8. Build ZIP, upload asset, write SHA-256 to updates.xml | +# | | +# | Every version change: archives main -> version/XX.YY branch | +# | All patches release (including 00). Patch 00/01 = full pipeline. | +# | First release only (patch == 01): | +# | 7b. Create new Gitea Release | +# | | +# | GitHub mirror: stable/rc releases only (continue-on-error) | +# | | +# +========================================================================+ + +name: Build & Release + +on: + pull_request: + types: [closed] + branches: + - main + paths: + - 'src/**' + - 'htdocs/**' + workflow_dispatch: + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +permissions: + contents: write + +jobs: + release: + name: Build & Release Pipeline + runs-on: release + if: >- + github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.GA_TOKEN }} + fetch-depth: 0 + + - name: Setup MokoStandards tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN }}"}}' + run: | + # Ensure PHP + Composer are available + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \ + /tmp/mokostandards-api + cd /tmp/mokostandards-api + composer install --no-dev --no-interaction --quiet + + # -- STEP 1: Read version ----------------------------------------------- + - name: "Step 1: Read version from README.md" + id: version + run: | + VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null) + if [ -z "$VERSION" ]; then + echo "No VERSION in README.md — skipping release" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + # Derive major.minor for branch naming (patches update existing branch) + MINOR=$(echo "$VERSION" | awk -F. '{printf "%s.%s", $1, $2}') + PATCH=$(echo "$VERSION" | awk -F. '{print $3}') + + MAJOR=$(echo "$VERSION" | awk -F. '{print $1}') + MINOR_NUM=$(echo "$VERSION" | awk -F. '{print $2}') + + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT" + echo "minor=$MINOR" >> "$GITHUB_OUTPUT" + echo "major=$MAJOR" >> "$GITHUB_OUTPUT" + echo "release_tag=stable" >> "$GITHUB_OUTPUT" + echo "stability=stable" >> "$GITHUB_OUTPUT" + echo "skip=false" >> "$GITHUB_OUTPUT" + if [ "$PATCH" = "00" ] || [ "$PATCH" = "01" ]; then + echo "is_minor=true" >> "$GITHUB_OUTPUT" + echo "Version: $VERSION (first release for this minor — full pipeline)" + else + echo "is_minor=false" >> "$GITHUB_OUTPUT" + echo "Version: $VERSION (patch — platform version + badges only)" + fi + + # -- STEP 1b: Bump minor version (stable = minor bump, reset patch) ------ + - name: "Step 1b: Bump minor version for stable release" + if: steps.version.outputs.skip != 'true' + id: bump + run: | + CURRENT=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1) + [ -z "$CURRENT" ] && { echo "skip=true" >> "$GITHUB_OUTPUT"; exit 0; } + + MAJOR=$((10#$(echo "$CURRENT" | cut -d. -f1))) + MINOR=$((10#$(echo "$CURRENT" | cut -d. -f2))) + + # Minor bump, reset patch. Rollover if minor > 99 + MINOR=$((MINOR + 1)) + if [ $MINOR -gt 99 ]; then + MINOR=0 + MAJOR=$((MAJOR + 1)) + fi + + VERSION=$(printf "%02d.%02d.00" $MAJOR $MINOR) + TODAY=$(date +%Y-%m-%d) + + echo "Stable bump: ${CURRENT} → ${VERSION} (minor)" + + # Update README.md + sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${VERSION}/" README.md + + # Update manifest + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) + if [ -n "$MANIFEST" ]; then + MANIFEST_VER=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1) + [ -n "$MANIFEST_VER" ] && sed -i "s|${MANIFEST_VER}|${VERSION}|" "$MANIFEST" + sed -i "s|[^<]*|${TODAY}|" "$MANIFEST" + fi + + # Promote [Unreleased] section in CHANGELOG.md to new version + if [ -f "CHANGELOG.md" ] && grep -qi "Unreleased" CHANGELOG.md; then + sed -i "s|## \[Unreleased\]|## [${VERSION}] --- ${TODAY}|" CHANGELOG.md + sed -i "s|## Unreleased|## [${VERSION}] --- ${TODAY}|" CHANGELOG.md + sed -i "2i ## [Unreleased]" CHANGELOG.md + sed -i "3i \\ " CHANGELOG.md + echo "CHANGELOG promoted to [${VERSION}]" + fi + + # Commit and push + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git add -A + git diff --cached --quiet || { + git commit -m "chore(version): bump ${CURRENT} → ${VERSION} [skip ci]" + git push origin HEAD:main 2>&1 + } + + # Override version output for rest of pipeline + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "major=$(printf "%02d" $MAJOR)" >> "$GITHUB_OUTPUT" + + - name: Check if already released + if: steps.version.outputs.skip != 'true' + id: check + run: | + TAG="${{ steps.version.outputs.release_tag }}" + BRANCH="${{ steps.version.outputs.branch }}" + + TAG_EXISTS=false + BRANCH_EXISTS=false + + git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true + git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH" && BRANCH_EXISTS=true + + echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT" + echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT" + + # Tag and branch may persist across patch releases — never skip + echo "already_released=false" >> "$GITHUB_OUTPUT" + + # -- SANITY CHECKS ------------------------------------------------------- + - name: "Sanity: Pre-release validation" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + ERRORS=0 + + echo "## Pre-Release Sanity Checks (Joomla)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # -- Version drift check (must pass before release) -------- + README_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1) + if [ "$README_VER" != "$VERSION" ]; then + echo "- Version drift: README says \`${README_VER}\` but releasing \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + else + echo "- Version consistent: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + + # Check CHANGELOG version matches + CL_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' CHANGELOG.md 2>/dev/null | head -1) + if [ -n "$CL_VER" ] && [ "$CL_VER" != "$VERSION" ]; then + echo "- CHANGELOG drift: \`${CL_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + fi + + # Check composer.json version if present + if [ -f "composer.json" ]; then + COMP_VER=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' composer.json 2>/dev/null | head -1) + if [ -n "$COMP_VER" ] && [ "$COMP_VER" != "$VERSION" ]; then + echo "- composer.json drift: \`${COMP_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + fi + fi + + # Common checks + if [ ! -f "LICENSE" ]; then + echo "- Missing LICENSE file" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + else + echo "- LICENSE present" >> $GITHUB_STEP_SUMMARY + fi + + if [ ! -d "src" ] && [ ! -d "htdocs" ]; then + echo "- Warning: No src/ or htdocs/ directory" >> $GITHUB_STEP_SUMMARY + else + echo "- Source directory present" >> $GITHUB_STEP_SUMMARY + fi + + # -- Joomla: manifest version drift -------- + MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1) + if [ -n "$MANIFEST" ]; then + XML_VER=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1) + if [ -n "$XML_VER" ] && [ "$XML_VER" != "$VERSION" ]; then + echo "- Manifest drift: \`${XML_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + else + echo "- Manifest version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + fi + + # -- Joomla: XML manifest existence -------- + if [ -z "$MANIFEST" ]; then + echo "- No Joomla XML manifest found" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + else + echo "- Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY + + # -- Joomla: extension type check -------- + TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null) + echo "- Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "$ERRORS" -gt 0 ]; then + echo "**${ERRORS} error(s) — release may be incomplete**" >> $GITHUB_STEP_SUMMARY + else + echo "**All sanity checks passed**" >> $GITHUB_STEP_SUMMARY + fi + + # -- STEP 2: Create or update version/XX.YY archive branch --------------- + # Always runs — every version change on main archives to version/XX.YY + - name: "Step 2: Version archive branch" + if: steps.check.outputs.already_released != 'true' + run: | + BRANCH="${{ steps.version.outputs.branch }}" + IS_MINOR="${{ steps.version.outputs.is_minor }}" + PATCH="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}') + + # Check if branch exists + if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then + git push origin HEAD:"$BRANCH" --force + echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY + else + git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH" + git push origin "$BRANCH" --force + echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY + fi + + # -- STEP 3: Set platform version ---------------------------------------- + - name: "Step 3: Set platform version" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + php /tmp/mokostandards-api/cli/version_set_platform.php \ + --path . --version "$VERSION" --branch main + + # -- STEP 4: Update version badges ---------------------------------------- + - name: "Step 4: Update version badges" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + find . -name "*.md" ! -path "./.git/*" ! -path "./vendor/*" | while read -r f; do + if grep -q '\[VERSION:' "$f" 2>/dev/null; then + sed -i "s/\[VERSION:[[:space:]]*[0-9]\{2\}\.[0-9]\{2\}\.[0-9]\{2\}\]/[VERSION: ${VERSION}]/" "$f" + fi + done + + # -- STEP 5: Write updates.xml (Joomla update server) --------------------- + - name: "Step 5: Write updates.xml" + id: updates + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + REPO="${{ github.repository }}" + + # -- Parse extension metadata from XML manifest ---------------- + MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1) + if [ -z "$MANIFEST" ]; then + echo "Warning: No Joomla XML manifest found — skipping updates.xml" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + # Extract fields using sed (portable — no grep -P) + EXT_NAME=$(sed -n 's/.*\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1) + EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1) + EXT_CLIENT=$(sed -n 's/.*]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_FOLDER=$(sed -n 's/.*]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + TARGET_PLATFORM=$(sed -n 's/.*\(\).*/\1/p' "$MANIFEST" | head -1) + PHP_MINIMUM=$(sed -n 's/.*\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1) + + # If EXT_NAME is a language key (e.g. PLG_SYSTEM_MOKOJGDPC), resolve from .ini + if echo "$EXT_NAME" | grep -qE '^[A-Z_]+$'; then + INI_NAME=$(find . -name "*.sys.ini" -path "*/en-GB/*" -exec grep -h "^${EXT_NAME}=" {} \; 2>/dev/null | head -1 | cut -d'"' -f2) + [ -z "$INI_NAME" ] && INI_NAME=$(find . -name "*.sys.ini" -exec grep -h "^${EXT_NAME}=" {} \; 2>/dev/null | head -1 | cut -d'"' -f2) + [ -n "$INI_NAME" ] && EXT_NAME="$INI_NAME" + fi + + # Fallbacks + [ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}" + [ -z "$EXT_TYPE" ] && EXT_TYPE="component" + + # Derive element if not in manifest: + # 1. plugin="xxx" attribute (plugins) + # 2. module="xxx" attribute (modules) + # 3. XML filename (components, packages) + # 4. Repo name fallback (templates, anything else) + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(sed -n 's/.*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + fi + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(sed -n 's/.*module="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + fi + if [ -z "$EXT_ELEMENT" ]; then + FNAME=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') + # If filename is generic (templateDetails, manifest), use repo name + case "$FNAME" in + templatedetails|manifest) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;; + *) EXT_ELEMENT="$FNAME" ;; + esac + fi + # Final fallback + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + + # Save for Steps 7, 8, 8b + echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" + echo "ext_name=${EXT_NAME}" >> "$GITHUB_OUTPUT" + echo "ext_type=${EXT_TYPE}" >> "$GITHUB_OUTPUT" + echo "ext_folder=${EXT_FOLDER}" >> "$GITHUB_OUTPUT" + + # Build client tag: plugins and frontend modules need site + CLIENT_TAG="" + if [ -n "$EXT_CLIENT" ]; then + CLIENT_TAG="${EXT_CLIENT}" + elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then + CLIENT_TAG="site" + fi + + # Build folder tag for plugins (required for Joomla to match the update) + FOLDER_TAG="" + if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then + FOLDER_TAG="${EXT_FOLDER}" + fi + + # Build targetplatform (fallback to Joomla 5 if not in manifest) + if [ -z "$TARGET_PLATFORM" ]; then + TARGET_PLATFORM=$(printf '' "/") + fi + + # Build php_minimum tag + PHP_TAG="" + if [ -n "$PHP_MINIMUM" ]; then + PHP_TAG="${PHP_MINIMUM}" + fi + + # Build TYPE_PREFIX for download URL + TYPE_PREFIX="" + case "${EXT_TYPE}" in + plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; + module) TYPE_PREFIX="mod_" ;; + component) TYPE_PREFIX="com_" ;; + template) TYPE_PREFIX="tpl_" ;; + library) TYPE_PREFIX="lib_" ;; + package) TYPE_PREFIX="pkg_" ;; + esac + + DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/stable/${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip" + INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/stable" + + # -- Build update entry for a given stability tag + build_entry() { + local TAG_NAME="$1" + printf '%s\n' ' ' + printf '%s\n' " ${EXT_NAME}" + printf '%s\n' " ${EXT_NAME} update" + printf '%s\n' " ${EXT_ELEMENT}" + printf '%s\n' " ${EXT_TYPE}" + printf '%s\n' " ${VERSION}" + [ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}" + [ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}" + printf '%s\n' " ${TAG_NAME}" + printf '%s\n' " ${INFO_URL}" + printf '%s\n' ' ' + printf '%s\n' " ${DOWNLOAD_URL}" + printf '%s\n' ' ' + printf '%s\n' " ${TARGET_PLATFORM}" + [ -n "$PHP_TAG" ] && printf '%s\n' " ${PHP_TAG}" + printf '%s\n' ' Moko Consulting' + printf '%s\n' ' https://mokoconsulting.tech' + printf '%s\n' ' ' + } + + # -- Write updates.xml with cascading channels + # Stable release updates ALL channels (development, alpha, beta, rc, stable) + { + printf '%s\n' "" + printf '%s\n' "" + printf '%s\n' "" + printf '%s\n' '' + build_entry "development" + build_entry "alpha" + build_entry "beta" + build_entry "rc" + build_entry "stable" + printf '%s\n' '' + } > updates.xml + + echo "updates.xml: ${VERSION} (all channels updated to stable)" >> $GITHUB_STEP_SUMMARY + + # -- Commit all changes --------------------------------------------------- + - name: Commit release changes + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + if git diff --quiet && git diff --cached --quiet; then + echo "No changes to commit" + exit 0 + fi + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + # Set push URL with token for branch-protected repos + git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git add -A + git commit -m "chore(release): build ${VERSION} [skip ci]" \ + --author="gitea-actions[bot] " + git push -u origin HEAD + + # -- STEP 6: Create tag --------------------------------------------------- + - name: "Step 6: Create git tag" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.tag_exists != 'true' && + steps.version.outputs.is_minor == 'true' + run: | + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + # Only create the major release tag if it doesn't exist yet + if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then + git tag "$RELEASE_TAG" + git push origin "$RELEASE_TAG" + echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY + else + echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY + fi + echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY + + # -- STEP 7: Create or update Gitea Release -------------------------------- + - name: "Step 7: Gitea Release" + if: >- + steps.version.outputs.skip != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + BRANCH="${{ steps.version.outputs.branch }}" + MAJOR="${{ steps.version.outputs.major }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + # Reuse metadata from Step 5 (single source of truth) + EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}" + EXT_NAME="${{ steps.updates.outputs.ext_name }}" + EXT_TYPE="${{ steps.updates.outputs.ext_type }}" + EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}" + + # Fallbacks if Step 5 was skipped + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + fi + [ -z "$EXT_NAME" ] && EXT_NAME="${GITEA_REPO}" + + NOTES=$(php /tmp/mokostandards-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null) + [ -z "$NOTES" ] && NOTES="Release ${VERSION}" + + # Build release name: "Pretty Name VERSION (type_element-VERSION)" + TYPE_PREFIX="" + case "${EXT_TYPE}" in + plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; + module) TYPE_PREFIX="mod_" ;; + component) TYPE_PREFIX="com_" ;; + template) TYPE_PREFIX="tpl_" ;; + library) TYPE_PREFIX="lib_" ;; + package) TYPE_PREFIX="pkg_" ;; + esac + RELEASE_NAME="${EXT_NAME} ${VERSION} (${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION})" + + # Delete existing release if present (overwrite, not append) + EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true) + EXISTING_ID=$(echo "$EXISTING" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true) + + if [ -n "$EXISTING_ID" ]; then + curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/${EXISTING_ID}" 2>/dev/null || true + curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/tags/${RELEASE_TAG}" 2>/dev/null || true + echo "Deleted previous stable release (id: ${EXISTING_ID})" + fi + + # Create fresh release + curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/releases" \ + -d "$(python3 -c "import json; print(json.dumps({ + 'tag_name': '${RELEASE_TAG}', + 'name': '${RELEASE_NAME}', + 'body': '''## ${VERSION} ($(date +%Y-%m-%d))\n${NOTES}''', + 'target_commitish': '${BRANCH}' + }))")" + echo "Release created: ${RELEASE_NAME}" >> $GITHUB_STEP_SUMMARY + + # -- STEP 8: Build Joomla install ZIP + SHA-256 checksum ------------------ + - name: "Step 8: Build Joomla package and update checksum" + if: >- + steps.version.outputs.skip != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + REPO="${{ github.repository }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + # All ZIPs upload to the major release tag (vXX) + RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true) + RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + if [ -z "$RELEASE_ID" ]; then + echo "No release ${RELEASE_TAG} found — skipping ZIP upload" + exit 0 + fi + + # Find extension element name from manifest + MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1 || true) + [ -z "$MANIFEST" ] && exit 0 + + # Reuse element from Step 5, with same fallback chain + EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}" + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1) + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(sed -n 's/.*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null | head -1) + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + fi + # ZIP name: type_folder_element-VERSION (e.g. plg_system_mokojgdpc-01.01.00.zip) + EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_FOLDER=$(sed -n 's/.*]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + TYPE_PREFIX="" + case "${EXT_TYPE}" in + plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; + module) TYPE_PREFIX="mod_" ;; + component) TYPE_PREFIX="com_" ;; + template) TYPE_PREFIX="tpl_" ;; + library) TYPE_PREFIX="lib_" ;; + package) TYPE_PREFIX="pkg_" ;; + esac + ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip" + TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz" + + # -- Build install packages from src/ ---------------------------- + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + [ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ — skipping package"; exit 0; } + + EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*" + + # ZIP package + cd "$SOURCE_DIR" + zip -r "/tmp/${ZIP_NAME}" . -x $EXCLUDES + cd .. + + # tar.gz package + tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \ + --exclude='.ftpignore' --exclude='sftp-config*' \ + --exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' . + + ZIP_SIZE=$(stat -c%s "/tmp/${ZIP_NAME}" 2>/dev/null || stat -f%z "/tmp/${ZIP_NAME}" 2>/dev/null || echo "unknown") + TAR_SIZE=$(stat -c%s "/tmp/${TAR_NAME}" 2>/dev/null || stat -f%z "/tmp/${TAR_NAME}" 2>/dev/null || echo "unknown") + + # -- Calculate SHA-256 for both ---------------------------------- + SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1) + SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1) + + # -- Delete existing assets with same name before uploading ------ + ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]") + for ASSET_NAME in "$ZIP_NAME" "$TAR_NAME"; do + ASSET_ID=$(echo "$ASSETS" | python3 -c " + import sys,json + assets = json.load(sys.stdin) + for a in assets: + if a['name'] == '${ASSET_NAME}': + print(a['id']); break + " 2>/dev/null || true) + if [ -n "$ASSET_ID" ]; then + curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true + fi + done + + # -- Upload both to release tag ---------------------------------- + curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @"/tmp/${ZIP_NAME}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" > /dev/null 2>&1 || true + + curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @"/tmp/${TAR_NAME}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets?name=${TAR_NAME}" > /dev/null 2>&1 || true + + # -- Update updates.xml with both download formats --------------- + if [ -f "updates.xml" ]; then + ZIP_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}" + TAR_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${TAR_NAME}" + + # Use Python to update only the stable entry's downloads + sha256 + export PY_ZIP_URL="$ZIP_URL" PY_TAR_URL="$TAR_URL" PY_SHA="$SHA256_ZIP" + python3 << 'PYEOF' + import re, os + + with open("updates.xml") as f: + content = f.read() + + zip_url = os.environ["PY_ZIP_URL"] + tar_url = os.environ["PY_TAR_URL"] + sha = os.environ["PY_SHA"] + + # Find the stable update block and replace its downloads + sha256 + def replace_stable(m): + block = m.group(0) + # Replace downloads block + new_downloads = ( + " \n" + f" {zip_url}\n" + " " + ) + block = re.sub(r' .*?', new_downloads, block, flags=re.DOTALL) + # Add or replace sha256 + if '' in block: + block = re.sub(r' .*?', f' {sha}', block) + else: + block = block.replace('', f'\n {sha}') + return block + + content = re.sub( + r' .*?stable.*?', + replace_stable, + content, + flags=re.DOTALL + ) + + with open("updates.xml", "w") as f: + f.write(content) + PYEOF + + CURRENT_BRANCH="${{ github.ref_name }}" + git add updates.xml + git commit -m "chore(release): ZIP + tar.gz for ${VERSION} [skip ci]" \ + --author="gitea-actions[bot] " || true + git push || true + + # Sync updates.xml to main via direct API (always runs — may be on version/XX branch) + GA_TOKEN="${{ secrets.GA_TOKEN }}" + API="${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}" + + FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \ + "${API}/contents/updates.xml?ref=main" | jq -r '.sha // empty') + + if [ -n "$FILE_SHA" ]; then + CONTENT=$(base64 -w0 updates.xml) + curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/contents/updates.xml" \ + -d "$(jq -n \ + --arg content "$CONTENT" \ + --arg sha "$FILE_SHA" \ + --arg msg "chore: sync updates.xml ${VERSION} [skip ci]" \ + --arg branch "main" \ + '{content: $content, sha: $sha, message: $msg, branch: $branch}' + )" > /dev/null 2>&1 \ + && echo "updates.xml synced to main via API" \ + || echo "WARNING: failed to sync updates.xml to main" + else + echo "WARNING: could not get updates.xml SHA from main" + fi + fi + + echo "### Joomla Packages" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Package | Size | SHA-256 |" >> $GITHUB_STEP_SUMMARY + echo "|---------|------|---------|" >> $GITHUB_STEP_SUMMARY + echo "| \`${ZIP_NAME}\` | ${ZIP_SIZE} | \`${SHA256_ZIP}\` |" >> $GITHUB_STEP_SUMMARY + echo "| \`${TAR_NAME}\` | ${TAR_SIZE} | \`${SHA256_TAR}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Release | \`${RELEASE_TAG}\` | |" >> $GITHUB_STEP_SUMMARY + echo "| Download | [${ZIP_NAME}](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}) |" >> $GITHUB_STEP_SUMMARY + + # -- STEP 8b: Update release description with changelog + SHA ---------------- + - name: "Step 8b: Update release body with changelog and SHA" + if: steps.version.outputs.skip != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}" + EXT_TYPE="${{ steps.updates.outputs.ext_type }}" + EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}" + + # Build TYPE_PREFIX to match Step 8's ZIP naming + TYPE_PREFIX="" + case "${EXT_TYPE}" in + plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; + module) TYPE_PREFIX="mod_" ;; + component) TYPE_PREFIX="com_" ;; + template) TYPE_PREFIX="tpl_" ;; + library) TYPE_PREFIX="lib_" ;; + package) TYPE_PREFIX="pkg_" ;; + esac + ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip" + TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz" + + # Get SHA from the built files + SHA256_ZIP="" + [ -f "/tmp/${ZIP_NAME}" ] && SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1) + SHA256_TAR="" + [ -f "/tmp/${TAR_NAME}" ] && SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1) + + # Extract latest changelog entry (strip the ## header to avoid duplicate) + CHANGELOG="" + if [ -f "CHANGELOG.md" ]; then + CHANGELOG=$(sed -n "/^## \[*${VERSION}/,/^## \[*[0-9]/p" CHANGELOG.md | sed '$d' | sed '1d') + [ -z "$CHANGELOG" ] && CHANGELOG=$(sed -n '/^## /,/^## /p' CHANGELOG.md | sed '$d' | sed '1d' | head -30) + fi + + # Build release body (single header, no duplicate from changelog) + BODY="## ${VERSION} ($(date +%Y-%m-%d))\n\n" + if [ -n "$CHANGELOG" ]; then + BODY="${BODY}${CHANGELOG}\n\n" + fi + BODY="${BODY}---\n\n### Checksums\n\n" + BODY="${BODY}| File | SHA-256 |\n|------|--------|\n" + [ -n "$SHA256_ZIP" ] && BODY="${BODY}| \`${ZIP_NAME}\` | \`${SHA256_ZIP}\` |\n" + [ -n "$SHA256_TAR" ] && BODY="${BODY}| \`${TAR_NAME}\` | \`${SHA256_TAR}\` |\n" + + # Get release ID and update body + RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null | \ + python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + + if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then + python3 -c " + import json, urllib.request + body = '''$(printf '%b' "$BODY")''' + data = json.dumps({'body': body}).encode() + req = urllib.request.Request( + '${API_BASE}/releases/${RELEASE_ID}', + data=data, + headers={'Authorization': 'token ${{ secrets.GA_TOKEN }}', 'Content-Type': 'application/json'}, + method='PATCH' + ) + urllib.request.urlopen(req) + " 2>/dev/null && echo "Release body updated with changelog + SHA" >> $GITHUB_STEP_SUMMARY + fi + + # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- + - name: "Step 9: Mirror release to GitHub" + if: >- + steps.version.outputs.skip != 'true' && + steps.version.outputs.stability == 'stable' && + secrets.GH_TOKEN != '' + continue-on-error: true + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + MAJOR="${{ steps.version.outputs.major }}" + BRANCH="${{ steps.version.outputs.branch }}" + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + + NOTES=$(php /tmp/mokostandards-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null || true) + [ -z "$NOTES" ] && NOTES="Release ${VERSION}" + echo "$NOTES" > /tmp/release_notes.md + + EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".tag_name // empty" || true) + + if [ -z "$EXISTING" ]; then + gh release create "$RELEASE_TAG" \ + --repo "$GH_REPO" \ + --title "v${MAJOR} (latest: ${VERSION})" \ + --notes-file /tmp/release_notes.md \ + --target "$BRANCH" || true + else + gh release edit "$RELEASE_TAG" \ + --repo "$GH_REPO" \ + --title "v${MAJOR} (latest: ${VERSION})" || true + fi + + # Upload assets to GitHub mirror + for PKG in /tmp/${EXT_ELEMENT:-pkg}-${VERSION}.*; do + if [ -f "$PKG" ]; then + _RELID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".id // empty") + [ -n "$_RELID" ] && curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" -H "Content-Type: application/octet-stream" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/${_RELID}/assets?name=$(basename $PKG)" --data-binary "@$PKG" > /dev/null 2>&1 || true + fi + done + echo "GitHub mirror updated: ${GH_REPO} ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY + + # -- STEP 10: Sync main branch to GitHub mirror ---------------------------- + - name: "Step 10: Push main to GitHub mirror" + if: >- + steps.version.outputs.skip != 'true' && + secrets.GH_TOKEN != '' + continue-on-error: true + run: | + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1) + GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2) + git remote add github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ + git remote set-url github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" + git fetch origin main --depth=1 + git push github origin/main:refs/heads/main --force 2>/dev/null \ + && echo "main branch pushed to GitHub mirror" \ + || echo "WARNING: GitHub mirror push failed" + + # -- Clean up lesser pre-releases (cascade) --------------------------------- + # stable → deletes all | rc → beta,alpha,dev | beta → alpha,dev | alpha → dev + - name: "Delete lesser pre-release channels" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.GA_TOKEN }}" + + # Stable deletes all pre-release channels + TAGS_TO_DELETE="development alpha beta release-candidate" + + DELETED=0 + for TAG in $TAGS_TO_DELETE; do + RELEASE_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/releases/tags/${TAG}" 2>/dev/null | \ + python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + + if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then + curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/releases/${RELEASE_ID}" 2>/dev/null || true + curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/tags/${TAG}" 2>/dev/null || true + echo "Deleted: ${TAG} (id: ${RELEASE_ID})" + DELETED=$((DELETED + 1)) + fi + done + echo "Cleaned up ${DELETED} pre-release channel(s)" >> $GITHUB_STEP_SUMMARY + + # -- STEP 11: Reset dev branch from main ------------------------------------ + - name: "Step 11: Delete and recreate dev branch from main" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.GA_TOKEN }}" + + # Delete dev branch + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch" + + # Recreate dev from main (now includes version bump + changelog promotion) + curl -sf -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/branches" \ + -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main" + + echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY + + # -- Summary -------------------------------------------------------------- + - name: Pipeline Summary + if: always() + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + if [ "${{ steps.version.outputs.skip }}" = "true" ]; then + echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY + echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY + elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then + echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Build & Release Complete (Joomla)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY + echo "|------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY + fi -- 2.52.0 From eb186446ece0d6a7f00ec795138cd7ff34b47a45 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Mon, 11 May 2026 21:27:18 +0000 Subject: [PATCH 011/136] chore: sync cascade-dev.yml from Template-Joomla [skip ci] --- .gitea/workflows/cascade-dev.yml | 213 +++++++++++++++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 .gitea/workflows/cascade-dev.yml diff --git a/.gitea/workflows/cascade-dev.yml b/.gitea/workflows/cascade-dev.yml new file mode 100644 index 0000000..d4780b1 --- /dev/null +++ b/.gitea/workflows/cascade-dev.yml @@ -0,0 +1,213 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Maintenance +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# PATH: /templates/workflows/cascade-dev.yml.template +# VERSION: 02.00.00 +# BRIEF: Forward-merge main → all open branches after every push to main +# +# +========================================================================+ +# | CASCADE MAIN → ALL BRANCHES | +# +========================================================================+ +# | | +# | Triggers on every push to main (PR merges, bot commits, etc.) | +# | | +# | 1. List all branches matching: dev, rc/*, beta/*, alpha/* | +# | 2. For each: create PR (main → branch), auto-merge if clean | +# | 3. On conflict: leave PR open for manual resolution | +# | | +# +========================================================================+ + +name: Cascade Main → Dev + +on: + push: + branches: + - main + workflow_dispatch: + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +permissions: + contents: write + pull-requests: write + +jobs: + cascade: + name: Cascade main → branches + runs-on: ubuntu-latest + if: >- + !contains(github.event.head_commit.message, '[skip ci]') && + !contains(github.event.head_commit.message, '[skip cascade]') + + steps: + - name: Discover target branches + id: branches + env: + GA_TOKEN: ${{ secrets.GA_TOKEN }} + run: | + API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + # Fetch all branches (paginated) + PAGE=1 + ALL_BRANCHES="" + while true; do + BATCH=$(curl -sS \ + -H "Authorization: token ${GA_TOKEN}" \ + "${API}/branches?page=${PAGE}&limit=50" \ + | jq -r '.[].name // empty') + [ -z "$BATCH" ] && break + ALL_BRANCHES="$ALL_BRANCHES $BATCH" + PAGE=$((PAGE + 1)) + done + + # Filter to cascade targets: dev, dev/*, rc/*, beta/*, alpha/* + TARGETS="" + for BRANCH in $ALL_BRANCHES; do + case "$BRANCH" in + dev|dev/*|rc/*|beta/*|alpha/*) + TARGETS="$TARGETS $BRANCH" + ;; + esac + done + + TARGETS=$(echo "$TARGETS" | xargs) # trim whitespace + + if [ -z "$TARGETS" ]; then + echo "targets=" >> "$GITHUB_OUTPUT" + echo "ℹ️ No cascade target branches found" + else + echo "targets=$TARGETS" >> "$GITHUB_OUTPUT" + COUNT=$(echo "$TARGETS" | wc -w) + echo "📋 Found ${COUNT} target branch(es): ${TARGETS}" + fi + + - name: Cascade to all target branches + if: steps.branches.outputs.targets != '' + env: + GA_TOKEN: ${{ secrets.GA_TOKEN }} + run: | + API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + SHORT_SHA="${GITHUB_SHA:0:7}" + TARGETS="${{ steps.branches.outputs.targets }}" + + SUCCESS=0 + CONFLICTS=0 + SKIPPED=0 + FAILED=0 + + for BRANCH in $TARGETS; do + echo "" + echo "═══ main → ${BRANCH} ═══" + + # Check if branch is already up to date + ENCODED_BRANCH=$(echo "$BRANCH" | sed 's|/|%2F|g') + RESPONSE=$(curl -sS \ + -H "Authorization: token ${GA_TOKEN}" \ + "${API}/compare/${ENCODED_BRANCH}...main") + + AHEAD=$(echo "$RESPONSE" | jq '.total_commits // 0') + + if [ "$AHEAD" -eq 0 ]; then + echo " ✅ Already up to date" + SKIPPED=$((SKIPPED + 1)) + continue + fi + + echo " ℹ️ main is ${AHEAD} commit(s) ahead" + + # Check for existing cascade PR + EXISTING=$(curl -sS \ + -H "Authorization: token ${GA_TOKEN}" \ + "${API}/pulls?state=open&head=${GITEA_ORG}:main&base=${ENCODED_BRANCH}&limit=1") + + EXISTING_COUNT=$(echo "$EXISTING" | jq 'length') + PR_NUMBER="" + + if [ "$EXISTING_COUNT" -gt 0 ]; then + PR_NUMBER=$(echo "$EXISTING" | jq -r '.[0].number') + echo " ℹ️ Reusing existing PR #${PR_NUMBER}" + else + # Create cascade PR + PR_RESPONSE=$(curl -sS -w "\n%{http_code}" \ + -X POST \ + -H "Authorization: token ${GA_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ + \"title\": \"chore: cascade main → ${BRANCH} (${SHORT_SHA}) [skip ci]\", + \"body\": \"## Automatic cascade\\n\\nForward-merging \`main\` (${SHORT_SHA}) into \`${BRANCH}\`.\\n\\nIf conflicts exist, resolve manually and merge.\\n\\n> Auto-created by **Cascade Main → Dev**.\", + \"head\": \"main\", + \"base\": \"${BRANCH}\" + }" \ + "${API}/pulls") + + HTTP_CODE=$(echo "$PR_RESPONSE" | tail -1) + BODY=$(echo "$PR_RESPONSE" | sed '$d') + PR_NUMBER=$(echo "$BODY" | jq -r '.number // empty') + + if [ "$HTTP_CODE" != "201" ] || [ -z "$PR_NUMBER" ]; then + MSG=$(echo "$BODY" | jq -r '.message // .' 2>/dev/null | head -1) + echo " ❌ Failed to create PR (HTTP ${HTTP_CODE}): ${MSG}" + FAILED=$((FAILED + 1)) + continue + fi + + echo " ✅ Created PR #${PR_NUMBER}" + fi + + # Try auto-merge + PR_DATA=$(curl -sS \ + -H "Authorization: token ${GA_TOKEN}" \ + "${API}/pulls/${PR_NUMBER}") + + MERGEABLE=$(echo "$PR_DATA" | jq -r '.mergeable // false') + + if [ "$MERGEABLE" != "true" ]; then + echo " ⚠️ Conflicts — PR #${PR_NUMBER} left open" + CONFLICTS=$((CONFLICTS + 1)) + continue + fi + + MERGE_RESPONSE=$(curl -sS -w "\n%{http_code}" \ + -X POST \ + -H "Authorization: token ${GA_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ + \"Do\": \"merge\", + \"merge_message_field\": \"chore: cascade main → ${BRANCH} [skip ci]\", + \"delete_branch_after_merge\": false + }" \ + "${API}/pulls/${PR_NUMBER}/merge") + + MERGE_HTTP=$(echo "$MERGE_RESPONSE" | tail -1) + + if [ "$MERGE_HTTP" = "200" ] || [ "$MERGE_HTTP" = "204" ]; then + echo " ✅ Merged — ${BRANCH} is in sync" + SUCCESS=$((SUCCESS + 1)) + else + MERGE_BODY=$(echo "$MERGE_RESPONSE" | sed '$d') + echo " ⚠️ Merge failed (HTTP ${MERGE_HTTP}) — PR #${PR_NUMBER} left open" + CONFLICTS=$((CONFLICTS + 1)) + fi + done + + # Summary + echo "" + echo "════════════════════════════════════════" + echo " ✅ Merged: ${SUCCESS}" + echo " ⚠️ Conflicts: ${CONFLICTS}" + echo " ⏭️ Up to date: ${SKIPPED}" + echo " ❌ Failed: ${FAILED}" + echo "════════════════════════════════════════" + + if [ "$FAILED" -gt 0 ]; then + exit 1 + fi -- 2.52.0 From 3c72f1a6c8a5bd239a63d66142f4c2c5d98957d8 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Mon, 11 May 2026 21:27:20 +0000 Subject: [PATCH 012/136] chore: sync ci-joomla.yml from Template-Joomla [skip ci] --- .gitea/workflows/ci-joomla.yml | 450 +++++++++++++++++++++++++++++++++ 1 file changed, 450 insertions(+) create mode 100644 .gitea/workflows/ci-joomla.yml diff --git a/.gitea/workflows/ci-joomla.yml b/.gitea/workflows/ci-joomla.yml new file mode 100644 index 0000000..28cee48 --- /dev/null +++ b/.gitea/workflows/ci-joomla.yml @@ -0,0 +1,450 @@ +# Copyright (C) 2026 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow.Template +# INGROUP: MokoStandards.CI +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# PATH: /templates/workflows/joomla/ci-joomla.yml.template +# VERSION: 04.06.00 +# BRIEF: CI workflow for Joomla extensions — lint, validate, test + +name: Joomla Extension CI + +on: + pull_request: + branches: + - main + - 'dev/**' + workflow_dispatch: + +permissions: + contents: read + pull-requests: write + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + lint-and-validate: + name: Lint & Validate + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP + run: | + php -v && composer --version + + - name: Clone MokoStandards + env: + GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} + MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} + MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }} + run: | + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \ + /tmp/mokostandards-api + + - name: Install dependencies + env: + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}' + run: | + if [ -f "composer.json" ]; then + composer install \ + --no-interaction \ + --prefer-dist \ + --optimize-autoloader + else + echo "No composer.json found — skipping dependency install" + fi + + - name: PHP syntax check + run: | + ERRORS=0 + for DIR in src/ htdocs/; do + if [ -d "$DIR" ]; then + FOUND=1 + while IFS= read -r -d '' FILE; do + OUTPUT=$(php -l "$FILE" 2>&1) + if echo "$OUTPUT" | grep -q "Parse error"; then + echo "::error file=${FILE}::${OUTPUT}" + ERRORS=$((ERRORS + 1)) + fi + done < <(find "$DIR" -name "*.php" -print0) + fi + done + echo "### PHP Syntax Check" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} syntax error(s) found.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY + fi + + - name: XML manifest validation + run: | + echo "### XML Manifest Validation" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + # Find the extension manifest (XML with /dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No Joomla extension manifest found (XML file with \`> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Manifest found: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY + + # Validate well-formed XML + php -r " + \$xml = @simplexml_load_file('$MANIFEST'); + if (\$xml === false) { + echo 'INVALID'; + exit(1); + } + echo 'VALID'; + " > /tmp/xml_result 2>&1 + XML_RESULT=$(cat /tmp/xml_result) + if [ "$XML_RESULT" != "VALID" ]; then + echo "Manifest is not well-formed XML." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Manifest is well-formed XML." >> $GITHUB_STEP_SUMMARY + fi + + # Check required tags: name, version, author, namespace (Joomla 5+) + for TAG in name version author namespace; do + if ! grep -q "<${TAG}>" "$MANIFEST" 2>/dev/null; then + echo "Missing required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Found required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY + fi + done + fi + + if [ "${ERRORS}" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**${ERRORS} manifest issue(s) found.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Manifest validation passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Check language files referenced in manifest + run: | + echo "### Language File Check" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -n "$MANIFEST" ]; then + # Extract language file references from manifest + LANG_FILES=$(grep -oP 'language\s+tag="[^"]*"[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true) + if [ -z "$LANG_FILES" ]; then + echo "No language file references found in manifest — skipping." >> $GITHUB_STEP_SUMMARY + else + while IFS= read -r LANG_FILE; do + LANG_FILE=$(echo "$LANG_FILE" | xargs) + if [ -z "$LANG_FILE" ]; then + continue + fi + # Check in common locations + FOUND=0 + for BASE in "." "src" "htdocs"; do + if [ -f "${BASE}/${LANG_FILE}" ]; then + FOUND=1 + break + fi + done + if [ "$FOUND" -eq 0 ]; then + echo "Missing language file: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Language file present: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY + fi + done <<< "$LANG_FILES" + fi + else + echo "No manifest found — skipping language check." >> $GITHUB_STEP_SUMMARY + fi + + if [ "${ERRORS}" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**${ERRORS} missing language file(s).**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Language file check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Check index.html files in directories + run: | + echo "### Index.html Check" >> $GITHUB_STEP_SUMMARY + MISSING=0 + CHECKED=0 + + for DIR in src/ htdocs/; do + if [ -d "$DIR" ]; then + while IFS= read -r -d '' SUBDIR; do + CHECKED=$((CHECKED + 1)) + if [ ! -f "${SUBDIR}/index.html" ]; then + echo "Missing index.html in: \`${SUBDIR}\`" >> $GITHUB_STEP_SUMMARY + MISSING=$((MISSING + 1)) + fi + done < <(find "$DIR" -type d -print0) + fi + done + + if [ "${CHECKED}" -eq 0 ]; then + echo "No src/ or htdocs/ directories found — skipping." >> $GITHUB_STEP_SUMMARY + elif [ "${MISSING}" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**${MISSING} director(ies) missing index.html out of ${CHECKED} checked.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "All ${CHECKED} directories contain index.html." >> $GITHUB_STEP_SUMMARY + fi + + release-readiness: + name: Release Readiness Check + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' && github.base_ref == 'main' + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Validate release readiness + run: | + echo "## Release Readiness" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + # Extract version from README.md + README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md | head -1) + if [ -z "$README_VERSION" ]; then + echo "No VERSION found in README.md FILE INFORMATION block." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "README version: \`${README_VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + + # Find the extension manifest + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No Joomla extension manifest found." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY + + # Check matches README VERSION + MANIFEST_VERSION=$(grep -oP '\K[^<]+' "$MANIFEST" | head -1) + if [ -z "$MANIFEST_VERSION" ]; then + echo "No \`\` tag in manifest." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + elif [ -n "$README_VERSION" ] && [ "$MANIFEST_VERSION" != "$README_VERSION" ]; then + echo "Manifest version \`${MANIFEST_VERSION}\` does not match README \`${README_VERSION}\`." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Manifest version: \`${MANIFEST_VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + + # Check extension type, element, client attributes + EXT_TYPE=$(grep -oP ']*\btype="\K[^"]+' "$MANIFEST" | head -1) + if [ -z "$EXT_TYPE" ]; then + echo "Missing \`type\` attribute on \`\` tag." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Extension type: \`${EXT_TYPE}\`" >> $GITHUB_STEP_SUMMARY + fi + + # Element check (component/module/plugin name) + HAS_ELEMENT=$(grep -cP '<(element|name)>' "$MANIFEST" 2>/dev/null || echo "0") + if [ "$HAS_ELEMENT" -eq 0 ]; then + echo "Missing \`\` or \`\` in manifest." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + + # Client attribute for site/admin modules and plugins + if echo "$EXT_TYPE" | grep -qP "^(module|plugin)$"; then + HAS_CLIENT=$(grep -cP ']*\bclient=' "$MANIFEST" 2>/dev/null || echo "0") + if [ "$HAS_CLIENT" -eq 0 ]; then + echo "Missing \`client\` attribute for ${EXT_TYPE} extension." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + fi + fi + + # Check updates.xml exists + if [ -f "updates.xml" ] || [ -f "updates.xml" ]; then + echo "Update XML present." >> $GITHUB_STEP_SUMMARY + else + echo "No updates.xml found." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + + # Check CHANGELOG.md exists + if [ -f "CHANGELOG.md" ]; then + echo "CHANGELOG.md present." >> $GITHUB_STEP_SUMMARY + else + echo "No CHANGELOG.md found." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ $ERRORS -gt 0 ]; then + echo "**${ERRORS} issue(s) must be resolved before release.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Extension is ready for release.**" >> $GITHUB_STEP_SUMMARY + fi + + test: + name: Tests (PHP ${{ matrix.php }}) + runs-on: ubuntu-latest + needs: lint-and-validate + + strategy: + fail-fast: false + matrix: + php: ['8.2', '8.3'] + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP ${{ matrix.php }} + run: | + php -v && composer --version + + - name: Install dependencies + env: + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}' + run: | + if [ -f "composer.json" ]; then + composer install \ + --no-interaction \ + --prefer-dist \ + --optimize-autoloader + else + echo "No composer.json found — skipping dependency install" + fi + + - name: Run tests + run: | + echo "### Test Results (PHP ${{ matrix.php }})" >> $GITHUB_STEP_SUMMARY + if [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then + vendor/bin/phpunit --testdox 2>&1 | tee /tmp/test-output.log + EXIT=${PIPESTATUS[0]} + if [ $EXIT -eq 0 ]; then + echo "All tests passed." >> $GITHUB_STEP_SUMMARY + else + echo "Test failures detected — see log." >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + cat /tmp/test-output.log >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + fi + exit $EXIT + else + echo "No phpunit.xml found — skipping tests." >> $GITHUB_STEP_SUMMARY + fi + + static-analysis: + name: PHPStan Analysis + runs-on: ubuntu-latest + needs: lint-and-validate + continue-on-error: true + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP + run: php -v && composer --version + + - name: Install dependencies + env: + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}' + run: | + if [ -f "composer.json" ]; then + composer install --no-interaction --prefer-dist --optimize-autoloader + fi + + - name: Install PHPStan + run: | + if ! command -v vendor/bin/phpstan &> /dev/null; then + composer require --dev phpstan/phpstan --no-interaction 2>/dev/null || \ + composer global require phpstan/phpstan --no-interaction + fi + + - name: Run PHPStan + run: | + echo "### PHPStan Static Analysis" >> $GITHUB_STEP_SUMMARY + PHPSTAN="vendor/bin/phpstan" + if [ ! -f "$PHPSTAN" ]; then + PHPSTAN=$(composer global config bin-dir --absolute 2>/dev/null)/phpstan + fi + + # Determine source directory + SRC_DIR="" + for DIR in src/ htdocs/ lib/; do + if [ -d "$DIR" ]; then + SRC_DIR="$DIR" + break + fi + done + + if [ -z "$SRC_DIR" ]; then + echo "No source directory found (src/, htdocs/, lib/) — skipping." >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + # Use repo phpstan.neon if present, otherwise use baseline config + ARGS="analyse ${SRC_DIR} --memory-limit=512M --no-progress --error-format=table" + if [ -f "phpstan.neon" ] || [ -f "phpstan.neon.dist" ]; then + echo "Using project PHPStan config." >> $GITHUB_STEP_SUMMARY + else + ARGS="$ARGS --level=3" + echo "No phpstan.neon found — using level 3 (type inference)." >> $GITHUB_STEP_SUMMARY + fi + + $PHPSTAN $ARGS 2>&1 | tee /tmp/phpstan-output.txt + EXIT=${PIPESTATUS[0]} + + if [ $EXIT -eq 0 ]; then + echo "**No errors found.**" >> $GITHUB_STEP_SUMMARY + else + ERRORS=$(grep -c "ERROR" /tmp/phpstan-output.txt 2>/dev/null || echo "some") + echo "**${ERRORS} error(s) found.** Review output above." >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + tail -30 /tmp/phpstan-output.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + fi + exit $EXIT -- 2.52.0 From 1df818fa7ad14b49d6ea464930a2ff6112d501cd Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Mon, 11 May 2026 21:27:22 +0000 Subject: [PATCH 013/136] chore: sync cleanup.yml from Template-Joomla [skip ci] --- .gitea/workflows/cleanup.yml | 87 ++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 .gitea/workflows/cleanup.yml diff --git a/.gitea/workflows/cleanup.yml b/.gitea/workflows/cleanup.yml new file mode 100644 index 0000000..78aa0c3 --- /dev/null +++ b/.gitea/workflows/cleanup.yml @@ -0,0 +1,87 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Maintenance +# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards +# PATH: /.gitea/workflows/cleanup.yml +# VERSION: 01.00.00 +# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs + +name: Repository Cleanup + +on: + schedule: + - cron: '0 3 * * 0' # Weekly on Sunday at 03:00 UTC + workflow_dispatch: + +permissions: + contents: write + +env: + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + +jobs: + cleanup: + name: Clean Merged Branches + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GA_TOKEN }} + + - name: Delete merged branches + env: + GA_TOKEN: ${{ secrets.GA_TOKEN }} + run: | + echo "=== Merged Branch Cleanup ===" + API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" + + # List branches via API + BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \ + "${API}/branches?limit=50" | jq -r '.[].name') + + DELETED=0 + for BRANCH in $BRANCHES; do + # Skip protected branches + case "$BRANCH" in + main|master|develop|release/*|hotfix/*) continue ;; + esac + + # 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}" \ + "${API}/branches/${BRANCH}" 2>/dev/null || true + DELETED=$((DELETED + 1)) + fi + done + + echo "Deleted ${DELETED} merged branch(es)" + + - name: Clean old workflow runs + env: + GA_TOKEN: ${{ secrets.GA_TOKEN }} + run: | + echo "=== Workflow Run Cleanup ===" + API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" + CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ) + + # Get old completed runs + RUNS=$(curl -sS -H "Authorization: token ${GA_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}" \ + "${API}/actions/runs/${RUN_ID}" 2>/dev/null || true + DELETED=$((DELETED + 1)) + done + + echo "Deleted ${DELETED} old workflow run(s)" -- 2.52.0 From 9da07b568209fd65e21338c0ec94f200737162c0 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Mon, 11 May 2026 21:27:23 +0000 Subject: [PATCH 014/136] chore: sync deploy-manual.yml from Template-Joomla [skip ci] --- .gitea/workflows/deploy-manual.yml | 126 +++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 .gitea/workflows/deploy-manual.yml diff --git a/.gitea/workflows/deploy-manual.yml b/.gitea/workflows/deploy-manual.yml new file mode 100644 index 0000000..a81cfa5 --- /dev/null +++ b/.gitea/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: 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: + GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} + MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} + MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }} + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_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 -- 2.52.0 From f50f9e95651ab7622a2f038da3485ecd6a047ce1 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Mon, 11 May 2026 21:27:25 +0000 Subject: [PATCH 015/136] chore: sync gitleaks.yml from Template-Joomla [skip ci] --- .gitea/workflows/gitleaks.yml | 96 +++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 .gitea/workflows/gitleaks.yml diff --git a/.gitea/workflows/gitleaks.yml b/.gitea/workflows/gitleaks.yml new file mode 100644 index 0000000..b29f881 --- /dev/null +++ b/.gitea/workflows/gitleaks.yml @@ -0,0 +1,96 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Security +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# PATH: /templates/workflows/gitleaks.yml.template +# VERSION: 01.00.00 +# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens +# +# +========================================================================+ +# | SECRET SCANNING | +# +========================================================================+ +# | | +# | Scans commits for leaked secrets using Gitleaks. | +# | | +# | - PR scan: only new commits in the PR | +# | - Scheduled: full repo scan weekly | +# | - Alerts via ntfy on findings | +# | | +# +========================================================================+ + +name: Secret Scanning + +on: + pull_request: + branches: + - main + - 'dev/**' + schedule: + - cron: '0 5 * * 1' # Weekly Monday 05:00 UTC + workflow_dispatch: + +permissions: + contents: read + +env: + NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }} + NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }} + +jobs: + gitleaks: + name: Gitleaks 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 + gitleaks version + + - name: Scan for secrets + id: scan + run: | + echo "### Secret Scanning" >> $GITHUB_STEP_SUMMARY + ARGS="--source . --verbose --report-format json --report-path /tmp/gitleaks-report.json" + + if [ "${{ github.event_name }}" = "pull_request" ]; then + # Scan only PR commits + ARGS="$ARGS --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}" + echo "Scanning PR commits only" >> $GITHUB_STEP_SUMMARY + else + echo "Full repository scan" >> $GITHUB_STEP_SUMMARY + fi + + if gitleaks detect $ARGS 2>&1; then + echo "result=clean" >> "$GITHUB_OUTPUT" + echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY + else + echo "result=found" >> "$GITHUB_OUTPUT" + FINDINGS=$(jq length /tmp/gitleaks-report.json 2>/dev/null || echo "unknown") + echo "**${FINDINGS} potential secret(s) detected.**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Review the findings and rotate any exposed credentials immediately." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + - name: Notify on findings + if: failure() && steps.scan.outputs.result == 'found' + run: | + REPO="${{ github.event.repository.name }}" + curl -sS \ + -H "Title: ${REPO} — secrets detected in code" \ + -H "Tags: rotating_light,key" \ + -H "Priority: urgent" \ + -d "Gitleaks found potential secrets. Review and rotate credentials immediately." \ + "${NTFY_URL}/${NTFY_TOPIC}" || true -- 2.52.0 From fcc81f181ccf087159e576e67c7374fe054187ea Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Mon, 11 May 2026 21:27:26 +0000 Subject: [PATCH 016/136] chore: sync notify.yml from Template-Joomla [skip ci] --- .gitea/workflows/notify.yml | 71 +++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 .gitea/workflows/notify.yml diff --git a/.gitea/workflows/notify.yml b/.gitea/workflows/notify.yml new file mode 100644 index 0000000..8cc8382 --- /dev/null +++ b/.gitea/workflows/notify.yml @@ -0,0 +1,71 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Notifications +# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards +# PATH: /.gitea/workflows/notify.yml +# VERSION: 01.00.00 +# BRIEF: Push notifications via ntfy on release success or workflow failure + +name: Notifications + +on: + workflow_run: + workflows: + - "Joomla Build & Release" + - "Joomla Extension CI" + - "Deploy" + - "Cascade Main → Dev" + types: + - completed + +permissions: + contents: read + +env: + NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }} + NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-releases' }} + +jobs: + notify: + name: Send Notification + runs-on: ubuntu-latest + if: >- + github.event.workflow_run.conclusion == 'success' || + github.event.workflow_run.conclusion == 'failure' + + steps: + - name: Notify on success (releases only) + if: >- + github.event.workflow_run.conclusion == 'success' && + contains(github.event.workflow_run.name, 'Release') + run: | + REPO="${{ github.event.repository.name }}" + WORKFLOW="${{ github.event.workflow_run.name }}" + URL="${{ github.event.workflow_run.html_url }}" + + curl -sS \ + -H "Title: ${REPO} released" \ + -H "Tags: white_check_mark,package" \ + -H "Priority: default" \ + -H "Click: ${URL}" \ + -d "${WORKFLOW} completed successfully." \ + "${NTFY_URL}/${NTFY_TOPIC}" + + - name: Notify on failure + if: github.event.workflow_run.conclusion == 'failure' + run: | + REPO="${{ github.event.repository.name }}" + WORKFLOW="${{ github.event.workflow_run.name }}" + URL="${{ github.event.workflow_run.html_url }}" + + curl -sS \ + -H "Title: ${REPO} workflow failed" \ + -H "Tags: x,warning" \ + -H "Priority: high" \ + -H "Click: ${URL}" \ + -d "${WORKFLOW} failed. Check the run for details." \ + "${NTFY_URL}/${NTFY_TOPIC}" -- 2.52.0 From fa06b7e1ef71587e15255f67a1f63850fd0a5f7f Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Mon, 11 May 2026 21:27:28 +0000 Subject: [PATCH 017/136] chore: sync pr-branch-check.yml from Template-Joomla [skip ci] --- .gitea/workflows/pr-branch-check.yml | 90 ++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 .gitea/workflows/pr-branch-check.yml diff --git a/.gitea/workflows/pr-branch-check.yml b/.gitea/workflows/pr-branch-check.yml new file mode 100644 index 0000000..b8d9742 --- /dev/null +++ b/.gitea/workflows/pr-branch-check.yml @@ -0,0 +1,90 @@ +# Copyright (C) 2026 Moko Consulting +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Enforces branch merge policy: +# feature/* → dev only +# fix/* → dev only +# hotfix/* → dev or main (emergency) +# dev → main only +# alpha/* → dev only +# beta/* → dev only +# rc/* → main only + +name: Branch Policy Check + +on: + pull_request: + types: [opened, synchronize, reopened, edited] + +jobs: + check-target: + name: Verify merge target + runs-on: ubuntu-latest + steps: + - name: Check branch policy + 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 + ;; + hotfix/*) + if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'" + fi + ;; + alpha/*|beta/*) + if [ "$BASE" != "dev" ]; then + ALLOWED=false + REASON="Pre-release branches must target 'dev', not '${BASE}'" + fi + ;; + rc/*) + if [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Release candidate branches must target '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 "" + 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 -- 2.52.0 From 9ec11d7140b42734dcb2312c0c03a6971111536f Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Mon, 11 May 2026 21:27:29 +0000 Subject: [PATCH 018/136] chore: sync pr-check.yml from Template-Joomla [skip ci] --- .gitea/workflows/pr-check.yml | 106 ++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 .gitea/workflows/pr-check.yml diff --git a/.gitea/workflows/pr-check.yml b/.gitea/workflows/pr-check.yml new file mode 100644 index 0000000..0220500 --- /dev/null +++ b/.gitea/workflows/pr-check.yml @@ -0,0 +1,106 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.CI +# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards +# PATH: /.gitea/workflows/pr-check.yml +# VERSION: 01.00.00 +# BRIEF: PR gate — validates code quality and manifest before merge to main + +name: PR Check + +on: + pull_request: + branches: + - main + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: write + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + validate: + name: Validate PR + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + run: | + if ! command -v php &> /dev/null; then + sudo apt-get update -qq + sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1 + fi + + - name: PHP syntax check + run: | + echo "=== PHP Lint ===" + 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 "Checked files, errors: ${ERRORS}" + [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; } + + - name: Validate Joomla manifest + run: | + echo "=== Manifest Validation ===" + 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" + exit 0 + fi + echo "Manifest: ${MANIFEST}" + + # Check well-formed XML + if ! 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);}"; then + echo "::error::Manifest XML is malformed" + exit 1 + fi + + # Check required elements + for ELEMENT in name version description; do + if ! grep -q "<${ELEMENT}>" "$MANIFEST"; then + echo "::error::Missing <${ELEMENT}> in manifest" + exit 1 + fi + done + echo "Manifest valid" + + - name: Check updates.xml format + run: | + if [ ! -f "updates.xml" ]; then + echo "No updates.xml — skipping" + exit 0 + fi + echo "=== updates.xml Validation ===" + if ! 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);}"; then + echo "::error::updates.xml is malformed" + exit 1 + fi + echo "updates.xml valid" + + - name: Verify package builds + run: | + echo "=== Package Build Test ===" + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + if [ ! -d "$SOURCE_DIR" ]; then + echo "::warning::No src/ or htdocs/ directory" + exit 0 + fi + # Dry-run: ensure zip would succeed + FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l) + echo "Source contains ${FILE_COUNT} files — package will build" + [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; } -- 2.52.0 From f446f916551d19d0c7a39930e43cf51ab6b2c299 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Mon, 11 May 2026 21:27:31 +0000 Subject: [PATCH 019/136] chore: sync pre-release.yml from Template-Joomla [skip ci] --- .gitea/workflows/pre-release.yml | 341 +++++++++++++++++++++++++++++++ 1 file changed, 341 insertions(+) create mode 100644 .gitea/workflows/pre-release.yml diff --git a/.gitea/workflows/pre-release.yml b/.gitea/workflows/pre-release.yml new file mode 100644 index 0000000..30c9bcf --- /dev/null +++ b/.gitea/workflows/pre-release.yml @@ -0,0 +1,341 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Release +# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards +# PATH: /.gitea/workflows/pre-release.yml +# VERSION: 01.00.00 +# BRIEF: Manual pre-release — builds dev/alpha/beta/rc packages from any branch + +name: Pre-Release + +on: + workflow_dispatch: + inputs: + stability: + description: 'Pre-release channel' + required: true + type: choice + options: + - development + - alpha + - beta + - release-candidate + +permissions: + contents: write + +env: + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +jobs: + build: + name: "Build Pre-Release (${{ inputs.stability }})" + runs-on: release + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GA_TOKEN }} + + - name: Setup PHP + run: | + if ! command -v php &> /dev/null; then + sudo apt-get update -qq + sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip >/dev/null 2>&1 + fi + + - name: Resolve metadata + id: meta + run: | + STABILITY="${{ inputs.stability }}" + + case "$STABILITY" in + development) SUFFIX="-dev"; TAG="development" ;; + alpha) SUFFIX="-alpha"; TAG="alpha" ;; + beta) SUFFIX="-beta"; TAG="beta" ;; + release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; + esac + + # Read and bump patch version (with rollover) + CURRENT=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1) + [ -z "$CURRENT" ] && CURRENT="00.00.00" + + MAJOR=$(echo "$CURRENT" | cut -d. -f1) + MINOR=$(echo "$CURRENT" | cut -d. -f2) + PATCH=$(echo "$CURRENT" | cut -d. -f3) + + # Patch bump with rollover: ZZ=99 → bump minor, YY=99 → bump major + NEW_PATCH=$((10#$PATCH + 1)) + NEW_MINOR=$((10#$MINOR)) + NEW_MAJOR=$((10#$MAJOR)) + + if [ $NEW_PATCH -gt 99 ]; then + NEW_PATCH=0 + NEW_MINOR=$((NEW_MINOR + 1)) + fi + if [ $NEW_MINOR -gt 99 ]; then + NEW_MINOR=0 + NEW_MAJOR=$((NEW_MAJOR + 1)) + fi + + VERSION=$(printf "%02d.%02d.%02d" $NEW_MAJOR $NEW_MINOR $NEW_PATCH) + TODAY=$(date +%Y-%m-%d) + + echo "Bumping: ${CURRENT} → ${VERSION} (patch)" + + # Update README.md + sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${VERSION}/" README.md + + # Update manifest + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) + if [ -n "$MANIFEST" ]; then + MANIFEST_VER=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1) + sed -i "s|${MANIFEST_VER}|${VERSION}|" "$MANIFEST" + sed -i "s|[^<]*|${TODAY}|" "$MANIFEST" + fi + + # Commit version bump + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git add -A + git diff --cached --quiet || { + git commit -m "chore(version): bump ${CURRENT} → ${VERSION} [skip ci]" + git push origin HEAD 2>&1 + } + + # Auto-detect element from manifest + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) + EXT_ELEMENT="" + if [ -n "$MANIFEST" ]; then + EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1) + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') + case "$EXT_ELEMENT" in + templatedetails|manifest) EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;; + esac + fi + else + EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + fi + + ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip" + + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" + echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" + echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" + echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT" + + echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ===" + + - name: Build package + run: | + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + if [ ! -d "$SOURCE_DIR" ]; then + echo "::error::No src/ or htdocs/ directory" + exit 1 + fi + + mkdir -p build/package + rsync -a \ + --exclude='sftp-config*' \ + --exclude='.ftpignore' \ + --exclude='*.ppk' \ + --exclude='*.pem' \ + --exclude='*.key' \ + --exclude='.env*' \ + --exclude='*.local' \ + --exclude='.build-trigger' \ + "${SOURCE_DIR}/" build/package/ + + - name: Create ZIP + id: zip + run: | + ZIP_NAME="${{ steps.meta.outputs.zip_name }}" + cd build/package + zip -r "../${ZIP_NAME}" . + cd .. + + SHA256=$(sha256sum "${ZIP_NAME}" | cut -d' ' -f1) + echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT" + echo "ZIP: ${ZIP_NAME} (SHA: ${SHA256:0:16}...)" + + - name: Create or replace Gitea release + id: release + run: | + TAG="${{ steps.meta.outputs.tag }}" + VERSION="${{ steps.meta.outputs.version }}" + STABILITY="${{ steps.meta.outputs.stability }}" + SHA256="${{ steps.zip.outputs.sha256 }}" + ZIP_NAME="${{ steps.meta.outputs.zip_name }}" + EXT_ELEMENT="${{ steps.meta.outputs.ext_element }}" + TOKEN="${{ secrets.GA_TOKEN }}" + API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + BRANCH=$(git branch --show-current) + + BODY="## ${VERSION} ($(date +%Y-%m-%d)) + **Channel:** ${STABILITY} + **SHA-256:** \`${SHA256}\`" + + # Delete existing release + EXISTING_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \ + "${API}/releases/tags/${TAG}" | jq -r '.id // empty' 2>/dev/null) + if [ -n "$EXISTING_ID" ]; then + curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API}/releases/${EXISTING_ID}" 2>/dev/null || true + curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API}/tags/${TAG}" 2>/dev/null || true + fi + + # Create release + RELEASE_ID=$(curl -sS -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/releases" \ + -d "$(jq -n \ + --arg tag "$TAG" \ + --arg target "$BRANCH" \ + --arg name "${EXT_ELEMENT} ${VERSION} (${STABILITY})" \ + --arg body "$BODY" \ + '{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: true}' + )" | jq -r '.id') + + echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT" + + # Upload ZIP + curl -sS -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/octet-stream" \ + "${API}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \ + --data-binary "@build/${ZIP_NAME}" + + echo "Released: ${EXT_ELEMENT} ${VERSION} (${STABILITY})" + + - name: Update updates.xml + run: | + STABILITY="${{ steps.meta.outputs.stability }}" + VERSION="${{ steps.meta.outputs.version }}" + SHA256="${{ steps.zip.outputs.sha256 }}" + ZIP_NAME="${{ steps.meta.outputs.zip_name }}" + TAG="${{ steps.meta.outputs.tag }}" + DATE=$(date +%Y-%m-%d) + + if [ ! -f "updates.xml" ]; then + echo "No updates.xml — skipping" + exit 0 + fi + + export PY_STABILITY="$STABILITY" PY_VERSION="$VERSION" PY_SHA256="$SHA256" \ + PY_ZIP_NAME="$ZIP_NAME" PY_TAG="$TAG" PY_DATE="$DATE" \ + PY_GITEA_ORG="$GITEA_ORG" PY_GITEA_REPO="$GITEA_REPO" + python3 << 'PYEOF' + import re, os + + stability = os.environ["PY_STABILITY"] + version = os.environ["PY_VERSION"] + sha256 = os.environ["PY_SHA256"] + zip_name = os.environ["PY_ZIP_NAME"] + tag = os.environ["PY_TAG"] + date = os.environ["PY_DATE"] + gitea_org = os.environ["PY_GITEA_ORG"] + gitea_repo = os.environ["PY_GITEA_REPO"] + download_url = f"https://git.mokoconsulting.tech/{gitea_org}/{gitea_repo}/releases/download/{tag}/{zip_name}" + + with open("updates.xml", "r") as f: + content = f.read() + + # Map stability to XML tag name + tag_map = {"development": "development", "alpha": "alpha", "beta": "beta", "release-candidate": "rc"} + xml_tag = tag_map.get(stability, stability) + + pattern = r"((?:(?!).)*?" + re.escape(xml_tag) + r".*?
)" + match = re.search(pattern, content, re.DOTALL) + if match: + block = match.group(1) + updated = re.sub(r"[^<]*", f"{version}", block) + updated = re.sub(r"[^<]*", f"{date}", updated) + if "" in updated: + updated = re.sub(r"[^<]*", f"{sha256}", updated) + else: + updated = updated.replace("", f"\n {sha256}") + updated = re.sub(r"(]*>)[^<]*()", rf"\g<1>{download_url}\g<2>", updated) + content = content.replace(block, updated) + print(f"Updated {xml_tag} channel: version={version}") + else: + print(f"WARNING: No {xml_tag} block in updates.xml") + + with open("updates.xml", "w") as f: + f.write(content) + PYEOF + + # Commit and push to current branch + if ! git diff --quiet updates.xml 2>/dev/null; then + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git add updates.xml + git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]" + git push origin HEAD 2>&1 || echo "WARNING: push failed" + fi + + - name: "Sync updates.xml to all branches" + run: | + CURRENT_BRANCH="${{ github.ref_name }}" + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + + # Sync updates.xml to main and dev (whichever isn't current) + for BRANCH in main dev; do + [ "$BRANCH" = "$CURRENT_BRANCH" ] && continue + + echo "Syncing updates.xml → ${BRANCH}" + git fetch origin "${BRANCH}" 2>/dev/null || continue + git checkout "origin/${BRANCH}" -- . 2>/dev/null || continue + git checkout "${CURRENT_BRANCH}" -- updates.xml + if ! git diff --quiet updates.xml 2>/dev/null; then + git add updates.xml + git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]" + git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed" + fi + git checkout "${CURRENT_BRANCH}" 2>/dev/null + done + + - name: "Delete lesser pre-release channels (cascade)" + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.GA_TOKEN }}" + STABILITY="${{ steps.meta.outputs.stability }}" + + # Cascade: rc → beta,alpha,dev | beta → alpha,dev | alpha → dev | dev → nothing + case "$STABILITY" in + release-candidate) TAGS_TO_DELETE="beta alpha development" ;; + beta) TAGS_TO_DELETE="alpha development" ;; + alpha) TAGS_TO_DELETE="development" ;; + *) TAGS_TO_DELETE="" ;; + esac + + [ -z "$TAGS_TO_DELETE" ] && exit 0 + + for TAG in $TAGS_TO_DELETE; do + RELEASE_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/releases/tags/${TAG}" 2>/dev/null | \ + python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + + if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then + curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/releases/${RELEASE_ID}" 2>/dev/null || true + curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/tags/${TAG}" 2>/dev/null || true + echo "Deleted: ${TAG} (id: ${RELEASE_ID})" + fi + done -- 2.52.0 From 49ef70ba602b8dbb09e1fe6fd77d19e4098389f1 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Mon, 11 May 2026 21:27:32 +0000 Subject: [PATCH 020/136] chore: sync repo-health.yml from Template-Joomla [skip ci] --- .gitea/workflows/repo-health.yml | 766 +++++++++++++++++++++++++++++++ 1 file changed, 766 insertions(+) create mode 100644 .gitea/workflows/repo-health.yml diff --git a/.gitea/workflows/repo-health.yml b/.gitea/workflows/repo-health.yml new file mode 100644 index 0000000..57b11ef --- /dev/null +++ b/.gitea/workflows/repo-health.yml @@ -0,0 +1,766 @@ +# ============================================================================ +# 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: MokoStandards.Validation +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# PATH: /templates/workflows/joomla/repo_health.yml.template +# VERSION: 04.06.00 +# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts. +# ============================================================================ + +name: Repo Health + +concurrency: + group: repo-health-${{ github.repository }}-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +on: + workflow_dispatch: + inputs: + profile: + description: 'Validation profile: all, release, scripts, or repo' + required: true + default: all + type: choice + options: + - all + - release + - scripts + - repo + pull_request: + push: + +permissions: + contents: read + +env: + # Release policy - Repository Variables Only + RELEASE_REQUIRED_REPO_VARS: RS_FTP_PATH_SUFFIX + RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX + + # 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,.gitea/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: .gitea/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.GA_TOKEN || secrets.GA_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 + + release_config: + name: Release configuration + 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: Guardrails release vars + env: + PROFILE_RAW: ${{ github.event.inputs.profile }} + RS_FTP_PATH_SUFFIX: ${{ vars.RS_FTP_PATH_SUFFIX }} + DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }} + run: | + set -euo pipefail + + profile="${PROFILE_RAW:-all}" + case "${profile}" in + all|release|scripts|repo) ;; + *) + printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" + exit 1 + ;; + esac + + if [ "${profile}" = 'scripts' ] || [ "${profile}" = 'repo' ]; then + { + printf '%s\n' '### Release configuration (Repository Variables)' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' 'Status: SKIPPED' + printf '%s\n' 'Reason: profile excludes release validation' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 0 + fi + + IFS=',' read -r -a required <<< "${RELEASE_REQUIRED_REPO_VARS}" + IFS=',' read -r -a optional <<< "${RELEASE_OPTIONAL_REPO_VARS}" + + missing=() + missing_optional=() + + for k in "${required[@]}"; do + v="${!k:-}" + [ -z "${v}" ] && missing+=("${k}") + done + + for k in "${optional[@]}"; do + v="${!k:-}" + [ -z "${v}" ] && missing_optional+=("${k}") + done + + { + printf '%s\n' '### Release configuration (Repository Variables)' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' '| Variable | Status |' + printf '%s\n' '|---|---|' + printf '%s\n' "| RS_FTP_PATH_SUFFIX | ${RS_FTP_PATH_SUFFIX:-NOT SET} |" + printf '%s\n' "| DEV_FTP_SUFFIX | ${DEV_FTP_SUFFIX:-NOT SET} |" + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + if [ "${#missing_optional[@]}" -gt 0 ]; then + { + printf '%s\n' '### Missing optional repository variables' + for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + if [ "${#missing[@]}" -gt 0 ]; then + { + printf '%s\n' '### Missing required repository variables' + for m in "${missing[@]}"; do printf '%s\n' "- ${m}"; done + printf '%s\n' 'ERROR: Guardrails failed. Missing required repository variables.' + } >> "${GITHUB_STEP_SUMMARY}" + exit 1 + fi + + { + printf '%s\n' '### Repository variables validation result' + printf '%s\n' 'Status: OK' + printf '%s\n' 'All required repository variables present.' + printf '%s\n' '' + printf '%s\n' '**Note**: Organization secrets (RS_FTP_HOST, RS_FTP_USER, etc.) are validated at deployment time, not in repository health checks.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + 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|release|scripts|repo) ;; + *) + printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" + exit 1 + ;; + esac + + if [ "${profile}" = 'release' ] || [ "${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 + + IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}" + 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|release|scripts|repo) ;; + *) + printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" + exit 1 + ;; + esac + + if [ "${profile}" = 'release' ] || [ "${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 + + # Source directory: src/ or htdocs/ (either is valid) + if [ -d "src" ]; then + SOURCE_DIR="src" + elif [ -d "htdocs" ]; then + SOURCE_DIR="htdocs" + else + missing_required+=("src/ or htdocs/ (source directory required)") + fi + + IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}" + IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}" + IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}" + IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES}" + + missing_required=() + missing_optional=() + + 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 ]; then + missing_required+=("dev/* branch (e.g. dev/01.00.00)") + fi + + if [ "${#dev_branches[@]}" -gt 0 ]; then + missing_required+=("invalid branch dev (must be dev/)") + 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="$(python3 - <<'PY' + import json + import os + + profile = os.environ.get('PROFILE_RAW') or 'all' + + missing_required = os.environ.get('MISSING_REQUIRED', '').splitlines() if os.environ.get('MISSING_REQUIRED') else [] + missing_optional = os.environ.get('MISSING_OPTIONAL', '').splitlines() if os.environ.get('MISSING_OPTIONAL') else [] + content_warnings = os.environ.get('CONTENT_WARNINGS', '').splitlines() if os.environ.get('CONTENT_WARNINGS') else [] + + out = { + 'profile': profile, + 'missing_required': [x for x in missing_required if x], + 'missing_optional': [x for x in missing_optional if x], + 'content_warnings': [x for x in content_warnings if x], + } + + print(json.dumps(out, indent=2)) + PY + )" + + { + 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 + + 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 + + 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="$(python3 - <<'PY' + import os + import re + + idx = os.environ.get('DOCS_INDEX', 'docs/docs-index.md') + base = os.getcwd() + + bad = [] + pat = re.compile(r'\[[^\]]+\]\(([^)]+)\)') + + with open(idx, 'r', encoding='utf-8') as f: + for line in f: + for m in pat.findall(line): + link = m.strip() + if link.startswith('http://') or link.startswith('https://') or link.startswith('#') or link.startswith('mailto:'): + continue + if link.startswith('/'): + rel = link.lstrip('/') + else: + rel = os.path.normpath(os.path.join(os.path.dirname(idx), link)) + rel = rel.split('#', 1)[0] + rel = rel.split('?', 1)[0] + if not rel: + continue + p = os.path.join(base, rel) + if not os.path.exists(p): + bad.append(rel) + + print('\n'.join(sorted(set(bad)))) + PY + )" + 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:' + while IFS= read -r l; do [ -n "${l}" ] && printf '%s\n' "- ${l}"; done <<< "${missing_links}" + 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 variables | OK | Repository variables validation |' + 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}" -- 2.52.0 From 226096e6e313a732100bb5f11be00d2daf4278f5 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Mon, 11 May 2026 21:27:34 +0000 Subject: [PATCH 021/136] chore: sync security-audit.yml from Template-Joomla [skip ci] --- .gitea/workflows/security-audit.yml | 82 +++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 .gitea/workflows/security-audit.yml diff --git a/.gitea/workflows/security-audit.yml b/.gitea/workflows/security-audit.yml new file mode 100644 index 0000000..ff6de4c --- /dev/null +++ b/.gitea/workflows/security-audit.yml @@ -0,0 +1,82 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Security +# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards +# PATH: /.gitea/workflows/security-audit.yml +# VERSION: 01.00.00 +# BRIEF: Dependency vulnerability scanning for composer and npm packages + +name: Security Audit + +on: + schedule: + - cron: '0 6 * * 1' # Weekly on Monday at 06:00 UTC + pull_request: + branches: + - main + paths: + - 'composer.json' + - 'composer.lock' + - 'package.json' + - 'package-lock.json' + workflow_dispatch: + +permissions: + contents: read + +env: + NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }} + NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }} + +jobs: + audit: + name: Dependency Audit + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Composer audit + if: hashFiles('composer.lock') != '' + run: | + echo "=== Composer Security Audit ===" + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq + sudo apt-get install -y -qq php-cli composer >/dev/null 2>&1 + fi + composer audit --format=plain 2>&1 | tee /tmp/composer-audit.txt + RESULT=$? + if [ $RESULT -ne 0 ]; then + echo "::warning::Composer vulnerabilities found" + echo "composer_vulnerable=true" >> "$GITHUB_ENV" + else + echo "No known vulnerabilities in composer dependencies" + fi + + - name: NPM audit + if: hashFiles('package-lock.json') != '' + run: | + echo "=== NPM Security Audit ===" + npm audit --production 2>&1 | tee /tmp/npm-audit.txt || true + if npm audit --production 2>&1 | grep -q "found 0 vulnerabilities"; then + echo "No known vulnerabilities in npm dependencies" + else + echo "::warning::NPM vulnerabilities found" + echo "npm_vulnerable=true" >> "$GITHUB_ENV" + fi + + - name: Notify on vulnerabilities + if: env.composer_vulnerable == 'true' || env.npm_vulnerable == 'true' + run: | + REPO="${{ github.event.repository.name }}" + curl -sS \ + -H "Title: ${REPO} has vulnerable dependencies" \ + -H "Tags: lock,warning" \ + -H "Priority: high" \ + -d "Security audit found vulnerabilities. Review dependency updates." \ + "${NTFY_URL}/${NTFY_TOPIC}" || true -- 2.52.0 From 48d4dbe10f3ccb8cfbe134d9c314058b604b8d88 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:23 +0000 Subject: [PATCH 022/136] chore: move .gitea/workflows/auto-release.yml to .mokogitea/auto-release.yml [skip ci] --- .mokogitea/auto-release.yml | 949 ++++++++++++++++++++++++++++++++++++ 1 file changed, 949 insertions(+) create mode 100644 .mokogitea/auto-release.yml diff --git a/.mokogitea/auto-release.yml b/.mokogitea/auto-release.yml new file mode 100644 index 0000000..279bc5e --- /dev/null +++ b/.mokogitea/auto-release.yml @@ -0,0 +1,949 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Release +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# PATH: /templates/workflows/joomla/auto-release.yml.template +# VERSION: 04.06.00 +# BRIEF: Joomla build & release — ZIP package, updates.xml, SHA-256 checksum +# +# +========================================================================+ +# | BUILD & RELEASE PIPELINE (JOOMLA) | +# +========================================================================+ +# | | +# | Triggers on push to main (skips bot commits + [skip ci]): | +# | | +# | Every push: | +# | 1. Read version from README.md | +# | 3. Set platform version (Joomla ) | +# | 4. Update [VERSION: XX.YY.ZZ] badges in markdown files | +# | 5. Write updates.xml (Joomla update server XML) | +# | 6. Create git tag vXX.YY.ZZ | +# | 7a. Patch: update existing Gitea Release for this minor | +# | 8. Build ZIP, upload asset, write SHA-256 to updates.xml | +# | | +# | Every version change: archives main -> version/XX.YY branch | +# | All patches release (including 00). Patch 00/01 = full pipeline. | +# | First release only (patch == 01): | +# | 7b. Create new Gitea Release | +# | | +# | GitHub mirror: stable/rc releases only (continue-on-error) | +# | | +# +========================================================================+ + +name: Build & Release + +on: + pull_request: + types: [closed] + branches: + - main + paths: + - 'src/**' + - 'htdocs/**' + workflow_dispatch: + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +permissions: + contents: write + +jobs: + release: + name: Build & Release Pipeline + runs-on: release + if: >- + github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.GA_TOKEN }} + fetch-depth: 0 + + - name: Setup MokoStandards tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN }}"}}' + run: | + # Ensure PHP + Composer are available + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \ + /tmp/mokostandards-api + cd /tmp/mokostandards-api + composer install --no-dev --no-interaction --quiet + + # -- STEP 1: Read version ----------------------------------------------- + - name: "Step 1: Read version from README.md" + id: version + run: | + VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null) + if [ -z "$VERSION" ]; then + echo "No VERSION in README.md — skipping release" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + # Derive major.minor for branch naming (patches update existing branch) + MINOR=$(echo "$VERSION" | awk -F. '{printf "%s.%s", $1, $2}') + PATCH=$(echo "$VERSION" | awk -F. '{print $3}') + + MAJOR=$(echo "$VERSION" | awk -F. '{print $1}') + MINOR_NUM=$(echo "$VERSION" | awk -F. '{print $2}') + + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT" + echo "minor=$MINOR" >> "$GITHUB_OUTPUT" + echo "major=$MAJOR" >> "$GITHUB_OUTPUT" + echo "release_tag=stable" >> "$GITHUB_OUTPUT" + echo "stability=stable" >> "$GITHUB_OUTPUT" + echo "skip=false" >> "$GITHUB_OUTPUT" + if [ "$PATCH" = "00" ] || [ "$PATCH" = "01" ]; then + echo "is_minor=true" >> "$GITHUB_OUTPUT" + echo "Version: $VERSION (first release for this minor — full pipeline)" + else + echo "is_minor=false" >> "$GITHUB_OUTPUT" + echo "Version: $VERSION (patch — platform version + badges only)" + fi + + # -- STEP 1b: Bump minor version (stable = minor bump, reset patch) ------ + - name: "Step 1b: Bump minor version for stable release" + if: steps.version.outputs.skip != 'true' + id: bump + run: | + CURRENT=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1) + [ -z "$CURRENT" ] && { echo "skip=true" >> "$GITHUB_OUTPUT"; exit 0; } + + MAJOR=$((10#$(echo "$CURRENT" | cut -d. -f1))) + MINOR=$((10#$(echo "$CURRENT" | cut -d. -f2))) + + # Minor bump, reset patch. Rollover if minor > 99 + MINOR=$((MINOR + 1)) + if [ $MINOR -gt 99 ]; then + MINOR=0 + MAJOR=$((MAJOR + 1)) + fi + + VERSION=$(printf "%02d.%02d.00" $MAJOR $MINOR) + TODAY=$(date +%Y-%m-%d) + + echo "Stable bump: ${CURRENT} → ${VERSION} (minor)" + + # Update README.md + sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${VERSION}/" README.md + + # Update manifest + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) + if [ -n "$MANIFEST" ]; then + MANIFEST_VER=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1) + [ -n "$MANIFEST_VER" ] && sed -i "s|${MANIFEST_VER}|${VERSION}|" "$MANIFEST" + sed -i "s|[^<]*|${TODAY}|" "$MANIFEST" + fi + + # Promote [Unreleased] section in CHANGELOG.md to new version + if [ -f "CHANGELOG.md" ] && grep -qi "Unreleased" CHANGELOG.md; then + sed -i "s|## \[Unreleased\]|## [${VERSION}] --- ${TODAY}|" CHANGELOG.md + sed -i "s|## Unreleased|## [${VERSION}] --- ${TODAY}|" CHANGELOG.md + sed -i "2i ## [Unreleased]" CHANGELOG.md + sed -i "3i \\ " CHANGELOG.md + echo "CHANGELOG promoted to [${VERSION}]" + fi + + # Commit and push + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git add -A + git diff --cached --quiet || { + git commit -m "chore(version): bump ${CURRENT} → ${VERSION} [skip ci]" + git push origin HEAD:main 2>&1 + } + + # Override version output for rest of pipeline + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "major=$(printf "%02d" $MAJOR)" >> "$GITHUB_OUTPUT" + + - name: Check if already released + if: steps.version.outputs.skip != 'true' + id: check + run: | + TAG="${{ steps.version.outputs.release_tag }}" + BRANCH="${{ steps.version.outputs.branch }}" + + TAG_EXISTS=false + BRANCH_EXISTS=false + + git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true + git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH" && BRANCH_EXISTS=true + + echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT" + echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT" + + # Tag and branch may persist across patch releases — never skip + echo "already_released=false" >> "$GITHUB_OUTPUT" + + # -- SANITY CHECKS ------------------------------------------------------- + - name: "Sanity: Pre-release validation" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + ERRORS=0 + + echo "## Pre-Release Sanity Checks (Joomla)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # -- Version drift check (must pass before release) -------- + README_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1) + if [ "$README_VER" != "$VERSION" ]; then + echo "- Version drift: README says \`${README_VER}\` but releasing \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + else + echo "- Version consistent: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + + # Check CHANGELOG version matches + CL_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' CHANGELOG.md 2>/dev/null | head -1) + if [ -n "$CL_VER" ] && [ "$CL_VER" != "$VERSION" ]; then + echo "- CHANGELOG drift: \`${CL_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + fi + + # Check composer.json version if present + if [ -f "composer.json" ]; then + COMP_VER=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' composer.json 2>/dev/null | head -1) + if [ -n "$COMP_VER" ] && [ "$COMP_VER" != "$VERSION" ]; then + echo "- composer.json drift: \`${COMP_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + fi + fi + + # Common checks + if [ ! -f "LICENSE" ]; then + echo "- Missing LICENSE file" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + else + echo "- LICENSE present" >> $GITHUB_STEP_SUMMARY + fi + + if [ ! -d "src" ] && [ ! -d "htdocs" ]; then + echo "- Warning: No src/ or htdocs/ directory" >> $GITHUB_STEP_SUMMARY + else + echo "- Source directory present" >> $GITHUB_STEP_SUMMARY + fi + + # -- Joomla: manifest version drift -------- + MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1) + if [ -n "$MANIFEST" ]; then + XML_VER=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1) + if [ -n "$XML_VER" ] && [ "$XML_VER" != "$VERSION" ]; then + echo "- Manifest drift: \`${XML_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + else + echo "- Manifest version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + fi + + # -- Joomla: XML manifest existence -------- + if [ -z "$MANIFEST" ]; then + echo "- No Joomla XML manifest found" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + else + echo "- Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY + + # -- Joomla: extension type check -------- + TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null) + echo "- Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "$ERRORS" -gt 0 ]; then + echo "**${ERRORS} error(s) — release may be incomplete**" >> $GITHUB_STEP_SUMMARY + else + echo "**All sanity checks passed**" >> $GITHUB_STEP_SUMMARY + fi + + # -- STEP 2: Create or update version/XX.YY archive branch --------------- + # Always runs — every version change on main archives to version/XX.YY + - name: "Step 2: Version archive branch" + if: steps.check.outputs.already_released != 'true' + run: | + BRANCH="${{ steps.version.outputs.branch }}" + IS_MINOR="${{ steps.version.outputs.is_minor }}" + PATCH="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}') + + # Check if branch exists + if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then + git push origin HEAD:"$BRANCH" --force + echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY + else + git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH" + git push origin "$BRANCH" --force + echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY + fi + + # -- STEP 3: Set platform version ---------------------------------------- + - name: "Step 3: Set platform version" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + php /tmp/mokostandards-api/cli/version_set_platform.php \ + --path . --version "$VERSION" --branch main + + # -- STEP 4: Update version badges ---------------------------------------- + - name: "Step 4: Update version badges" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + find . -name "*.md" ! -path "./.git/*" ! -path "./vendor/*" | while read -r f; do + if grep -q '\[VERSION:' "$f" 2>/dev/null; then + sed -i "s/\[VERSION:[[:space:]]*[0-9]\{2\}\.[0-9]\{2\}\.[0-9]\{2\}\]/[VERSION: ${VERSION}]/" "$f" + fi + done + + # -- STEP 5: Write updates.xml (Joomla update server) --------------------- + - name: "Step 5: Write updates.xml" + id: updates + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + REPO="${{ github.repository }}" + + # -- Parse extension metadata from XML manifest ---------------- + MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1) + if [ -z "$MANIFEST" ]; then + echo "Warning: No Joomla XML manifest found — skipping updates.xml" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + # Extract fields using sed (portable — no grep -P) + EXT_NAME=$(sed -n 's/.*\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1) + EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1) + EXT_CLIENT=$(sed -n 's/.*]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_FOLDER=$(sed -n 's/.*]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + TARGET_PLATFORM=$(sed -n 's/.*\(\).*/\1/p' "$MANIFEST" | head -1) + PHP_MINIMUM=$(sed -n 's/.*\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1) + + # If EXT_NAME is a language key (e.g. PLG_SYSTEM_MOKOJGDPC), resolve from .ini + if echo "$EXT_NAME" | grep -qE '^[A-Z_]+$'; then + INI_NAME=$(find . -name "*.sys.ini" -path "*/en-GB/*" -exec grep -h "^${EXT_NAME}=" {} \; 2>/dev/null | head -1 | cut -d'"' -f2) + [ -z "$INI_NAME" ] && INI_NAME=$(find . -name "*.sys.ini" -exec grep -h "^${EXT_NAME}=" {} \; 2>/dev/null | head -1 | cut -d'"' -f2) + [ -n "$INI_NAME" ] && EXT_NAME="$INI_NAME" + fi + + # Fallbacks + [ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}" + [ -z "$EXT_TYPE" ] && EXT_TYPE="component" + + # Derive element if not in manifest: + # 1. plugin="xxx" attribute (plugins) + # 2. module="xxx" attribute (modules) + # 3. XML filename (components, packages) + # 4. Repo name fallback (templates, anything else) + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(sed -n 's/.*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + fi + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(sed -n 's/.*module="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + fi + if [ -z "$EXT_ELEMENT" ]; then + FNAME=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') + # If filename is generic (templateDetails, manifest), use repo name + case "$FNAME" in + templatedetails|manifest) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;; + *) EXT_ELEMENT="$FNAME" ;; + esac + fi + # Final fallback + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + + # Save for Steps 7, 8, 8b + echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" + echo "ext_name=${EXT_NAME}" >> "$GITHUB_OUTPUT" + echo "ext_type=${EXT_TYPE}" >> "$GITHUB_OUTPUT" + echo "ext_folder=${EXT_FOLDER}" >> "$GITHUB_OUTPUT" + + # Build client tag: plugins and frontend modules need site + CLIENT_TAG="" + if [ -n "$EXT_CLIENT" ]; then + CLIENT_TAG="${EXT_CLIENT}" + elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then + CLIENT_TAG="site" + fi + + # Build folder tag for plugins (required for Joomla to match the update) + FOLDER_TAG="" + if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then + FOLDER_TAG="${EXT_FOLDER}" + fi + + # Build targetplatform (fallback to Joomla 5 if not in manifest) + if [ -z "$TARGET_PLATFORM" ]; then + TARGET_PLATFORM=$(printf '' "/") + fi + + # Build php_minimum tag + PHP_TAG="" + if [ -n "$PHP_MINIMUM" ]; then + PHP_TAG="${PHP_MINIMUM}" + fi + + # Build TYPE_PREFIX for download URL + TYPE_PREFIX="" + case "${EXT_TYPE}" in + plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; + module) TYPE_PREFIX="mod_" ;; + component) TYPE_PREFIX="com_" ;; + template) TYPE_PREFIX="tpl_" ;; + library) TYPE_PREFIX="lib_" ;; + package) TYPE_PREFIX="pkg_" ;; + esac + + DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/stable/${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip" + INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/stable" + + # -- Build update entry for a given stability tag + build_entry() { + local TAG_NAME="$1" + printf '%s\n' ' ' + printf '%s\n' " ${EXT_NAME}" + printf '%s\n' " ${EXT_NAME} update" + printf '%s\n' " ${EXT_ELEMENT}" + printf '%s\n' " ${EXT_TYPE}" + printf '%s\n' " ${VERSION}" + [ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}" + [ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}" + printf '%s\n' " ${TAG_NAME}" + printf '%s\n' " ${INFO_URL}" + printf '%s\n' ' ' + printf '%s\n' " ${DOWNLOAD_URL}" + printf '%s\n' ' ' + printf '%s\n' " ${TARGET_PLATFORM}" + [ -n "$PHP_TAG" ] && printf '%s\n' " ${PHP_TAG}" + printf '%s\n' ' Moko Consulting' + printf '%s\n' ' https://mokoconsulting.tech' + printf '%s\n' ' ' + } + + # -- Write updates.xml with cascading channels + # Stable release updates ALL channels (development, alpha, beta, rc, stable) + { + printf '%s\n' "" + printf '%s\n' "" + printf '%s\n' "" + printf '%s\n' '' + build_entry "development" + build_entry "alpha" + build_entry "beta" + build_entry "rc" + build_entry "stable" + printf '%s\n' '' + } > updates.xml + + echo "updates.xml: ${VERSION} (all channels updated to stable)" >> $GITHUB_STEP_SUMMARY + + # -- Commit all changes --------------------------------------------------- + - name: Commit release changes + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + if git diff --quiet && git diff --cached --quiet; then + echo "No changes to commit" + exit 0 + fi + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + # Set push URL with token for branch-protected repos + git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git add -A + git commit -m "chore(release): build ${VERSION} [skip ci]" \ + --author="gitea-actions[bot] " + git push -u origin HEAD + + # -- STEP 6: Create tag --------------------------------------------------- + - name: "Step 6: Create git tag" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.tag_exists != 'true' && + steps.version.outputs.is_minor == 'true' + run: | + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + # Only create the major release tag if it doesn't exist yet + if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then + git tag "$RELEASE_TAG" + git push origin "$RELEASE_TAG" + echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY + else + echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY + fi + echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY + + # -- STEP 7: Create or update Gitea Release -------------------------------- + - name: "Step 7: Gitea Release" + if: >- + steps.version.outputs.skip != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + BRANCH="${{ steps.version.outputs.branch }}" + MAJOR="${{ steps.version.outputs.major }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + # Reuse metadata from Step 5 (single source of truth) + EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}" + EXT_NAME="${{ steps.updates.outputs.ext_name }}" + EXT_TYPE="${{ steps.updates.outputs.ext_type }}" + EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}" + + # Fallbacks if Step 5 was skipped + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + fi + [ -z "$EXT_NAME" ] && EXT_NAME="${GITEA_REPO}" + + NOTES=$(php /tmp/mokostandards-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null) + [ -z "$NOTES" ] && NOTES="Release ${VERSION}" + + # Build release name: "Pretty Name VERSION (type_element-VERSION)" + TYPE_PREFIX="" + case "${EXT_TYPE}" in + plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; + module) TYPE_PREFIX="mod_" ;; + component) TYPE_PREFIX="com_" ;; + template) TYPE_PREFIX="tpl_" ;; + library) TYPE_PREFIX="lib_" ;; + package) TYPE_PREFIX="pkg_" ;; + esac + RELEASE_NAME="${EXT_NAME} ${VERSION} (${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION})" + + # Delete existing release if present (overwrite, not append) + EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true) + EXISTING_ID=$(echo "$EXISTING" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true) + + if [ -n "$EXISTING_ID" ]; then + curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/${EXISTING_ID}" 2>/dev/null || true + curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/tags/${RELEASE_TAG}" 2>/dev/null || true + echo "Deleted previous stable release (id: ${EXISTING_ID})" + fi + + # Create fresh release + curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/releases" \ + -d "$(python3 -c "import json; print(json.dumps({ + 'tag_name': '${RELEASE_TAG}', + 'name': '${RELEASE_NAME}', + 'body': '''## ${VERSION} ($(date +%Y-%m-%d))\n${NOTES}''', + 'target_commitish': '${BRANCH}' + }))")" + echo "Release created: ${RELEASE_NAME}" >> $GITHUB_STEP_SUMMARY + + # -- STEP 8: Build Joomla install ZIP + SHA-256 checksum ------------------ + - name: "Step 8: Build Joomla package and update checksum" + if: >- + steps.version.outputs.skip != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + REPO="${{ github.repository }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + # All ZIPs upload to the major release tag (vXX) + RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true) + RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + if [ -z "$RELEASE_ID" ]; then + echo "No release ${RELEASE_TAG} found — skipping ZIP upload" + exit 0 + fi + + # Find extension element name from manifest + MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1 || true) + [ -z "$MANIFEST" ] && exit 0 + + # Reuse element from Step 5, with same fallback chain + EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}" + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1) + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(sed -n 's/.*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null | head -1) + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + fi + # ZIP name: type_folder_element-VERSION (e.g. plg_system_mokojgdpc-01.01.00.zip) + EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_FOLDER=$(sed -n 's/.*]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + TYPE_PREFIX="" + case "${EXT_TYPE}" in + plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; + module) TYPE_PREFIX="mod_" ;; + component) TYPE_PREFIX="com_" ;; + template) TYPE_PREFIX="tpl_" ;; + library) TYPE_PREFIX="lib_" ;; + package) TYPE_PREFIX="pkg_" ;; + esac + ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip" + TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz" + + # -- Build install packages from src/ ---------------------------- + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + [ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ — skipping package"; exit 0; } + + EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*" + + # ZIP package + cd "$SOURCE_DIR" + zip -r "/tmp/${ZIP_NAME}" . -x $EXCLUDES + cd .. + + # tar.gz package + tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \ + --exclude='.ftpignore' --exclude='sftp-config*' \ + --exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' . + + ZIP_SIZE=$(stat -c%s "/tmp/${ZIP_NAME}" 2>/dev/null || stat -f%z "/tmp/${ZIP_NAME}" 2>/dev/null || echo "unknown") + TAR_SIZE=$(stat -c%s "/tmp/${TAR_NAME}" 2>/dev/null || stat -f%z "/tmp/${TAR_NAME}" 2>/dev/null || echo "unknown") + + # -- Calculate SHA-256 for both ---------------------------------- + SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1) + SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1) + + # -- Delete existing assets with same name before uploading ------ + ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]") + for ASSET_NAME in "$ZIP_NAME" "$TAR_NAME"; do + ASSET_ID=$(echo "$ASSETS" | python3 -c " + import sys,json + assets = json.load(sys.stdin) + for a in assets: + if a['name'] == '${ASSET_NAME}': + print(a['id']); break + " 2>/dev/null || true) + if [ -n "$ASSET_ID" ]; then + curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true + fi + done + + # -- Upload both to release tag ---------------------------------- + curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @"/tmp/${ZIP_NAME}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" > /dev/null 2>&1 || true + + curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @"/tmp/${TAR_NAME}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets?name=${TAR_NAME}" > /dev/null 2>&1 || true + + # -- Update updates.xml with both download formats --------------- + if [ -f "updates.xml" ]; then + ZIP_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}" + TAR_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${TAR_NAME}" + + # Use Python to update only the stable entry's downloads + sha256 + export PY_ZIP_URL="$ZIP_URL" PY_TAR_URL="$TAR_URL" PY_SHA="$SHA256_ZIP" + python3 << 'PYEOF' + import re, os + + with open("updates.xml") as f: + content = f.read() + + zip_url = os.environ["PY_ZIP_URL"] + tar_url = os.environ["PY_TAR_URL"] + sha = os.environ["PY_SHA"] + + # Find the stable update block and replace its downloads + sha256 + def replace_stable(m): + block = m.group(0) + # Replace downloads block + new_downloads = ( + " \n" + f" {zip_url}\n" + " " + ) + block = re.sub(r' .*?', new_downloads, block, flags=re.DOTALL) + # Add or replace sha256 + if '' in block: + block = re.sub(r' .*?', f' {sha}', block) + else: + block = block.replace('', f'\n {sha}') + return block + + content = re.sub( + r' .*?stable.*?', + replace_stable, + content, + flags=re.DOTALL + ) + + with open("updates.xml", "w") as f: + f.write(content) + PYEOF + + CURRENT_BRANCH="${{ github.ref_name }}" + git add updates.xml + git commit -m "chore(release): ZIP + tar.gz for ${VERSION} [skip ci]" \ + --author="gitea-actions[bot] " || true + git push || true + + # Sync updates.xml to main via direct API (always runs — may be on version/XX branch) + GA_TOKEN="${{ secrets.GA_TOKEN }}" + API="${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}" + + FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \ + "${API}/contents/updates.xml?ref=main" | jq -r '.sha // empty') + + if [ -n "$FILE_SHA" ]; then + CONTENT=$(base64 -w0 updates.xml) + curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/contents/updates.xml" \ + -d "$(jq -n \ + --arg content "$CONTENT" \ + --arg sha "$FILE_SHA" \ + --arg msg "chore: sync updates.xml ${VERSION} [skip ci]" \ + --arg branch "main" \ + '{content: $content, sha: $sha, message: $msg, branch: $branch}' + )" > /dev/null 2>&1 \ + && echo "updates.xml synced to main via API" \ + || echo "WARNING: failed to sync updates.xml to main" + else + echo "WARNING: could not get updates.xml SHA from main" + fi + fi + + echo "### Joomla Packages" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Package | Size | SHA-256 |" >> $GITHUB_STEP_SUMMARY + echo "|---------|------|---------|" >> $GITHUB_STEP_SUMMARY + echo "| \`${ZIP_NAME}\` | ${ZIP_SIZE} | \`${SHA256_ZIP}\` |" >> $GITHUB_STEP_SUMMARY + echo "| \`${TAR_NAME}\` | ${TAR_SIZE} | \`${SHA256_TAR}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Release | \`${RELEASE_TAG}\` | |" >> $GITHUB_STEP_SUMMARY + echo "| Download | [${ZIP_NAME}](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}) |" >> $GITHUB_STEP_SUMMARY + + # -- STEP 8b: Update release description with changelog + SHA ---------------- + - name: "Step 8b: Update release body with changelog and SHA" + if: steps.version.outputs.skip != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}" + EXT_TYPE="${{ steps.updates.outputs.ext_type }}" + EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}" + + # Build TYPE_PREFIX to match Step 8's ZIP naming + TYPE_PREFIX="" + case "${EXT_TYPE}" in + plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; + module) TYPE_PREFIX="mod_" ;; + component) TYPE_PREFIX="com_" ;; + template) TYPE_PREFIX="tpl_" ;; + library) TYPE_PREFIX="lib_" ;; + package) TYPE_PREFIX="pkg_" ;; + esac + ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip" + TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz" + + # Get SHA from the built files + SHA256_ZIP="" + [ -f "/tmp/${ZIP_NAME}" ] && SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1) + SHA256_TAR="" + [ -f "/tmp/${TAR_NAME}" ] && SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1) + + # Extract latest changelog entry (strip the ## header to avoid duplicate) + CHANGELOG="" + if [ -f "CHANGELOG.md" ]; then + CHANGELOG=$(sed -n "/^## \[*${VERSION}/,/^## \[*[0-9]/p" CHANGELOG.md | sed '$d' | sed '1d') + [ -z "$CHANGELOG" ] && CHANGELOG=$(sed -n '/^## /,/^## /p' CHANGELOG.md | sed '$d' | sed '1d' | head -30) + fi + + # Build release body (single header, no duplicate from changelog) + BODY="## ${VERSION} ($(date +%Y-%m-%d))\n\n" + if [ -n "$CHANGELOG" ]; then + BODY="${BODY}${CHANGELOG}\n\n" + fi + BODY="${BODY}---\n\n### Checksums\n\n" + BODY="${BODY}| File | SHA-256 |\n|------|--------|\n" + [ -n "$SHA256_ZIP" ] && BODY="${BODY}| \`${ZIP_NAME}\` | \`${SHA256_ZIP}\` |\n" + [ -n "$SHA256_TAR" ] && BODY="${BODY}| \`${TAR_NAME}\` | \`${SHA256_TAR}\` |\n" + + # Get release ID and update body + RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null | \ + python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + + if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then + python3 -c " + import json, urllib.request + body = '''$(printf '%b' "$BODY")''' + data = json.dumps({'body': body}).encode() + req = urllib.request.Request( + '${API_BASE}/releases/${RELEASE_ID}', + data=data, + headers={'Authorization': 'token ${{ secrets.GA_TOKEN }}', 'Content-Type': 'application/json'}, + method='PATCH' + ) + urllib.request.urlopen(req) + " 2>/dev/null && echo "Release body updated with changelog + SHA" >> $GITHUB_STEP_SUMMARY + fi + + # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- + - name: "Step 9: Mirror release to GitHub" + if: >- + steps.version.outputs.skip != 'true' && + steps.version.outputs.stability == 'stable' && + secrets.GH_TOKEN != '' + continue-on-error: true + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + MAJOR="${{ steps.version.outputs.major }}" + BRANCH="${{ steps.version.outputs.branch }}" + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + + NOTES=$(php /tmp/mokostandards-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null || true) + [ -z "$NOTES" ] && NOTES="Release ${VERSION}" + echo "$NOTES" > /tmp/release_notes.md + + EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".tag_name // empty" || true) + + if [ -z "$EXISTING" ]; then + gh release create "$RELEASE_TAG" \ + --repo "$GH_REPO" \ + --title "v${MAJOR} (latest: ${VERSION})" \ + --notes-file /tmp/release_notes.md \ + --target "$BRANCH" || true + else + gh release edit "$RELEASE_TAG" \ + --repo "$GH_REPO" \ + --title "v${MAJOR} (latest: ${VERSION})" || true + fi + + # Upload assets to GitHub mirror + for PKG in /tmp/${EXT_ELEMENT:-pkg}-${VERSION}.*; do + if [ -f "$PKG" ]; then + _RELID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".id // empty") + [ -n "$_RELID" ] && curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" -H "Content-Type: application/octet-stream" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/${_RELID}/assets?name=$(basename $PKG)" --data-binary "@$PKG" > /dev/null 2>&1 || true + fi + done + echo "GitHub mirror updated: ${GH_REPO} ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY + + # -- STEP 10: Sync main branch to GitHub mirror ---------------------------- + - name: "Step 10: Push main to GitHub mirror" + if: >- + steps.version.outputs.skip != 'true' && + secrets.GH_TOKEN != '' + continue-on-error: true + run: | + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1) + GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2) + git remote add github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ + git remote set-url github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" + git fetch origin main --depth=1 + git push github origin/main:refs/heads/main --force 2>/dev/null \ + && echo "main branch pushed to GitHub mirror" \ + || echo "WARNING: GitHub mirror push failed" + + # -- Clean up lesser pre-releases (cascade) --------------------------------- + # stable → deletes all | rc → beta,alpha,dev | beta → alpha,dev | alpha → dev + - name: "Delete lesser pre-release channels" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.GA_TOKEN }}" + + # Stable deletes all pre-release channels + TAGS_TO_DELETE="development alpha beta release-candidate" + + DELETED=0 + for TAG in $TAGS_TO_DELETE; do + RELEASE_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/releases/tags/${TAG}" 2>/dev/null | \ + python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + + if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then + curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/releases/${RELEASE_ID}" 2>/dev/null || true + curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/tags/${TAG}" 2>/dev/null || true + echo "Deleted: ${TAG} (id: ${RELEASE_ID})" + DELETED=$((DELETED + 1)) + fi + done + echo "Cleaned up ${DELETED} pre-release channel(s)" >> $GITHUB_STEP_SUMMARY + + # -- STEP 11: Reset dev branch from main ------------------------------------ + - name: "Step 11: Delete and recreate dev branch from main" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.GA_TOKEN }}" + + # Delete dev branch + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch" + + # Recreate dev from main (now includes version bump + changelog promotion) + curl -sf -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/branches" \ + -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main" + + echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY + + # -- Summary -------------------------------------------------------------- + - name: Pipeline Summary + if: always() + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + if [ "${{ steps.version.outputs.skip }}" = "true" ]; then + echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY + echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY + elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then + echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Build & Release Complete (Joomla)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY + echo "|------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY + fi -- 2.52.0 From 944e1b75883a61d467b8941cfd337d2ca0edaa4b Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:23 +0000 Subject: [PATCH 023/136] chore: remove .gitea/workflows/auto-release.yml (moved to .mokogitea/) [skip ci] --- .gitea/workflows/auto-release.yml | 949 ------------------------------ 1 file changed, 949 deletions(-) delete mode 100644 .gitea/workflows/auto-release.yml diff --git a/.gitea/workflows/auto-release.yml b/.gitea/workflows/auto-release.yml deleted file mode 100644 index 279bc5e..0000000 --- a/.gitea/workflows/auto-release.yml +++ /dev/null @@ -1,949 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: MokoStandards.Release -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API -# PATH: /templates/workflows/joomla/auto-release.yml.template -# VERSION: 04.06.00 -# BRIEF: Joomla build & release — ZIP package, updates.xml, SHA-256 checksum -# -# +========================================================================+ -# | BUILD & RELEASE PIPELINE (JOOMLA) | -# +========================================================================+ -# | | -# | Triggers on push to main (skips bot commits + [skip ci]): | -# | | -# | Every push: | -# | 1. Read version from README.md | -# | 3. Set platform version (Joomla ) | -# | 4. Update [VERSION: XX.YY.ZZ] badges in markdown files | -# | 5. Write updates.xml (Joomla update server XML) | -# | 6. Create git tag vXX.YY.ZZ | -# | 7a. Patch: update existing Gitea Release for this minor | -# | 8. Build ZIP, upload asset, write SHA-256 to updates.xml | -# | | -# | Every version change: archives main -> version/XX.YY branch | -# | All patches release (including 00). Patch 00/01 = full pipeline. | -# | First release only (patch == 01): | -# | 7b. Create new Gitea Release | -# | | -# | GitHub mirror: stable/rc releases only (continue-on-error) | -# | | -# +========================================================================+ - -name: Build & Release - -on: - pull_request: - types: [closed] - branches: - - main - paths: - - 'src/**' - - 'htdocs/**' - workflow_dispatch: - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} - -permissions: - contents: write - -jobs: - release: - name: Build & Release Pipeline - runs-on: release - if: >- - github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.GA_TOKEN }} - fetch-depth: 0 - - - name: Setup MokoStandards tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN }}"}}' - run: | - # Ensure PHP + Composer are available - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \ - /tmp/mokostandards-api - cd /tmp/mokostandards-api - composer install --no-dev --no-interaction --quiet - - # -- STEP 1: Read version ----------------------------------------------- - - name: "Step 1: Read version from README.md" - id: version - run: | - VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null) - if [ -z "$VERSION" ]; then - echo "No VERSION in README.md — skipping release" - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - # Derive major.minor for branch naming (patches update existing branch) - MINOR=$(echo "$VERSION" | awk -F. '{printf "%s.%s", $1, $2}') - PATCH=$(echo "$VERSION" | awk -F. '{print $3}') - - MAJOR=$(echo "$VERSION" | awk -F. '{print $1}') - MINOR_NUM=$(echo "$VERSION" | awk -F. '{print $2}') - - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT" - echo "minor=$MINOR" >> "$GITHUB_OUTPUT" - echo "major=$MAJOR" >> "$GITHUB_OUTPUT" - echo "release_tag=stable" >> "$GITHUB_OUTPUT" - echo "stability=stable" >> "$GITHUB_OUTPUT" - echo "skip=false" >> "$GITHUB_OUTPUT" - if [ "$PATCH" = "00" ] || [ "$PATCH" = "01" ]; then - echo "is_minor=true" >> "$GITHUB_OUTPUT" - echo "Version: $VERSION (first release for this minor — full pipeline)" - else - echo "is_minor=false" >> "$GITHUB_OUTPUT" - echo "Version: $VERSION (patch — platform version + badges only)" - fi - - # -- STEP 1b: Bump minor version (stable = minor bump, reset patch) ------ - - name: "Step 1b: Bump minor version for stable release" - if: steps.version.outputs.skip != 'true' - id: bump - run: | - CURRENT=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1) - [ -z "$CURRENT" ] && { echo "skip=true" >> "$GITHUB_OUTPUT"; exit 0; } - - MAJOR=$((10#$(echo "$CURRENT" | cut -d. -f1))) - MINOR=$((10#$(echo "$CURRENT" | cut -d. -f2))) - - # Minor bump, reset patch. Rollover if minor > 99 - MINOR=$((MINOR + 1)) - if [ $MINOR -gt 99 ]; then - MINOR=0 - MAJOR=$((MAJOR + 1)) - fi - - VERSION=$(printf "%02d.%02d.00" $MAJOR $MINOR) - TODAY=$(date +%Y-%m-%d) - - echo "Stable bump: ${CURRENT} → ${VERSION} (minor)" - - # Update README.md - sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${VERSION}/" README.md - - # Update manifest - MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) - if [ -n "$MANIFEST" ]; then - MANIFEST_VER=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1) - [ -n "$MANIFEST_VER" ] && sed -i "s|${MANIFEST_VER}|${VERSION}|" "$MANIFEST" - sed -i "s|[^<]*|${TODAY}|" "$MANIFEST" - fi - - # Promote [Unreleased] section in CHANGELOG.md to new version - if [ -f "CHANGELOG.md" ] && grep -qi "Unreleased" CHANGELOG.md; then - sed -i "s|## \[Unreleased\]|## [${VERSION}] --- ${TODAY}|" CHANGELOG.md - sed -i "s|## Unreleased|## [${VERSION}] --- ${TODAY}|" CHANGELOG.md - sed -i "2i ## [Unreleased]" CHANGELOG.md - sed -i "3i \\ " CHANGELOG.md - echo "CHANGELOG promoted to [${VERSION}]" - fi - - # Commit and push - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - git add -A - git diff --cached --quiet || { - git commit -m "chore(version): bump ${CURRENT} → ${VERSION} [skip ci]" - git push origin HEAD:main 2>&1 - } - - # Override version output for rest of pipeline - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "major=$(printf "%02d" $MAJOR)" >> "$GITHUB_OUTPUT" - - - name: Check if already released - if: steps.version.outputs.skip != 'true' - id: check - run: | - TAG="${{ steps.version.outputs.release_tag }}" - BRANCH="${{ steps.version.outputs.branch }}" - - TAG_EXISTS=false - BRANCH_EXISTS=false - - git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true - git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH" && BRANCH_EXISTS=true - - echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT" - echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT" - - # Tag and branch may persist across patch releases — never skip - echo "already_released=false" >> "$GITHUB_OUTPUT" - - # -- SANITY CHECKS ------------------------------------------------------- - - name: "Sanity: Pre-release validation" - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - ERRORS=0 - - echo "## Pre-Release Sanity Checks (Joomla)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # -- Version drift check (must pass before release) -------- - README_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1) - if [ "$README_VER" != "$VERSION" ]; then - echo "- Version drift: README says \`${README_VER}\` but releasing \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS+1)) - else - echo "- Version consistent: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY - fi - - # Check CHANGELOG version matches - CL_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' CHANGELOG.md 2>/dev/null | head -1) - if [ -n "$CL_VER" ] && [ "$CL_VER" != "$VERSION" ]; then - echo "- CHANGELOG drift: \`${CL_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS+1)) - fi - - # Check composer.json version if present - if [ -f "composer.json" ]; then - COMP_VER=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' composer.json 2>/dev/null | head -1) - if [ -n "$COMP_VER" ] && [ "$COMP_VER" != "$VERSION" ]; then - echo "- composer.json drift: \`${COMP_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS+1)) - fi - fi - - # Common checks - if [ ! -f "LICENSE" ]; then - echo "- Missing LICENSE file" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS+1)) - else - echo "- LICENSE present" >> $GITHUB_STEP_SUMMARY - fi - - if [ ! -d "src" ] && [ ! -d "htdocs" ]; then - echo "- Warning: No src/ or htdocs/ directory" >> $GITHUB_STEP_SUMMARY - else - echo "- Source directory present" >> $GITHUB_STEP_SUMMARY - fi - - # -- Joomla: manifest version drift -------- - MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1) - if [ -n "$MANIFEST" ]; then - XML_VER=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1) - if [ -n "$XML_VER" ] && [ "$XML_VER" != "$VERSION" ]; then - echo "- Manifest drift: \`${XML_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS+1)) - else - echo "- Manifest version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY - fi - fi - - # -- Joomla: XML manifest existence -------- - if [ -z "$MANIFEST" ]; then - echo "- No Joomla XML manifest found" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS+1)) - else - echo "- Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY - - # -- Joomla: extension type check -------- - TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null) - echo "- Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY - fi - - echo "" >> $GITHUB_STEP_SUMMARY - if [ "$ERRORS" -gt 0 ]; then - echo "**${ERRORS} error(s) — release may be incomplete**" >> $GITHUB_STEP_SUMMARY - else - echo "**All sanity checks passed**" >> $GITHUB_STEP_SUMMARY - fi - - # -- STEP 2: Create or update version/XX.YY archive branch --------------- - # Always runs — every version change on main archives to version/XX.YY - - name: "Step 2: Version archive branch" - if: steps.check.outputs.already_released != 'true' - run: | - BRANCH="${{ steps.version.outputs.branch }}" - IS_MINOR="${{ steps.version.outputs.is_minor }}" - PATCH="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}') - - # Check if branch exists - if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then - git push origin HEAD:"$BRANCH" --force - echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY - else - git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH" - git push origin "$BRANCH" --force - echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY - fi - - # -- STEP 3: Set platform version ---------------------------------------- - - name: "Step 3: Set platform version" - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - php /tmp/mokostandards-api/cli/version_set_platform.php \ - --path . --version "$VERSION" --branch main - - # -- STEP 4: Update version badges ---------------------------------------- - - name: "Step 4: Update version badges" - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - find . -name "*.md" ! -path "./.git/*" ! -path "./vendor/*" | while read -r f; do - if grep -q '\[VERSION:' "$f" 2>/dev/null; then - sed -i "s/\[VERSION:[[:space:]]*[0-9]\{2\}\.[0-9]\{2\}\.[0-9]\{2\}\]/[VERSION: ${VERSION}]/" "$f" - fi - done - - # -- STEP 5: Write updates.xml (Joomla update server) --------------------- - - name: "Step 5: Write updates.xml" - id: updates - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - REPO="${{ github.repository }}" - - # -- Parse extension metadata from XML manifest ---------------- - MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1) - if [ -z "$MANIFEST" ]; then - echo "Warning: No Joomla XML manifest found — skipping updates.xml" >> $GITHUB_STEP_SUMMARY - exit 0 - fi - - # Extract fields using sed (portable — no grep -P) - EXT_NAME=$(sed -n 's/.*\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1) - EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) - EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1) - EXT_CLIENT=$(sed -n 's/.*]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) - EXT_FOLDER=$(sed -n 's/.*]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) - TARGET_PLATFORM=$(sed -n 's/.*\(\).*/\1/p' "$MANIFEST" | head -1) - PHP_MINIMUM=$(sed -n 's/.*\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1) - - # If EXT_NAME is a language key (e.g. PLG_SYSTEM_MOKOJGDPC), resolve from .ini - if echo "$EXT_NAME" | grep -qE '^[A-Z_]+$'; then - INI_NAME=$(find . -name "*.sys.ini" -path "*/en-GB/*" -exec grep -h "^${EXT_NAME}=" {} \; 2>/dev/null | head -1 | cut -d'"' -f2) - [ -z "$INI_NAME" ] && INI_NAME=$(find . -name "*.sys.ini" -exec grep -h "^${EXT_NAME}=" {} \; 2>/dev/null | head -1 | cut -d'"' -f2) - [ -n "$INI_NAME" ] && EXT_NAME="$INI_NAME" - fi - - # Fallbacks - [ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}" - [ -z "$EXT_TYPE" ] && EXT_TYPE="component" - - # Derive element if not in manifest: - # 1. plugin="xxx" attribute (plugins) - # 2. module="xxx" attribute (modules) - # 3. XML filename (components, packages) - # 4. Repo name fallback (templates, anything else) - if [ -z "$EXT_ELEMENT" ]; then - EXT_ELEMENT=$(sed -n 's/.*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) - fi - if [ -z "$EXT_ELEMENT" ]; then - EXT_ELEMENT=$(sed -n 's/.*module="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) - fi - if [ -z "$EXT_ELEMENT" ]; then - FNAME=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') - # If filename is generic (templateDetails, manifest), use repo name - case "$FNAME" in - templatedetails|manifest) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;; - *) EXT_ELEMENT="$FNAME" ;; - esac - fi - # Final fallback - [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') - - # Save for Steps 7, 8, 8b - echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" - echo "ext_name=${EXT_NAME}" >> "$GITHUB_OUTPUT" - echo "ext_type=${EXT_TYPE}" >> "$GITHUB_OUTPUT" - echo "ext_folder=${EXT_FOLDER}" >> "$GITHUB_OUTPUT" - - # Build client tag: plugins and frontend modules need site - CLIENT_TAG="" - if [ -n "$EXT_CLIENT" ]; then - CLIENT_TAG="${EXT_CLIENT}" - elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then - CLIENT_TAG="site" - fi - - # Build folder tag for plugins (required for Joomla to match the update) - FOLDER_TAG="" - if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then - FOLDER_TAG="${EXT_FOLDER}" - fi - - # Build targetplatform (fallback to Joomla 5 if not in manifest) - if [ -z "$TARGET_PLATFORM" ]; then - TARGET_PLATFORM=$(printf '' "/") - fi - - # Build php_minimum tag - PHP_TAG="" - if [ -n "$PHP_MINIMUM" ]; then - PHP_TAG="${PHP_MINIMUM}" - fi - - # Build TYPE_PREFIX for download URL - TYPE_PREFIX="" - case "${EXT_TYPE}" in - plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; - module) TYPE_PREFIX="mod_" ;; - component) TYPE_PREFIX="com_" ;; - template) TYPE_PREFIX="tpl_" ;; - library) TYPE_PREFIX="lib_" ;; - package) TYPE_PREFIX="pkg_" ;; - esac - - DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/stable/${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip" - INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/stable" - - # -- Build update entry for a given stability tag - build_entry() { - local TAG_NAME="$1" - printf '%s\n' ' ' - printf '%s\n' " ${EXT_NAME}" - printf '%s\n' " ${EXT_NAME} update" - printf '%s\n' " ${EXT_ELEMENT}" - printf '%s\n' " ${EXT_TYPE}" - printf '%s\n' " ${VERSION}" - [ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}" - [ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}" - printf '%s\n' " ${TAG_NAME}" - printf '%s\n' " ${INFO_URL}" - printf '%s\n' ' ' - printf '%s\n' " ${DOWNLOAD_URL}" - printf '%s\n' ' ' - printf '%s\n' " ${TARGET_PLATFORM}" - [ -n "$PHP_TAG" ] && printf '%s\n' " ${PHP_TAG}" - printf '%s\n' ' Moko Consulting' - printf '%s\n' ' https://mokoconsulting.tech' - printf '%s\n' ' ' - } - - # -- Write updates.xml with cascading channels - # Stable release updates ALL channels (development, alpha, beta, rc, stable) - { - printf '%s\n' "" - printf '%s\n' "" - printf '%s\n' "" - printf '%s\n' '' - build_entry "development" - build_entry "alpha" - build_entry "beta" - build_entry "rc" - build_entry "stable" - printf '%s\n' '' - } > updates.xml - - echo "updates.xml: ${VERSION} (all channels updated to stable)" >> $GITHUB_STEP_SUMMARY - - # -- Commit all changes --------------------------------------------------- - - name: Commit release changes - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' - run: | - if git diff --quiet && git diff --cached --quiet; then - echo "No changes to commit" - exit 0 - fi - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - # Set push URL with token for branch-protected repos - git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - git add -A - git commit -m "chore(release): build ${VERSION} [skip ci]" \ - --author="gitea-actions[bot] " - git push -u origin HEAD - - # -- STEP 6: Create tag --------------------------------------------------- - - name: "Step 6: Create git tag" - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.tag_exists != 'true' && - steps.version.outputs.is_minor == 'true' - run: | - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - # Only create the major release tag if it doesn't exist yet - if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then - git tag "$RELEASE_TAG" - git push origin "$RELEASE_TAG" - echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY - else - echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY - fi - echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY - - # -- STEP 7: Create or update Gitea Release -------------------------------- - - name: "Step 7: Gitea Release" - if: >- - steps.version.outputs.skip != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - BRANCH="${{ steps.version.outputs.branch }}" - MAJOR="${{ steps.version.outputs.major }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - - # Reuse metadata from Step 5 (single source of truth) - EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}" - EXT_NAME="${{ steps.updates.outputs.ext_name }}" - EXT_TYPE="${{ steps.updates.outputs.ext_type }}" - EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}" - - # Fallbacks if Step 5 was skipped - if [ -z "$EXT_ELEMENT" ]; then - EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') - fi - [ -z "$EXT_NAME" ] && EXT_NAME="${GITEA_REPO}" - - NOTES=$(php /tmp/mokostandards-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null) - [ -z "$NOTES" ] && NOTES="Release ${VERSION}" - - # Build release name: "Pretty Name VERSION (type_element-VERSION)" - TYPE_PREFIX="" - case "${EXT_TYPE}" in - plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; - module) TYPE_PREFIX="mod_" ;; - component) TYPE_PREFIX="com_" ;; - template) TYPE_PREFIX="tpl_" ;; - library) TYPE_PREFIX="lib_" ;; - package) TYPE_PREFIX="pkg_" ;; - esac - RELEASE_NAME="${EXT_NAME} ${VERSION} (${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION})" - - # Delete existing release if present (overwrite, not append) - EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true) - EXISTING_ID=$(echo "$EXISTING" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true) - - if [ -n "$EXISTING_ID" ]; then - curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - "${API_BASE}/releases/${EXISTING_ID}" 2>/dev/null || true - curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - "${API_BASE}/tags/${RELEASE_TAG}" 2>/dev/null || true - echo "Deleted previous stable release (id: ${EXISTING_ID})" - fi - - # Create fresh release - curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - -H "Content-Type: application/json" \ - "${API_BASE}/releases" \ - -d "$(python3 -c "import json; print(json.dumps({ - 'tag_name': '${RELEASE_TAG}', - 'name': '${RELEASE_NAME}', - 'body': '''## ${VERSION} ($(date +%Y-%m-%d))\n${NOTES}''', - 'target_commitish': '${BRANCH}' - }))")" - echo "Release created: ${RELEASE_NAME}" >> $GITHUB_STEP_SUMMARY - - # -- STEP 8: Build Joomla install ZIP + SHA-256 checksum ------------------ - - name: "Step 8: Build Joomla package and update checksum" - if: >- - steps.version.outputs.skip != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - REPO="${{ github.repository }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - - # All ZIPs upload to the major release tag (vXX) - RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true) - RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) - if [ -z "$RELEASE_ID" ]; then - echo "No release ${RELEASE_TAG} found — skipping ZIP upload" - exit 0 - fi - - # Find extension element name from manifest - MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1 || true) - [ -z "$MANIFEST" ] && exit 0 - - # Reuse element from Step 5, with same fallback chain - EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}" - if [ -z "$EXT_ELEMENT" ]; then - EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1) - [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(sed -n 's/.*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null | head -1) - [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') - [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') - fi - # ZIP name: type_folder_element-VERSION (e.g. plg_system_mokojgdpc-01.01.00.zip) - EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) - EXT_FOLDER=$(sed -n 's/.*]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) - TYPE_PREFIX="" - case "${EXT_TYPE}" in - plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; - module) TYPE_PREFIX="mod_" ;; - component) TYPE_PREFIX="com_" ;; - template) TYPE_PREFIX="tpl_" ;; - library) TYPE_PREFIX="lib_" ;; - package) TYPE_PREFIX="pkg_" ;; - esac - ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip" - TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz" - - # -- Build install packages from src/ ---------------------------- - SOURCE_DIR="src" - [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" - [ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ — skipping package"; exit 0; } - - EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*" - - # ZIP package - cd "$SOURCE_DIR" - zip -r "/tmp/${ZIP_NAME}" . -x $EXCLUDES - cd .. - - # tar.gz package - tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \ - --exclude='.ftpignore' --exclude='sftp-config*' \ - --exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' . - - ZIP_SIZE=$(stat -c%s "/tmp/${ZIP_NAME}" 2>/dev/null || stat -f%z "/tmp/${ZIP_NAME}" 2>/dev/null || echo "unknown") - TAR_SIZE=$(stat -c%s "/tmp/${TAR_NAME}" 2>/dev/null || stat -f%z "/tmp/${TAR_NAME}" 2>/dev/null || echo "unknown") - - # -- Calculate SHA-256 for both ---------------------------------- - SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1) - SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1) - - # -- Delete existing assets with same name before uploading ------ - ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - "${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]") - for ASSET_NAME in "$ZIP_NAME" "$TAR_NAME"; do - ASSET_ID=$(echo "$ASSETS" | python3 -c " - import sys,json - assets = json.load(sys.stdin) - for a in assets: - if a['name'] == '${ASSET_NAME}': - print(a['id']); break - " 2>/dev/null || true) - if [ -n "$ASSET_ID" ]; then - curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - "${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true - fi - done - - # -- Upload both to release tag ---------------------------------- - curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - -H "Content-Type: application/octet-stream" \ - --data-binary @"/tmp/${ZIP_NAME}" \ - "${API_BASE}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" > /dev/null 2>&1 || true - - curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - -H "Content-Type: application/octet-stream" \ - --data-binary @"/tmp/${TAR_NAME}" \ - "${API_BASE}/releases/${RELEASE_ID}/assets?name=${TAR_NAME}" > /dev/null 2>&1 || true - - # -- Update updates.xml with both download formats --------------- - if [ -f "updates.xml" ]; then - ZIP_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}" - TAR_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${TAR_NAME}" - - # Use Python to update only the stable entry's downloads + sha256 - export PY_ZIP_URL="$ZIP_URL" PY_TAR_URL="$TAR_URL" PY_SHA="$SHA256_ZIP" - python3 << 'PYEOF' - import re, os - - with open("updates.xml") as f: - content = f.read() - - zip_url = os.environ["PY_ZIP_URL"] - tar_url = os.environ["PY_TAR_URL"] - sha = os.environ["PY_SHA"] - - # Find the stable update block and replace its downloads + sha256 - def replace_stable(m): - block = m.group(0) - # Replace downloads block - new_downloads = ( - " \n" - f" {zip_url}\n" - " " - ) - block = re.sub(r' .*?', new_downloads, block, flags=re.DOTALL) - # Add or replace sha256 - if '' in block: - block = re.sub(r' .*?', f' {sha}', block) - else: - block = block.replace('', f'\n {sha}') - return block - - content = re.sub( - r' .*?stable.*?', - replace_stable, - content, - flags=re.DOTALL - ) - - with open("updates.xml", "w") as f: - f.write(content) - PYEOF - - CURRENT_BRANCH="${{ github.ref_name }}" - git add updates.xml - git commit -m "chore(release): ZIP + tar.gz for ${VERSION} [skip ci]" \ - --author="gitea-actions[bot] " || true - git push || true - - # Sync updates.xml to main via direct API (always runs — may be on version/XX branch) - GA_TOKEN="${{ secrets.GA_TOKEN }}" - API="${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}" - - FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \ - "${API}/contents/updates.xml?ref=main" | jq -r '.sha // empty') - - if [ -n "$FILE_SHA" ]; then - CONTENT=$(base64 -w0 updates.xml) - curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \ - -H "Content-Type: application/json" \ - "${API}/contents/updates.xml" \ - -d "$(jq -n \ - --arg content "$CONTENT" \ - --arg sha "$FILE_SHA" \ - --arg msg "chore: sync updates.xml ${VERSION} [skip ci]" \ - --arg branch "main" \ - '{content: $content, sha: $sha, message: $msg, branch: $branch}' - )" > /dev/null 2>&1 \ - && echo "updates.xml synced to main via API" \ - || echo "WARNING: failed to sync updates.xml to main" - else - echo "WARNING: could not get updates.xml SHA from main" - fi - fi - - echo "### Joomla Packages" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Package | Size | SHA-256 |" >> $GITHUB_STEP_SUMMARY - echo "|---------|------|---------|" >> $GITHUB_STEP_SUMMARY - echo "| \`${ZIP_NAME}\` | ${ZIP_SIZE} | \`${SHA256_ZIP}\` |" >> $GITHUB_STEP_SUMMARY - echo "| \`${TAR_NAME}\` | ${TAR_SIZE} | \`${SHA256_TAR}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Release | \`${RELEASE_TAG}\` | |" >> $GITHUB_STEP_SUMMARY - echo "| Download | [${ZIP_NAME}](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}) |" >> $GITHUB_STEP_SUMMARY - - # -- STEP 8b: Update release description with changelog + SHA ---------------- - - name: "Step 8b: Update release body with changelog and SHA" - if: steps.version.outputs.skip != 'true' - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}" - EXT_TYPE="${{ steps.updates.outputs.ext_type }}" - EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}" - - # Build TYPE_PREFIX to match Step 8's ZIP naming - TYPE_PREFIX="" - case "${EXT_TYPE}" in - plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; - module) TYPE_PREFIX="mod_" ;; - component) TYPE_PREFIX="com_" ;; - template) TYPE_PREFIX="tpl_" ;; - library) TYPE_PREFIX="lib_" ;; - package) TYPE_PREFIX="pkg_" ;; - esac - ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip" - TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz" - - # Get SHA from the built files - SHA256_ZIP="" - [ -f "/tmp/${ZIP_NAME}" ] && SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1) - SHA256_TAR="" - [ -f "/tmp/${TAR_NAME}" ] && SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1) - - # Extract latest changelog entry (strip the ## header to avoid duplicate) - CHANGELOG="" - if [ -f "CHANGELOG.md" ]; then - CHANGELOG=$(sed -n "/^## \[*${VERSION}/,/^## \[*[0-9]/p" CHANGELOG.md | sed '$d' | sed '1d') - [ -z "$CHANGELOG" ] && CHANGELOG=$(sed -n '/^## /,/^## /p' CHANGELOG.md | sed '$d' | sed '1d' | head -30) - fi - - # Build release body (single header, no duplicate from changelog) - BODY="## ${VERSION} ($(date +%Y-%m-%d))\n\n" - if [ -n "$CHANGELOG" ]; then - BODY="${BODY}${CHANGELOG}\n\n" - fi - BODY="${BODY}---\n\n### Checksums\n\n" - BODY="${BODY}| File | SHA-256 |\n|------|--------|\n" - [ -n "$SHA256_ZIP" ] && BODY="${BODY}| \`${ZIP_NAME}\` | \`${SHA256_ZIP}\` |\n" - [ -n "$SHA256_TAR" ] && BODY="${BODY}| \`${TAR_NAME}\` | \`${SHA256_TAR}\` |\n" - - # Get release ID and update body - RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null | \ - python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) - - if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then - python3 -c " - import json, urllib.request - body = '''$(printf '%b' "$BODY")''' - data = json.dumps({'body': body}).encode() - req = urllib.request.Request( - '${API_BASE}/releases/${RELEASE_ID}', - data=data, - headers={'Authorization': 'token ${{ secrets.GA_TOKEN }}', 'Content-Type': 'application/json'}, - method='PATCH' - ) - urllib.request.urlopen(req) - " 2>/dev/null && echo "Release body updated with changelog + SHA" >> $GITHUB_STEP_SUMMARY - fi - - # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- - - name: "Step 9: Mirror release to GitHub" - if: >- - steps.version.outputs.skip != 'true' && - steps.version.outputs.stability == 'stable' && - secrets.GH_TOKEN != '' - continue-on-error: true - env: - GH_TOKEN: ${{ secrets.GH_TOKEN }} - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - MAJOR="${{ steps.version.outputs.major }}" - BRANCH="${{ steps.version.outputs.branch }}" - GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" - - NOTES=$(php /tmp/mokostandards-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null || true) - [ -z "$NOTES" ] && NOTES="Release ${VERSION}" - echo "$NOTES" > /tmp/release_notes.md - - EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".tag_name // empty" || true) - - if [ -z "$EXISTING" ]; then - gh release create "$RELEASE_TAG" \ - --repo "$GH_REPO" \ - --title "v${MAJOR} (latest: ${VERSION})" \ - --notes-file /tmp/release_notes.md \ - --target "$BRANCH" || true - else - gh release edit "$RELEASE_TAG" \ - --repo "$GH_REPO" \ - --title "v${MAJOR} (latest: ${VERSION})" || true - fi - - # Upload assets to GitHub mirror - for PKG in /tmp/${EXT_ELEMENT:-pkg}-${VERSION}.*; do - if [ -f "$PKG" ]; then - _RELID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".id // empty") - [ -n "$_RELID" ] && curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" -H "Content-Type: application/octet-stream" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/${_RELID}/assets?name=$(basename $PKG)" --data-binary "@$PKG" > /dev/null 2>&1 || true - fi - done - echo "GitHub mirror updated: ${GH_REPO} ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY - - # -- STEP 10: Sync main branch to GitHub mirror ---------------------------- - - name: "Step 10: Push main to GitHub mirror" - if: >- - steps.version.outputs.skip != 'true' && - secrets.GH_TOKEN != '' - continue-on-error: true - run: | - GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" - GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1) - GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2) - git remote add github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ - git remote set-url github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" - git fetch origin main --depth=1 - git push github origin/main:refs/heads/main --force 2>/dev/null \ - && echo "main branch pushed to GitHub mirror" \ - || echo "WARNING: GitHub mirror push failed" - - # -- Clean up lesser pre-releases (cascade) --------------------------------- - # stable → deletes all | rc → beta,alpha,dev | beta → alpha,dev | alpha → dev - - name: "Delete lesser pre-release channels" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.GA_TOKEN }}" - - # Stable deletes all pre-release channels - TAGS_TO_DELETE="development alpha beta release-candidate" - - DELETED=0 - for TAG in $TAGS_TO_DELETE; do - RELEASE_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/releases/tags/${TAG}" 2>/dev/null | \ - python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) - - if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then - curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/releases/${RELEASE_ID}" 2>/dev/null || true - curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/tags/${TAG}" 2>/dev/null || true - echo "Deleted: ${TAG} (id: ${RELEASE_ID})" - DELETED=$((DELETED + 1)) - fi - done - echo "Cleaned up ${DELETED} pre-release channel(s)" >> $GITHUB_STEP_SUMMARY - - # -- STEP 11: Reset dev branch from main ------------------------------------ - - name: "Step 11: Delete and recreate dev branch from main" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.GA_TOKEN }}" - - # Delete dev branch - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch" - - # Recreate dev from main (now includes version bump + changelog promotion) - curl -sf -X POST -H "Authorization: token ${TOKEN}" \ - -H "Content-Type: application/json" \ - "${API_BASE}/branches" \ - -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main" - - echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY - - # -- Summary -------------------------------------------------------------- - - name: Pipeline Summary - if: always() - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - if [ "${{ steps.version.outputs.skip }}" = "true" ]; then - echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY - echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY - elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then - echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY - else - echo "" >> $GITHUB_STEP_SUMMARY - echo "## Build & Release Complete (Joomla)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY - echo "|------|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY - fi -- 2.52.0 From b5e8639d6c8a4458d5b8147c706f9540e42f3b21 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:23 +0000 Subject: [PATCH 024/136] chore: move .gitea/workflows/cascade-dev.yml to .mokogitea/cascade-dev.yml [skip ci] --- .mokogitea/cascade-dev.yml | 213 +++++++++++++++++++++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 .mokogitea/cascade-dev.yml diff --git a/.mokogitea/cascade-dev.yml b/.mokogitea/cascade-dev.yml new file mode 100644 index 0000000..d4780b1 --- /dev/null +++ b/.mokogitea/cascade-dev.yml @@ -0,0 +1,213 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Maintenance +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# PATH: /templates/workflows/cascade-dev.yml.template +# VERSION: 02.00.00 +# BRIEF: Forward-merge main → all open branches after every push to main +# +# +========================================================================+ +# | CASCADE MAIN → ALL BRANCHES | +# +========================================================================+ +# | | +# | Triggers on every push to main (PR merges, bot commits, etc.) | +# | | +# | 1. List all branches matching: dev, rc/*, beta/*, alpha/* | +# | 2. For each: create PR (main → branch), auto-merge if clean | +# | 3. On conflict: leave PR open for manual resolution | +# | | +# +========================================================================+ + +name: Cascade Main → Dev + +on: + push: + branches: + - main + workflow_dispatch: + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +permissions: + contents: write + pull-requests: write + +jobs: + cascade: + name: Cascade main → branches + runs-on: ubuntu-latest + if: >- + !contains(github.event.head_commit.message, '[skip ci]') && + !contains(github.event.head_commit.message, '[skip cascade]') + + steps: + - name: Discover target branches + id: branches + env: + GA_TOKEN: ${{ secrets.GA_TOKEN }} + run: | + API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + # Fetch all branches (paginated) + PAGE=1 + ALL_BRANCHES="" + while true; do + BATCH=$(curl -sS \ + -H "Authorization: token ${GA_TOKEN}" \ + "${API}/branches?page=${PAGE}&limit=50" \ + | jq -r '.[].name // empty') + [ -z "$BATCH" ] && break + ALL_BRANCHES="$ALL_BRANCHES $BATCH" + PAGE=$((PAGE + 1)) + done + + # Filter to cascade targets: dev, dev/*, rc/*, beta/*, alpha/* + TARGETS="" + for BRANCH in $ALL_BRANCHES; do + case "$BRANCH" in + dev|dev/*|rc/*|beta/*|alpha/*) + TARGETS="$TARGETS $BRANCH" + ;; + esac + done + + TARGETS=$(echo "$TARGETS" | xargs) # trim whitespace + + if [ -z "$TARGETS" ]; then + echo "targets=" >> "$GITHUB_OUTPUT" + echo "ℹ️ No cascade target branches found" + else + echo "targets=$TARGETS" >> "$GITHUB_OUTPUT" + COUNT=$(echo "$TARGETS" | wc -w) + echo "📋 Found ${COUNT} target branch(es): ${TARGETS}" + fi + + - name: Cascade to all target branches + if: steps.branches.outputs.targets != '' + env: + GA_TOKEN: ${{ secrets.GA_TOKEN }} + run: | + API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + SHORT_SHA="${GITHUB_SHA:0:7}" + TARGETS="${{ steps.branches.outputs.targets }}" + + SUCCESS=0 + CONFLICTS=0 + SKIPPED=0 + FAILED=0 + + for BRANCH in $TARGETS; do + echo "" + echo "═══ main → ${BRANCH} ═══" + + # Check if branch is already up to date + ENCODED_BRANCH=$(echo "$BRANCH" | sed 's|/|%2F|g') + RESPONSE=$(curl -sS \ + -H "Authorization: token ${GA_TOKEN}" \ + "${API}/compare/${ENCODED_BRANCH}...main") + + AHEAD=$(echo "$RESPONSE" | jq '.total_commits // 0') + + if [ "$AHEAD" -eq 0 ]; then + echo " ✅ Already up to date" + SKIPPED=$((SKIPPED + 1)) + continue + fi + + echo " ℹ️ main is ${AHEAD} commit(s) ahead" + + # Check for existing cascade PR + EXISTING=$(curl -sS \ + -H "Authorization: token ${GA_TOKEN}" \ + "${API}/pulls?state=open&head=${GITEA_ORG}:main&base=${ENCODED_BRANCH}&limit=1") + + EXISTING_COUNT=$(echo "$EXISTING" | jq 'length') + PR_NUMBER="" + + if [ "$EXISTING_COUNT" -gt 0 ]; then + PR_NUMBER=$(echo "$EXISTING" | jq -r '.[0].number') + echo " ℹ️ Reusing existing PR #${PR_NUMBER}" + else + # Create cascade PR + PR_RESPONSE=$(curl -sS -w "\n%{http_code}" \ + -X POST \ + -H "Authorization: token ${GA_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ + \"title\": \"chore: cascade main → ${BRANCH} (${SHORT_SHA}) [skip ci]\", + \"body\": \"## Automatic cascade\\n\\nForward-merging \`main\` (${SHORT_SHA}) into \`${BRANCH}\`.\\n\\nIf conflicts exist, resolve manually and merge.\\n\\n> Auto-created by **Cascade Main → Dev**.\", + \"head\": \"main\", + \"base\": \"${BRANCH}\" + }" \ + "${API}/pulls") + + HTTP_CODE=$(echo "$PR_RESPONSE" | tail -1) + BODY=$(echo "$PR_RESPONSE" | sed '$d') + PR_NUMBER=$(echo "$BODY" | jq -r '.number // empty') + + if [ "$HTTP_CODE" != "201" ] || [ -z "$PR_NUMBER" ]; then + MSG=$(echo "$BODY" | jq -r '.message // .' 2>/dev/null | head -1) + echo " ❌ Failed to create PR (HTTP ${HTTP_CODE}): ${MSG}" + FAILED=$((FAILED + 1)) + continue + fi + + echo " ✅ Created PR #${PR_NUMBER}" + fi + + # Try auto-merge + PR_DATA=$(curl -sS \ + -H "Authorization: token ${GA_TOKEN}" \ + "${API}/pulls/${PR_NUMBER}") + + MERGEABLE=$(echo "$PR_DATA" | jq -r '.mergeable // false') + + if [ "$MERGEABLE" != "true" ]; then + echo " ⚠️ Conflicts — PR #${PR_NUMBER} left open" + CONFLICTS=$((CONFLICTS + 1)) + continue + fi + + MERGE_RESPONSE=$(curl -sS -w "\n%{http_code}" \ + -X POST \ + -H "Authorization: token ${GA_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ + \"Do\": \"merge\", + \"merge_message_field\": \"chore: cascade main → ${BRANCH} [skip ci]\", + \"delete_branch_after_merge\": false + }" \ + "${API}/pulls/${PR_NUMBER}/merge") + + MERGE_HTTP=$(echo "$MERGE_RESPONSE" | tail -1) + + if [ "$MERGE_HTTP" = "200" ] || [ "$MERGE_HTTP" = "204" ]; then + echo " ✅ Merged — ${BRANCH} is in sync" + SUCCESS=$((SUCCESS + 1)) + else + MERGE_BODY=$(echo "$MERGE_RESPONSE" | sed '$d') + echo " ⚠️ Merge failed (HTTP ${MERGE_HTTP}) — PR #${PR_NUMBER} left open" + CONFLICTS=$((CONFLICTS + 1)) + fi + done + + # Summary + echo "" + echo "════════════════════════════════════════" + echo " ✅ Merged: ${SUCCESS}" + echo " ⚠️ Conflicts: ${CONFLICTS}" + echo " ⏭️ Up to date: ${SKIPPED}" + echo " ❌ Failed: ${FAILED}" + echo "════════════════════════════════════════" + + if [ "$FAILED" -gt 0 ]; then + exit 1 + fi -- 2.52.0 From 5a069ca279b1e8cec9b2fc80d7a072ec4ad15d37 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:24 +0000 Subject: [PATCH 025/136] chore: remove .gitea/workflows/cascade-dev.yml (moved to .mokogitea/) [skip ci] --- .gitea/workflows/cascade-dev.yml | 213 ------------------------------- 1 file changed, 213 deletions(-) delete mode 100644 .gitea/workflows/cascade-dev.yml diff --git a/.gitea/workflows/cascade-dev.yml b/.gitea/workflows/cascade-dev.yml deleted file mode 100644 index d4780b1..0000000 --- a/.gitea/workflows/cascade-dev.yml +++ /dev/null @@ -1,213 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: MokoStandards.Maintenance -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API -# PATH: /templates/workflows/cascade-dev.yml.template -# VERSION: 02.00.00 -# BRIEF: Forward-merge main → all open branches after every push to main -# -# +========================================================================+ -# | CASCADE MAIN → ALL BRANCHES | -# +========================================================================+ -# | | -# | Triggers on every push to main (PR merges, bot commits, etc.) | -# | | -# | 1. List all branches matching: dev, rc/*, beta/*, alpha/* | -# | 2. For each: create PR (main → branch), auto-merge if clean | -# | 3. On conflict: leave PR open for manual resolution | -# | | -# +========================================================================+ - -name: Cascade Main → Dev - -on: - push: - branches: - - main - workflow_dispatch: - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} - -permissions: - contents: write - pull-requests: write - -jobs: - cascade: - name: Cascade main → branches - runs-on: ubuntu-latest - if: >- - !contains(github.event.head_commit.message, '[skip ci]') && - !contains(github.event.head_commit.message, '[skip cascade]') - - steps: - - name: Discover target branches - id: branches - env: - GA_TOKEN: ${{ secrets.GA_TOKEN }} - run: | - API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - - # Fetch all branches (paginated) - PAGE=1 - ALL_BRANCHES="" - while true; do - BATCH=$(curl -sS \ - -H "Authorization: token ${GA_TOKEN}" \ - "${API}/branches?page=${PAGE}&limit=50" \ - | jq -r '.[].name // empty') - [ -z "$BATCH" ] && break - ALL_BRANCHES="$ALL_BRANCHES $BATCH" - PAGE=$((PAGE + 1)) - done - - # Filter to cascade targets: dev, dev/*, rc/*, beta/*, alpha/* - TARGETS="" - for BRANCH in $ALL_BRANCHES; do - case "$BRANCH" in - dev|dev/*|rc/*|beta/*|alpha/*) - TARGETS="$TARGETS $BRANCH" - ;; - esac - done - - TARGETS=$(echo "$TARGETS" | xargs) # trim whitespace - - if [ -z "$TARGETS" ]; then - echo "targets=" >> "$GITHUB_OUTPUT" - echo "ℹ️ No cascade target branches found" - else - echo "targets=$TARGETS" >> "$GITHUB_OUTPUT" - COUNT=$(echo "$TARGETS" | wc -w) - echo "📋 Found ${COUNT} target branch(es): ${TARGETS}" - fi - - - name: Cascade to all target branches - if: steps.branches.outputs.targets != '' - env: - GA_TOKEN: ${{ secrets.GA_TOKEN }} - run: | - API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - SHORT_SHA="${GITHUB_SHA:0:7}" - TARGETS="${{ steps.branches.outputs.targets }}" - - SUCCESS=0 - CONFLICTS=0 - SKIPPED=0 - FAILED=0 - - for BRANCH in $TARGETS; do - echo "" - echo "═══ main → ${BRANCH} ═══" - - # Check if branch is already up to date - ENCODED_BRANCH=$(echo "$BRANCH" | sed 's|/|%2F|g') - RESPONSE=$(curl -sS \ - -H "Authorization: token ${GA_TOKEN}" \ - "${API}/compare/${ENCODED_BRANCH}...main") - - AHEAD=$(echo "$RESPONSE" | jq '.total_commits // 0') - - if [ "$AHEAD" -eq 0 ]; then - echo " ✅ Already up to date" - SKIPPED=$((SKIPPED + 1)) - continue - fi - - echo " ℹ️ main is ${AHEAD} commit(s) ahead" - - # Check for existing cascade PR - EXISTING=$(curl -sS \ - -H "Authorization: token ${GA_TOKEN}" \ - "${API}/pulls?state=open&head=${GITEA_ORG}:main&base=${ENCODED_BRANCH}&limit=1") - - EXISTING_COUNT=$(echo "$EXISTING" | jq 'length') - PR_NUMBER="" - - if [ "$EXISTING_COUNT" -gt 0 ]; then - PR_NUMBER=$(echo "$EXISTING" | jq -r '.[0].number') - echo " ℹ️ Reusing existing PR #${PR_NUMBER}" - else - # Create cascade PR - PR_RESPONSE=$(curl -sS -w "\n%{http_code}" \ - -X POST \ - -H "Authorization: token ${GA_TOKEN}" \ - -H "Content-Type: application/json" \ - -d "{ - \"title\": \"chore: cascade main → ${BRANCH} (${SHORT_SHA}) [skip ci]\", - \"body\": \"## Automatic cascade\\n\\nForward-merging \`main\` (${SHORT_SHA}) into \`${BRANCH}\`.\\n\\nIf conflicts exist, resolve manually and merge.\\n\\n> Auto-created by **Cascade Main → Dev**.\", - \"head\": \"main\", - \"base\": \"${BRANCH}\" - }" \ - "${API}/pulls") - - HTTP_CODE=$(echo "$PR_RESPONSE" | tail -1) - BODY=$(echo "$PR_RESPONSE" | sed '$d') - PR_NUMBER=$(echo "$BODY" | jq -r '.number // empty') - - if [ "$HTTP_CODE" != "201" ] || [ -z "$PR_NUMBER" ]; then - MSG=$(echo "$BODY" | jq -r '.message // .' 2>/dev/null | head -1) - echo " ❌ Failed to create PR (HTTP ${HTTP_CODE}): ${MSG}" - FAILED=$((FAILED + 1)) - continue - fi - - echo " ✅ Created PR #${PR_NUMBER}" - fi - - # Try auto-merge - PR_DATA=$(curl -sS \ - -H "Authorization: token ${GA_TOKEN}" \ - "${API}/pulls/${PR_NUMBER}") - - MERGEABLE=$(echo "$PR_DATA" | jq -r '.mergeable // false') - - if [ "$MERGEABLE" != "true" ]; then - echo " ⚠️ Conflicts — PR #${PR_NUMBER} left open" - CONFLICTS=$((CONFLICTS + 1)) - continue - fi - - MERGE_RESPONSE=$(curl -sS -w "\n%{http_code}" \ - -X POST \ - -H "Authorization: token ${GA_TOKEN}" \ - -H "Content-Type: application/json" \ - -d "{ - \"Do\": \"merge\", - \"merge_message_field\": \"chore: cascade main → ${BRANCH} [skip ci]\", - \"delete_branch_after_merge\": false - }" \ - "${API}/pulls/${PR_NUMBER}/merge") - - MERGE_HTTP=$(echo "$MERGE_RESPONSE" | tail -1) - - if [ "$MERGE_HTTP" = "200" ] || [ "$MERGE_HTTP" = "204" ]; then - echo " ✅ Merged — ${BRANCH} is in sync" - SUCCESS=$((SUCCESS + 1)) - else - MERGE_BODY=$(echo "$MERGE_RESPONSE" | sed '$d') - echo " ⚠️ Merge failed (HTTP ${MERGE_HTTP}) — PR #${PR_NUMBER} left open" - CONFLICTS=$((CONFLICTS + 1)) - fi - done - - # Summary - echo "" - echo "════════════════════════════════════════" - echo " ✅ Merged: ${SUCCESS}" - echo " ⚠️ Conflicts: ${CONFLICTS}" - echo " ⏭️ Up to date: ${SKIPPED}" - echo " ❌ Failed: ${FAILED}" - echo "════════════════════════════════════════" - - if [ "$FAILED" -gt 0 ]; then - exit 1 - fi -- 2.52.0 From e42fb93e822954ce8041056322446e7d9e9fa5d7 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:24 +0000 Subject: [PATCH 026/136] chore: move .gitea/workflows/ci-joomla.yml to .mokogitea/ci-joomla.yml [skip ci] --- .mokogitea/ci-joomla.yml | 450 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 450 insertions(+) create mode 100644 .mokogitea/ci-joomla.yml diff --git a/.mokogitea/ci-joomla.yml b/.mokogitea/ci-joomla.yml new file mode 100644 index 0000000..28cee48 --- /dev/null +++ b/.mokogitea/ci-joomla.yml @@ -0,0 +1,450 @@ +# Copyright (C) 2026 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow.Template +# INGROUP: MokoStandards.CI +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# PATH: /templates/workflows/joomla/ci-joomla.yml.template +# VERSION: 04.06.00 +# BRIEF: CI workflow for Joomla extensions — lint, validate, test + +name: Joomla Extension CI + +on: + pull_request: + branches: + - main + - 'dev/**' + workflow_dispatch: + +permissions: + contents: read + pull-requests: write + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + lint-and-validate: + name: Lint & Validate + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP + run: | + php -v && composer --version + + - name: Clone MokoStandards + env: + GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} + MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} + MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }} + run: | + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \ + /tmp/mokostandards-api + + - name: Install dependencies + env: + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}' + run: | + if [ -f "composer.json" ]; then + composer install \ + --no-interaction \ + --prefer-dist \ + --optimize-autoloader + else + echo "No composer.json found — skipping dependency install" + fi + + - name: PHP syntax check + run: | + ERRORS=0 + for DIR in src/ htdocs/; do + if [ -d "$DIR" ]; then + FOUND=1 + while IFS= read -r -d '' FILE; do + OUTPUT=$(php -l "$FILE" 2>&1) + if echo "$OUTPUT" | grep -q "Parse error"; then + echo "::error file=${FILE}::${OUTPUT}" + ERRORS=$((ERRORS + 1)) + fi + done < <(find "$DIR" -name "*.php" -print0) + fi + done + echo "### PHP Syntax Check" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} syntax error(s) found.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY + fi + + - name: XML manifest validation + run: | + echo "### XML Manifest Validation" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + # Find the extension manifest (XML with /dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No Joomla extension manifest found (XML file with \`> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Manifest found: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY + + # Validate well-formed XML + php -r " + \$xml = @simplexml_load_file('$MANIFEST'); + if (\$xml === false) { + echo 'INVALID'; + exit(1); + } + echo 'VALID'; + " > /tmp/xml_result 2>&1 + XML_RESULT=$(cat /tmp/xml_result) + if [ "$XML_RESULT" != "VALID" ]; then + echo "Manifest is not well-formed XML." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Manifest is well-formed XML." >> $GITHUB_STEP_SUMMARY + fi + + # Check required tags: name, version, author, namespace (Joomla 5+) + for TAG in name version author namespace; do + if ! grep -q "<${TAG}>" "$MANIFEST" 2>/dev/null; then + echo "Missing required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Found required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY + fi + done + fi + + if [ "${ERRORS}" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**${ERRORS} manifest issue(s) found.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Manifest validation passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Check language files referenced in manifest + run: | + echo "### Language File Check" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -n "$MANIFEST" ]; then + # Extract language file references from manifest + LANG_FILES=$(grep -oP 'language\s+tag="[^"]*"[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true) + if [ -z "$LANG_FILES" ]; then + echo "No language file references found in manifest — skipping." >> $GITHUB_STEP_SUMMARY + else + while IFS= read -r LANG_FILE; do + LANG_FILE=$(echo "$LANG_FILE" | xargs) + if [ -z "$LANG_FILE" ]; then + continue + fi + # Check in common locations + FOUND=0 + for BASE in "." "src" "htdocs"; do + if [ -f "${BASE}/${LANG_FILE}" ]; then + FOUND=1 + break + fi + done + if [ "$FOUND" -eq 0 ]; then + echo "Missing language file: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Language file present: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY + fi + done <<< "$LANG_FILES" + fi + else + echo "No manifest found — skipping language check." >> $GITHUB_STEP_SUMMARY + fi + + if [ "${ERRORS}" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**${ERRORS} missing language file(s).**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Language file check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Check index.html files in directories + run: | + echo "### Index.html Check" >> $GITHUB_STEP_SUMMARY + MISSING=0 + CHECKED=0 + + for DIR in src/ htdocs/; do + if [ -d "$DIR" ]; then + while IFS= read -r -d '' SUBDIR; do + CHECKED=$((CHECKED + 1)) + if [ ! -f "${SUBDIR}/index.html" ]; then + echo "Missing index.html in: \`${SUBDIR}\`" >> $GITHUB_STEP_SUMMARY + MISSING=$((MISSING + 1)) + fi + done < <(find "$DIR" -type d -print0) + fi + done + + if [ "${CHECKED}" -eq 0 ]; then + echo "No src/ or htdocs/ directories found — skipping." >> $GITHUB_STEP_SUMMARY + elif [ "${MISSING}" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**${MISSING} director(ies) missing index.html out of ${CHECKED} checked.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "All ${CHECKED} directories contain index.html." >> $GITHUB_STEP_SUMMARY + fi + + release-readiness: + name: Release Readiness Check + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' && github.base_ref == 'main' + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Validate release readiness + run: | + echo "## Release Readiness" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + # Extract version from README.md + README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md | head -1) + if [ -z "$README_VERSION" ]; then + echo "No VERSION found in README.md FILE INFORMATION block." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "README version: \`${README_VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + + # Find the extension manifest + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No Joomla extension manifest found." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY + + # Check matches README VERSION + MANIFEST_VERSION=$(grep -oP '\K[^<]+' "$MANIFEST" | head -1) + if [ -z "$MANIFEST_VERSION" ]; then + echo "No \`\` tag in manifest." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + elif [ -n "$README_VERSION" ] && [ "$MANIFEST_VERSION" != "$README_VERSION" ]; then + echo "Manifest version \`${MANIFEST_VERSION}\` does not match README \`${README_VERSION}\`." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Manifest version: \`${MANIFEST_VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + + # Check extension type, element, client attributes + EXT_TYPE=$(grep -oP ']*\btype="\K[^"]+' "$MANIFEST" | head -1) + if [ -z "$EXT_TYPE" ]; then + echo "Missing \`type\` attribute on \`\` tag." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Extension type: \`${EXT_TYPE}\`" >> $GITHUB_STEP_SUMMARY + fi + + # Element check (component/module/plugin name) + HAS_ELEMENT=$(grep -cP '<(element|name)>' "$MANIFEST" 2>/dev/null || echo "0") + if [ "$HAS_ELEMENT" -eq 0 ]; then + echo "Missing \`\` or \`\` in manifest." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + + # Client attribute for site/admin modules and plugins + if echo "$EXT_TYPE" | grep -qP "^(module|plugin)$"; then + HAS_CLIENT=$(grep -cP ']*\bclient=' "$MANIFEST" 2>/dev/null || echo "0") + if [ "$HAS_CLIENT" -eq 0 ]; then + echo "Missing \`client\` attribute for ${EXT_TYPE} extension." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + fi + fi + + # Check updates.xml exists + if [ -f "updates.xml" ] || [ -f "updates.xml" ]; then + echo "Update XML present." >> $GITHUB_STEP_SUMMARY + else + echo "No updates.xml found." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + + # Check CHANGELOG.md exists + if [ -f "CHANGELOG.md" ]; then + echo "CHANGELOG.md present." >> $GITHUB_STEP_SUMMARY + else + echo "No CHANGELOG.md found." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ $ERRORS -gt 0 ]; then + echo "**${ERRORS} issue(s) must be resolved before release.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Extension is ready for release.**" >> $GITHUB_STEP_SUMMARY + fi + + test: + name: Tests (PHP ${{ matrix.php }}) + runs-on: ubuntu-latest + needs: lint-and-validate + + strategy: + fail-fast: false + matrix: + php: ['8.2', '8.3'] + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP ${{ matrix.php }} + run: | + php -v && composer --version + + - name: Install dependencies + env: + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}' + run: | + if [ -f "composer.json" ]; then + composer install \ + --no-interaction \ + --prefer-dist \ + --optimize-autoloader + else + echo "No composer.json found — skipping dependency install" + fi + + - name: Run tests + run: | + echo "### Test Results (PHP ${{ matrix.php }})" >> $GITHUB_STEP_SUMMARY + if [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then + vendor/bin/phpunit --testdox 2>&1 | tee /tmp/test-output.log + EXIT=${PIPESTATUS[0]} + if [ $EXIT -eq 0 ]; then + echo "All tests passed." >> $GITHUB_STEP_SUMMARY + else + echo "Test failures detected — see log." >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + cat /tmp/test-output.log >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + fi + exit $EXIT + else + echo "No phpunit.xml found — skipping tests." >> $GITHUB_STEP_SUMMARY + fi + + static-analysis: + name: PHPStan Analysis + runs-on: ubuntu-latest + needs: lint-and-validate + continue-on-error: true + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP + run: php -v && composer --version + + - name: Install dependencies + env: + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}' + run: | + if [ -f "composer.json" ]; then + composer install --no-interaction --prefer-dist --optimize-autoloader + fi + + - name: Install PHPStan + run: | + if ! command -v vendor/bin/phpstan &> /dev/null; then + composer require --dev phpstan/phpstan --no-interaction 2>/dev/null || \ + composer global require phpstan/phpstan --no-interaction + fi + + - name: Run PHPStan + run: | + echo "### PHPStan Static Analysis" >> $GITHUB_STEP_SUMMARY + PHPSTAN="vendor/bin/phpstan" + if [ ! -f "$PHPSTAN" ]; then + PHPSTAN=$(composer global config bin-dir --absolute 2>/dev/null)/phpstan + fi + + # Determine source directory + SRC_DIR="" + for DIR in src/ htdocs/ lib/; do + if [ -d "$DIR" ]; then + SRC_DIR="$DIR" + break + fi + done + + if [ -z "$SRC_DIR" ]; then + echo "No source directory found (src/, htdocs/, lib/) — skipping." >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + # Use repo phpstan.neon if present, otherwise use baseline config + ARGS="analyse ${SRC_DIR} --memory-limit=512M --no-progress --error-format=table" + if [ -f "phpstan.neon" ] || [ -f "phpstan.neon.dist" ]; then + echo "Using project PHPStan config." >> $GITHUB_STEP_SUMMARY + else + ARGS="$ARGS --level=3" + echo "No phpstan.neon found — using level 3 (type inference)." >> $GITHUB_STEP_SUMMARY + fi + + $PHPSTAN $ARGS 2>&1 | tee /tmp/phpstan-output.txt + EXIT=${PIPESTATUS[0]} + + if [ $EXIT -eq 0 ]; then + echo "**No errors found.**" >> $GITHUB_STEP_SUMMARY + else + ERRORS=$(grep -c "ERROR" /tmp/phpstan-output.txt 2>/dev/null || echo "some") + echo "**${ERRORS} error(s) found.** Review output above." >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + tail -30 /tmp/phpstan-output.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + fi + exit $EXIT -- 2.52.0 From 1765f752d9e488ed03d2fa22be785e08fc5fb9c5 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:24 +0000 Subject: [PATCH 027/136] chore: remove .gitea/workflows/ci-joomla.yml (moved to .mokogitea/) [skip ci] --- .gitea/workflows/ci-joomla.yml | 450 --------------------------------- 1 file changed, 450 deletions(-) delete mode 100644 .gitea/workflows/ci-joomla.yml diff --git a/.gitea/workflows/ci-joomla.yml b/.gitea/workflows/ci-joomla.yml deleted file mode 100644 index 28cee48..0000000 --- a/.gitea/workflows/ci-joomla.yml +++ /dev/null @@ -1,450 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# This file is part of a Moko Consulting project. -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow.Template -# INGROUP: MokoStandards.CI -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API -# PATH: /templates/workflows/joomla/ci-joomla.yml.template -# VERSION: 04.06.00 -# BRIEF: CI workflow for Joomla extensions — lint, validate, test - -name: Joomla Extension CI - -on: - pull_request: - branches: - - main - - 'dev/**' - workflow_dispatch: - -permissions: - contents: read - pull-requests: write - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - lint-and-validate: - name: Lint & Validate - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Setup PHP - run: | - php -v && composer --version - - - name: Clone MokoStandards - env: - GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} - MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} - MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }} - run: | - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \ - /tmp/mokostandards-api - - - name: Install dependencies - env: - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}' - run: | - if [ -f "composer.json" ]; then - composer install \ - --no-interaction \ - --prefer-dist \ - --optimize-autoloader - else - echo "No composer.json found — skipping dependency install" - fi - - - name: PHP syntax check - run: | - ERRORS=0 - for DIR in src/ htdocs/; do - if [ -d "$DIR" ]; then - FOUND=1 - while IFS= read -r -d '' FILE; do - OUTPUT=$(php -l "$FILE" 2>&1) - if echo "$OUTPUT" | grep -q "Parse error"; then - echo "::error file=${FILE}::${OUTPUT}" - ERRORS=$((ERRORS + 1)) - fi - done < <(find "$DIR" -name "*.php" -print0) - fi - done - echo "### PHP Syntax Check" >> $GITHUB_STEP_SUMMARY - if [ "${ERRORS}" -gt 0 ]; then - echo "**${ERRORS} syntax error(s) found.**" >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY - fi - - - name: XML manifest validation - run: | - echo "### XML Manifest Validation" >> $GITHUB_STEP_SUMMARY - ERRORS=0 - - # Find the extension manifest (XML with /dev/null; then - MANIFEST="$XML_FILE" - break - fi - done - - if [ -z "$MANIFEST" ]; then - echo "No Joomla extension manifest found (XML file with \`> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - echo "Manifest found: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY - - # Validate well-formed XML - php -r " - \$xml = @simplexml_load_file('$MANIFEST'); - if (\$xml === false) { - echo 'INVALID'; - exit(1); - } - echo 'VALID'; - " > /tmp/xml_result 2>&1 - XML_RESULT=$(cat /tmp/xml_result) - if [ "$XML_RESULT" != "VALID" ]; then - echo "Manifest is not well-formed XML." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - echo "Manifest is well-formed XML." >> $GITHUB_STEP_SUMMARY - fi - - # Check required tags: name, version, author, namespace (Joomla 5+) - for TAG in name version author namespace; do - if ! grep -q "<${TAG}>" "$MANIFEST" 2>/dev/null; then - echo "Missing required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - echo "Found required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY - fi - done - fi - - if [ "${ERRORS}" -gt 0 ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "**${ERRORS} manifest issue(s) found.**" >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Manifest validation passed.**" >> $GITHUB_STEP_SUMMARY - fi - - - name: Check language files referenced in manifest - run: | - echo "### Language File Check" >> $GITHUB_STEP_SUMMARY - ERRORS=0 - - MANIFEST="" - for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do - if grep -q "/dev/null; then - MANIFEST="$XML_FILE" - break - fi - done - - if [ -n "$MANIFEST" ]; then - # Extract language file references from manifest - LANG_FILES=$(grep -oP 'language\s+tag="[^"]*"[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true) - if [ -z "$LANG_FILES" ]; then - echo "No language file references found in manifest — skipping." >> $GITHUB_STEP_SUMMARY - else - while IFS= read -r LANG_FILE; do - LANG_FILE=$(echo "$LANG_FILE" | xargs) - if [ -z "$LANG_FILE" ]; then - continue - fi - # Check in common locations - FOUND=0 - for BASE in "." "src" "htdocs"; do - if [ -f "${BASE}/${LANG_FILE}" ]; then - FOUND=1 - break - fi - done - if [ "$FOUND" -eq 0 ]; then - echo "Missing language file: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - echo "Language file present: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY - fi - done <<< "$LANG_FILES" - fi - else - echo "No manifest found — skipping language check." >> $GITHUB_STEP_SUMMARY - fi - - if [ "${ERRORS}" -gt 0 ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "**${ERRORS} missing language file(s).**" >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Language file check passed.**" >> $GITHUB_STEP_SUMMARY - fi - - - name: Check index.html files in directories - run: | - echo "### Index.html Check" >> $GITHUB_STEP_SUMMARY - MISSING=0 - CHECKED=0 - - for DIR in src/ htdocs/; do - if [ -d "$DIR" ]; then - while IFS= read -r -d '' SUBDIR; do - CHECKED=$((CHECKED + 1)) - if [ ! -f "${SUBDIR}/index.html" ]; then - echo "Missing index.html in: \`${SUBDIR}\`" >> $GITHUB_STEP_SUMMARY - MISSING=$((MISSING + 1)) - fi - done < <(find "$DIR" -type d -print0) - fi - done - - if [ "${CHECKED}" -eq 0 ]; then - echo "No src/ or htdocs/ directories found — skipping." >> $GITHUB_STEP_SUMMARY - elif [ "${MISSING}" -gt 0 ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "**${MISSING} director(ies) missing index.html out of ${CHECKED} checked.**" >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "All ${CHECKED} directories contain index.html." >> $GITHUB_STEP_SUMMARY - fi - - release-readiness: - name: Release Readiness Check - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' && github.base_ref == 'main' - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Validate release readiness - run: | - echo "## Release Readiness" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - ERRORS=0 - - # Extract version from README.md - README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md | head -1) - if [ -z "$README_VERSION" ]; then - echo "No VERSION found in README.md FILE INFORMATION block." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - echo "README version: \`${README_VERSION}\`" >> $GITHUB_STEP_SUMMARY - fi - - # Find the extension manifest - MANIFEST="" - for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do - if grep -q "/dev/null; then - MANIFEST="$XML_FILE" - break - fi - done - - if [ -z "$MANIFEST" ]; then - echo "No Joomla extension manifest found." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - echo "Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY - - # Check matches README VERSION - MANIFEST_VERSION=$(grep -oP '\K[^<]+' "$MANIFEST" | head -1) - if [ -z "$MANIFEST_VERSION" ]; then - echo "No \`\` tag in manifest." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - elif [ -n "$README_VERSION" ] && [ "$MANIFEST_VERSION" != "$README_VERSION" ]; then - echo "Manifest version \`${MANIFEST_VERSION}\` does not match README \`${README_VERSION}\`." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - echo "Manifest version: \`${MANIFEST_VERSION}\`" >> $GITHUB_STEP_SUMMARY - fi - - # Check extension type, element, client attributes - EXT_TYPE=$(grep -oP ']*\btype="\K[^"]+' "$MANIFEST" | head -1) - if [ -z "$EXT_TYPE" ]; then - echo "Missing \`type\` attribute on \`\` tag." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - echo "Extension type: \`${EXT_TYPE}\`" >> $GITHUB_STEP_SUMMARY - fi - - # Element check (component/module/plugin name) - HAS_ELEMENT=$(grep -cP '<(element|name)>' "$MANIFEST" 2>/dev/null || echo "0") - if [ "$HAS_ELEMENT" -eq 0 ]; then - echo "Missing \`\` or \`\` in manifest." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - fi - - # Client attribute for site/admin modules and plugins - if echo "$EXT_TYPE" | grep -qP "^(module|plugin)$"; then - HAS_CLIENT=$(grep -cP ']*\bclient=' "$MANIFEST" 2>/dev/null || echo "0") - if [ "$HAS_CLIENT" -eq 0 ]; then - echo "Missing \`client\` attribute for ${EXT_TYPE} extension." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - fi - fi - fi - - # Check updates.xml exists - if [ -f "updates.xml" ] || [ -f "updates.xml" ]; then - echo "Update XML present." >> $GITHUB_STEP_SUMMARY - else - echo "No updates.xml found." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - fi - - # Check CHANGELOG.md exists - if [ -f "CHANGELOG.md" ]; then - echo "CHANGELOG.md present." >> $GITHUB_STEP_SUMMARY - else - echo "No CHANGELOG.md found." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - fi - - echo "" >> $GITHUB_STEP_SUMMARY - if [ $ERRORS -gt 0 ]; then - echo "**${ERRORS} issue(s) must be resolved before release.**" >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "**Extension is ready for release.**" >> $GITHUB_STEP_SUMMARY - fi - - test: - name: Tests (PHP ${{ matrix.php }}) - runs-on: ubuntu-latest - needs: lint-and-validate - - strategy: - fail-fast: false - matrix: - php: ['8.2', '8.3'] - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Setup PHP ${{ matrix.php }} - run: | - php -v && composer --version - - - name: Install dependencies - env: - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}' - run: | - if [ -f "composer.json" ]; then - composer install \ - --no-interaction \ - --prefer-dist \ - --optimize-autoloader - else - echo "No composer.json found — skipping dependency install" - fi - - - name: Run tests - run: | - echo "### Test Results (PHP ${{ matrix.php }})" >> $GITHUB_STEP_SUMMARY - if [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then - vendor/bin/phpunit --testdox 2>&1 | tee /tmp/test-output.log - EXIT=${PIPESTATUS[0]} - if [ $EXIT -eq 0 ]; then - echo "All tests passed." >> $GITHUB_STEP_SUMMARY - else - echo "Test failures detected — see log." >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - cat /tmp/test-output.log >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - fi - exit $EXIT - else - echo "No phpunit.xml found — skipping tests." >> $GITHUB_STEP_SUMMARY - fi - - static-analysis: - name: PHPStan Analysis - runs-on: ubuntu-latest - needs: lint-and-validate - continue-on-error: true - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Setup PHP - run: php -v && composer --version - - - name: Install dependencies - env: - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}' - run: | - if [ -f "composer.json" ]; then - composer install --no-interaction --prefer-dist --optimize-autoloader - fi - - - name: Install PHPStan - run: | - if ! command -v vendor/bin/phpstan &> /dev/null; then - composer require --dev phpstan/phpstan --no-interaction 2>/dev/null || \ - composer global require phpstan/phpstan --no-interaction - fi - - - name: Run PHPStan - run: | - echo "### PHPStan Static Analysis" >> $GITHUB_STEP_SUMMARY - PHPSTAN="vendor/bin/phpstan" - if [ ! -f "$PHPSTAN" ]; then - PHPSTAN=$(composer global config bin-dir --absolute 2>/dev/null)/phpstan - fi - - # Determine source directory - SRC_DIR="" - for DIR in src/ htdocs/ lib/; do - if [ -d "$DIR" ]; then - SRC_DIR="$DIR" - break - fi - done - - if [ -z "$SRC_DIR" ]; then - echo "No source directory found (src/, htdocs/, lib/) — skipping." >> $GITHUB_STEP_SUMMARY - exit 0 - fi - - # Use repo phpstan.neon if present, otherwise use baseline config - ARGS="analyse ${SRC_DIR} --memory-limit=512M --no-progress --error-format=table" - if [ -f "phpstan.neon" ] || [ -f "phpstan.neon.dist" ]; then - echo "Using project PHPStan config." >> $GITHUB_STEP_SUMMARY - else - ARGS="$ARGS --level=3" - echo "No phpstan.neon found — using level 3 (type inference)." >> $GITHUB_STEP_SUMMARY - fi - - $PHPSTAN $ARGS 2>&1 | tee /tmp/phpstan-output.txt - EXIT=${PIPESTATUS[0]} - - if [ $EXIT -eq 0 ]; then - echo "**No errors found.**" >> $GITHUB_STEP_SUMMARY - else - ERRORS=$(grep -c "ERROR" /tmp/phpstan-output.txt 2>/dev/null || echo "some") - echo "**${ERRORS} error(s) found.** Review output above." >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - tail -30 /tmp/phpstan-output.txt >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - fi - exit $EXIT -- 2.52.0 From eb4611d57e6330d3746b18eb17f6a4f3961d008e Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:25 +0000 Subject: [PATCH 028/136] chore: move .gitea/workflows/cleanup.yml to .mokogitea/cleanup.yml [skip ci] --- .mokogitea/cleanup.yml | 87 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 .mokogitea/cleanup.yml diff --git a/.mokogitea/cleanup.yml b/.mokogitea/cleanup.yml new file mode 100644 index 0000000..78aa0c3 --- /dev/null +++ b/.mokogitea/cleanup.yml @@ -0,0 +1,87 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Maintenance +# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards +# PATH: /.gitea/workflows/cleanup.yml +# VERSION: 01.00.00 +# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs + +name: Repository Cleanup + +on: + schedule: + - cron: '0 3 * * 0' # Weekly on Sunday at 03:00 UTC + workflow_dispatch: + +permissions: + contents: write + +env: + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + +jobs: + cleanup: + name: Clean Merged Branches + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GA_TOKEN }} + + - name: Delete merged branches + env: + GA_TOKEN: ${{ secrets.GA_TOKEN }} + run: | + echo "=== Merged Branch Cleanup ===" + API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" + + # List branches via API + BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \ + "${API}/branches?limit=50" | jq -r '.[].name') + + DELETED=0 + for BRANCH in $BRANCHES; do + # Skip protected branches + case "$BRANCH" in + main|master|develop|release/*|hotfix/*) continue ;; + esac + + # 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}" \ + "${API}/branches/${BRANCH}" 2>/dev/null || true + DELETED=$((DELETED + 1)) + fi + done + + echo "Deleted ${DELETED} merged branch(es)" + + - name: Clean old workflow runs + env: + GA_TOKEN: ${{ secrets.GA_TOKEN }} + run: | + echo "=== Workflow Run Cleanup ===" + API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" + CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ) + + # Get old completed runs + RUNS=$(curl -sS -H "Authorization: token ${GA_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}" \ + "${API}/actions/runs/${RUN_ID}" 2>/dev/null || true + DELETED=$((DELETED + 1)) + done + + echo "Deleted ${DELETED} old workflow run(s)" -- 2.52.0 From 602a0f9b778e8189156c78fd5605bb15c08e2ece Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:25 +0000 Subject: [PATCH 029/136] chore: remove .gitea/workflows/cleanup.yml (moved to .mokogitea/) [skip ci] --- .gitea/workflows/cleanup.yml | 87 ------------------------------------ 1 file changed, 87 deletions(-) delete mode 100644 .gitea/workflows/cleanup.yml diff --git a/.gitea/workflows/cleanup.yml b/.gitea/workflows/cleanup.yml deleted file mode 100644 index 78aa0c3..0000000 --- a/.gitea/workflows/cleanup.yml +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: MokoStandards.Maintenance -# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards -# PATH: /.gitea/workflows/cleanup.yml -# VERSION: 01.00.00 -# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs - -name: Repository Cleanup - -on: - schedule: - - cron: '0 3 * * 0' # Weekly on Sunday at 03:00 UTC - workflow_dispatch: - -permissions: - contents: write - -env: - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - -jobs: - cleanup: - name: Clean Merged Branches - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.GA_TOKEN }} - - - name: Delete merged branches - env: - GA_TOKEN: ${{ secrets.GA_TOKEN }} - run: | - echo "=== Merged Branch Cleanup ===" - API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" - - # List branches via API - BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \ - "${API}/branches?limit=50" | jq -r '.[].name') - - DELETED=0 - for BRANCH in $BRANCHES; do - # Skip protected branches - case "$BRANCH" in - main|master|develop|release/*|hotfix/*) continue ;; - esac - - # 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}" \ - "${API}/branches/${BRANCH}" 2>/dev/null || true - DELETED=$((DELETED + 1)) - fi - done - - echo "Deleted ${DELETED} merged branch(es)" - - - name: Clean old workflow runs - env: - GA_TOKEN: ${{ secrets.GA_TOKEN }} - run: | - echo "=== Workflow Run Cleanup ===" - API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" - CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ) - - # Get old completed runs - RUNS=$(curl -sS -H "Authorization: token ${GA_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}" \ - "${API}/actions/runs/${RUN_ID}" 2>/dev/null || true - DELETED=$((DELETED + 1)) - done - - echo "Deleted ${DELETED} old workflow run(s)" -- 2.52.0 From 293674298c055b56f5821a1168abe42acfd2b87f Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:26 +0000 Subject: [PATCH 030/136] chore: move .gitea/workflows/deploy-manual.yml to .mokogitea/deploy-manual.yml [skip ci] --- .mokogitea/deploy-manual.yml | 126 +++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 .mokogitea/deploy-manual.yml diff --git a/.mokogitea/deploy-manual.yml b/.mokogitea/deploy-manual.yml new file mode 100644 index 0000000..a81cfa5 --- /dev/null +++ b/.mokogitea/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: 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: + GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} + MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} + MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }} + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_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 -- 2.52.0 From ccd166a6c9128f003a96e3770cad5222422e97a0 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:26 +0000 Subject: [PATCH 031/136] chore: remove .gitea/workflows/deploy-manual.yml (moved to .mokogitea/) [skip ci] --- .gitea/workflows/deploy-manual.yml | 126 ----------------------------- 1 file changed, 126 deletions(-) delete mode 100644 .gitea/workflows/deploy-manual.yml diff --git a/.gitea/workflows/deploy-manual.yml b/.gitea/workflows/deploy-manual.yml deleted file mode 100644 index a81cfa5..0000000 --- a/.gitea/workflows/deploy-manual.yml +++ /dev/null @@ -1,126 +0,0 @@ -# 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: 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: - GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} - MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} - MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }} - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_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 -- 2.52.0 From d430eb90c0253e6158ad19ad5f6764fbc36e43da Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:26 +0000 Subject: [PATCH 032/136] chore: move .gitea/workflows/gitleaks.yml to .mokogitea/gitleaks.yml [skip ci] --- .mokogitea/gitleaks.yml | 96 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 .mokogitea/gitleaks.yml diff --git a/.mokogitea/gitleaks.yml b/.mokogitea/gitleaks.yml new file mode 100644 index 0000000..b29f881 --- /dev/null +++ b/.mokogitea/gitleaks.yml @@ -0,0 +1,96 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Security +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# PATH: /templates/workflows/gitleaks.yml.template +# VERSION: 01.00.00 +# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens +# +# +========================================================================+ +# | SECRET SCANNING | +# +========================================================================+ +# | | +# | Scans commits for leaked secrets using Gitleaks. | +# | | +# | - PR scan: only new commits in the PR | +# | - Scheduled: full repo scan weekly | +# | - Alerts via ntfy on findings | +# | | +# +========================================================================+ + +name: Secret Scanning + +on: + pull_request: + branches: + - main + - 'dev/**' + schedule: + - cron: '0 5 * * 1' # Weekly Monday 05:00 UTC + workflow_dispatch: + +permissions: + contents: read + +env: + NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }} + NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }} + +jobs: + gitleaks: + name: Gitleaks 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 + gitleaks version + + - name: Scan for secrets + id: scan + run: | + echo "### Secret Scanning" >> $GITHUB_STEP_SUMMARY + ARGS="--source . --verbose --report-format json --report-path /tmp/gitleaks-report.json" + + if [ "${{ github.event_name }}" = "pull_request" ]; then + # Scan only PR commits + ARGS="$ARGS --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}" + echo "Scanning PR commits only" >> $GITHUB_STEP_SUMMARY + else + echo "Full repository scan" >> $GITHUB_STEP_SUMMARY + fi + + if gitleaks detect $ARGS 2>&1; then + echo "result=clean" >> "$GITHUB_OUTPUT" + echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY + else + echo "result=found" >> "$GITHUB_OUTPUT" + FINDINGS=$(jq length /tmp/gitleaks-report.json 2>/dev/null || echo "unknown") + echo "**${FINDINGS} potential secret(s) detected.**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Review the findings and rotate any exposed credentials immediately." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + - name: Notify on findings + if: failure() && steps.scan.outputs.result == 'found' + run: | + REPO="${{ github.event.repository.name }}" + curl -sS \ + -H "Title: ${REPO} — secrets detected in code" \ + -H "Tags: rotating_light,key" \ + -H "Priority: urgent" \ + -d "Gitleaks found potential secrets. Review and rotate credentials immediately." \ + "${NTFY_URL}/${NTFY_TOPIC}" || true -- 2.52.0 From 177b0d5c010fe79712a82905a37f99bcfcfa3930 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:27 +0000 Subject: [PATCH 033/136] chore: remove .gitea/workflows/gitleaks.yml (moved to .mokogitea/) [skip ci] --- .gitea/workflows/gitleaks.yml | 96 ----------------------------------- 1 file changed, 96 deletions(-) delete mode 100644 .gitea/workflows/gitleaks.yml diff --git a/.gitea/workflows/gitleaks.yml b/.gitea/workflows/gitleaks.yml deleted file mode 100644 index b29f881..0000000 --- a/.gitea/workflows/gitleaks.yml +++ /dev/null @@ -1,96 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: MokoStandards.Security -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API -# PATH: /templates/workflows/gitleaks.yml.template -# VERSION: 01.00.00 -# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens -# -# +========================================================================+ -# | SECRET SCANNING | -# +========================================================================+ -# | | -# | Scans commits for leaked secrets using Gitleaks. | -# | | -# | - PR scan: only new commits in the PR | -# | - Scheduled: full repo scan weekly | -# | - Alerts via ntfy on findings | -# | | -# +========================================================================+ - -name: Secret Scanning - -on: - pull_request: - branches: - - main - - 'dev/**' - schedule: - - cron: '0 5 * * 1' # Weekly Monday 05:00 UTC - workflow_dispatch: - -permissions: - contents: read - -env: - NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }} - NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }} - -jobs: - gitleaks: - name: Gitleaks 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 - gitleaks version - - - name: Scan for secrets - id: scan - run: | - echo "### Secret Scanning" >> $GITHUB_STEP_SUMMARY - ARGS="--source . --verbose --report-format json --report-path /tmp/gitleaks-report.json" - - if [ "${{ github.event_name }}" = "pull_request" ]; then - # Scan only PR commits - ARGS="$ARGS --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}" - echo "Scanning PR commits only" >> $GITHUB_STEP_SUMMARY - else - echo "Full repository scan" >> $GITHUB_STEP_SUMMARY - fi - - if gitleaks detect $ARGS 2>&1; then - echo "result=clean" >> "$GITHUB_OUTPUT" - echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY - else - echo "result=found" >> "$GITHUB_OUTPUT" - FINDINGS=$(jq length /tmp/gitleaks-report.json 2>/dev/null || echo "unknown") - echo "**${FINDINGS} potential secret(s) detected.**" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Review the findings and rotate any exposed credentials immediately." >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - - name: Notify on findings - if: failure() && steps.scan.outputs.result == 'found' - run: | - REPO="${{ github.event.repository.name }}" - curl -sS \ - -H "Title: ${REPO} — secrets detected in code" \ - -H "Tags: rotating_light,key" \ - -H "Priority: urgent" \ - -d "Gitleaks found potential secrets. Review and rotate credentials immediately." \ - "${NTFY_URL}/${NTFY_TOPIC}" || true -- 2.52.0 From d409edd355006fc932f0ba1ec91cf67e9f3d504d Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:27 +0000 Subject: [PATCH 034/136] chore: move .gitea/workflows/notify.yml to .mokogitea/notify.yml [skip ci] --- .mokogitea/notify.yml | 71 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 .mokogitea/notify.yml diff --git a/.mokogitea/notify.yml b/.mokogitea/notify.yml new file mode 100644 index 0000000..8cc8382 --- /dev/null +++ b/.mokogitea/notify.yml @@ -0,0 +1,71 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Notifications +# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards +# PATH: /.gitea/workflows/notify.yml +# VERSION: 01.00.00 +# BRIEF: Push notifications via ntfy on release success or workflow failure + +name: Notifications + +on: + workflow_run: + workflows: + - "Joomla Build & Release" + - "Joomla Extension CI" + - "Deploy" + - "Cascade Main → Dev" + types: + - completed + +permissions: + contents: read + +env: + NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }} + NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-releases' }} + +jobs: + notify: + name: Send Notification + runs-on: ubuntu-latest + if: >- + github.event.workflow_run.conclusion == 'success' || + github.event.workflow_run.conclusion == 'failure' + + steps: + - name: Notify on success (releases only) + if: >- + github.event.workflow_run.conclusion == 'success' && + contains(github.event.workflow_run.name, 'Release') + run: | + REPO="${{ github.event.repository.name }}" + WORKFLOW="${{ github.event.workflow_run.name }}" + URL="${{ github.event.workflow_run.html_url }}" + + curl -sS \ + -H "Title: ${REPO} released" \ + -H "Tags: white_check_mark,package" \ + -H "Priority: default" \ + -H "Click: ${URL}" \ + -d "${WORKFLOW} completed successfully." \ + "${NTFY_URL}/${NTFY_TOPIC}" + + - name: Notify on failure + if: github.event.workflow_run.conclusion == 'failure' + run: | + REPO="${{ github.event.repository.name }}" + WORKFLOW="${{ github.event.workflow_run.name }}" + URL="${{ github.event.workflow_run.html_url }}" + + curl -sS \ + -H "Title: ${REPO} workflow failed" \ + -H "Tags: x,warning" \ + -H "Priority: high" \ + -H "Click: ${URL}" \ + -d "${WORKFLOW} failed. Check the run for details." \ + "${NTFY_URL}/${NTFY_TOPIC}" -- 2.52.0 From fd74cbe535bd8acb0a4c157e653d51d29ddb8fcc Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:27 +0000 Subject: [PATCH 035/136] chore: remove .gitea/workflows/notify.yml (moved to .mokogitea/) [skip ci] --- .gitea/workflows/notify.yml | 71 ------------------------------------- 1 file changed, 71 deletions(-) delete mode 100644 .gitea/workflows/notify.yml diff --git a/.gitea/workflows/notify.yml b/.gitea/workflows/notify.yml deleted file mode 100644 index 8cc8382..0000000 --- a/.gitea/workflows/notify.yml +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: MokoStandards.Notifications -# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards -# PATH: /.gitea/workflows/notify.yml -# VERSION: 01.00.00 -# BRIEF: Push notifications via ntfy on release success or workflow failure - -name: Notifications - -on: - workflow_run: - workflows: - - "Joomla Build & Release" - - "Joomla Extension CI" - - "Deploy" - - "Cascade Main → Dev" - types: - - completed - -permissions: - contents: read - -env: - NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }} - NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-releases' }} - -jobs: - notify: - name: Send Notification - runs-on: ubuntu-latest - if: >- - github.event.workflow_run.conclusion == 'success' || - github.event.workflow_run.conclusion == 'failure' - - steps: - - name: Notify on success (releases only) - if: >- - github.event.workflow_run.conclusion == 'success' && - contains(github.event.workflow_run.name, 'Release') - run: | - REPO="${{ github.event.repository.name }}" - WORKFLOW="${{ github.event.workflow_run.name }}" - URL="${{ github.event.workflow_run.html_url }}" - - curl -sS \ - -H "Title: ${REPO} released" \ - -H "Tags: white_check_mark,package" \ - -H "Priority: default" \ - -H "Click: ${URL}" \ - -d "${WORKFLOW} completed successfully." \ - "${NTFY_URL}/${NTFY_TOPIC}" - - - name: Notify on failure - if: github.event.workflow_run.conclusion == 'failure' - run: | - REPO="${{ github.event.repository.name }}" - WORKFLOW="${{ github.event.workflow_run.name }}" - URL="${{ github.event.workflow_run.html_url }}" - - curl -sS \ - -H "Title: ${REPO} workflow failed" \ - -H "Tags: x,warning" \ - -H "Priority: high" \ - -H "Click: ${URL}" \ - -d "${WORKFLOW} failed. Check the run for details." \ - "${NTFY_URL}/${NTFY_TOPIC}" -- 2.52.0 From 6051cdb14896ef4cfb6903c8e9143fd9ae662b64 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:28 +0000 Subject: [PATCH 036/136] chore: move .gitea/workflows/pr-branch-check.yml to .mokogitea/pr-branch-check.yml [skip ci] --- .mokogitea/pr-branch-check.yml | 90 ++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 .mokogitea/pr-branch-check.yml diff --git a/.mokogitea/pr-branch-check.yml b/.mokogitea/pr-branch-check.yml new file mode 100644 index 0000000..b8d9742 --- /dev/null +++ b/.mokogitea/pr-branch-check.yml @@ -0,0 +1,90 @@ +# Copyright (C) 2026 Moko Consulting +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Enforces branch merge policy: +# feature/* → dev only +# fix/* → dev only +# hotfix/* → dev or main (emergency) +# dev → main only +# alpha/* → dev only +# beta/* → dev only +# rc/* → main only + +name: Branch Policy Check + +on: + pull_request: + types: [opened, synchronize, reopened, edited] + +jobs: + check-target: + name: Verify merge target + runs-on: ubuntu-latest + steps: + - name: Check branch policy + 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 + ;; + hotfix/*) + if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'" + fi + ;; + alpha/*|beta/*) + if [ "$BASE" != "dev" ]; then + ALLOWED=false + REASON="Pre-release branches must target 'dev', not '${BASE}'" + fi + ;; + rc/*) + if [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Release candidate branches must target '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 "" + 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 -- 2.52.0 From 95de7a5e8afc7cdc6e4ea9990ab6f2ad1fef6f00 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:28 +0000 Subject: [PATCH 037/136] chore: remove .gitea/workflows/pr-branch-check.yml (moved to .mokogitea/) [skip ci] --- .gitea/workflows/pr-branch-check.yml | 90 ---------------------------- 1 file changed, 90 deletions(-) delete mode 100644 .gitea/workflows/pr-branch-check.yml diff --git a/.gitea/workflows/pr-branch-check.yml b/.gitea/workflows/pr-branch-check.yml deleted file mode 100644 index b8d9742..0000000 --- a/.gitea/workflows/pr-branch-check.yml +++ /dev/null @@ -1,90 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# SPDX-License-Identifier: GPL-3.0-or-later -# -# Enforces branch merge policy: -# feature/* → dev only -# fix/* → dev only -# hotfix/* → dev or main (emergency) -# dev → main only -# alpha/* → dev only -# beta/* → dev only -# rc/* → main only - -name: Branch Policy Check - -on: - pull_request: - types: [opened, synchronize, reopened, edited] - -jobs: - check-target: - name: Verify merge target - runs-on: ubuntu-latest - steps: - - name: Check branch policy - 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 - ;; - hotfix/*) - if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then - ALLOWED=false - REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'" - fi - ;; - alpha/*|beta/*) - if [ "$BASE" != "dev" ]; then - ALLOWED=false - REASON="Pre-release branches must target 'dev', not '${BASE}'" - fi - ;; - rc/*) - if [ "$BASE" != "main" ]; then - ALLOWED=false - REASON="Release candidate branches must target '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 "" - 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 -- 2.52.0 From 7b1d38f2c7b6aa36b970dd7279a277fde663f718 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:29 +0000 Subject: [PATCH 038/136] chore: move .gitea/workflows/pr-check.yml to .mokogitea/pr-check.yml [skip ci] --- .mokogitea/pr-check.yml | 106 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 .mokogitea/pr-check.yml diff --git a/.mokogitea/pr-check.yml b/.mokogitea/pr-check.yml new file mode 100644 index 0000000..0220500 --- /dev/null +++ b/.mokogitea/pr-check.yml @@ -0,0 +1,106 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.CI +# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards +# PATH: /.gitea/workflows/pr-check.yml +# VERSION: 01.00.00 +# BRIEF: PR gate — validates code quality and manifest before merge to main + +name: PR Check + +on: + pull_request: + branches: + - main + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: write + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + validate: + name: Validate PR + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + run: | + if ! command -v php &> /dev/null; then + sudo apt-get update -qq + sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1 + fi + + - name: PHP syntax check + run: | + echo "=== PHP Lint ===" + 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 "Checked files, errors: ${ERRORS}" + [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; } + + - name: Validate Joomla manifest + run: | + echo "=== Manifest Validation ===" + 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" + exit 0 + fi + echo "Manifest: ${MANIFEST}" + + # Check well-formed XML + if ! 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);}"; then + echo "::error::Manifest XML is malformed" + exit 1 + fi + + # Check required elements + for ELEMENT in name version description; do + if ! grep -q "<${ELEMENT}>" "$MANIFEST"; then + echo "::error::Missing <${ELEMENT}> in manifest" + exit 1 + fi + done + echo "Manifest valid" + + - name: Check updates.xml format + run: | + if [ ! -f "updates.xml" ]; then + echo "No updates.xml — skipping" + exit 0 + fi + echo "=== updates.xml Validation ===" + if ! 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);}"; then + echo "::error::updates.xml is malformed" + exit 1 + fi + echo "updates.xml valid" + + - name: Verify package builds + run: | + echo "=== Package Build Test ===" + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + if [ ! -d "$SOURCE_DIR" ]; then + echo "::warning::No src/ or htdocs/ directory" + exit 0 + fi + # Dry-run: ensure zip would succeed + FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l) + echo "Source contains ${FILE_COUNT} files — package will build" + [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; } -- 2.52.0 From de7032fc1686fab27e7758c3926502a4b13814f4 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:29 +0000 Subject: [PATCH 039/136] chore: remove .gitea/workflows/pr-check.yml (moved to .mokogitea/) [skip ci] --- .gitea/workflows/pr-check.yml | 106 ---------------------------------- 1 file changed, 106 deletions(-) delete mode 100644 .gitea/workflows/pr-check.yml diff --git a/.gitea/workflows/pr-check.yml b/.gitea/workflows/pr-check.yml deleted file mode 100644 index 0220500..0000000 --- a/.gitea/workflows/pr-check.yml +++ /dev/null @@ -1,106 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: MokoStandards.CI -# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards -# PATH: /.gitea/workflows/pr-check.yml -# VERSION: 01.00.00 -# BRIEF: PR gate — validates code quality and manifest before merge to main - -name: PR Check - -on: - pull_request: - branches: - - main - types: [opened, synchronize, reopened] - -permissions: - contents: read - pull-requests: write - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - validate: - name: Validate PR - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup PHP - run: | - if ! command -v php &> /dev/null; then - sudo apt-get update -qq - sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1 - fi - - - name: PHP syntax check - run: | - echo "=== PHP Lint ===" - 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 "Checked files, errors: ${ERRORS}" - [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; } - - - name: Validate Joomla manifest - run: | - echo "=== Manifest Validation ===" - 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" - exit 0 - fi - echo "Manifest: ${MANIFEST}" - - # Check well-formed XML - if ! 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);}"; then - echo "::error::Manifest XML is malformed" - exit 1 - fi - - # Check required elements - for ELEMENT in name version description; do - if ! grep -q "<${ELEMENT}>" "$MANIFEST"; then - echo "::error::Missing <${ELEMENT}> in manifest" - exit 1 - fi - done - echo "Manifest valid" - - - name: Check updates.xml format - run: | - if [ ! -f "updates.xml" ]; then - echo "No updates.xml — skipping" - exit 0 - fi - echo "=== updates.xml Validation ===" - if ! 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);}"; then - echo "::error::updates.xml is malformed" - exit 1 - fi - echo "updates.xml valid" - - - name: Verify package builds - run: | - echo "=== Package Build Test ===" - SOURCE_DIR="src" - [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" - if [ ! -d "$SOURCE_DIR" ]; then - echo "::warning::No src/ or htdocs/ directory" - exit 0 - fi - # Dry-run: ensure zip would succeed - FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l) - echo "Source contains ${FILE_COUNT} files — package will build" - [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; } -- 2.52.0 From 140a9524a33ea0fb0b721648f953febb4dbff666 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:29 +0000 Subject: [PATCH 040/136] chore: move .gitea/workflows/pre-release.yml to .mokogitea/pre-release.yml [skip ci] --- .mokogitea/pre-release.yml | 341 +++++++++++++++++++++++++++++++++++++ 1 file changed, 341 insertions(+) create mode 100644 .mokogitea/pre-release.yml diff --git a/.mokogitea/pre-release.yml b/.mokogitea/pre-release.yml new file mode 100644 index 0000000..30c9bcf --- /dev/null +++ b/.mokogitea/pre-release.yml @@ -0,0 +1,341 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Release +# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards +# PATH: /.gitea/workflows/pre-release.yml +# VERSION: 01.00.00 +# BRIEF: Manual pre-release — builds dev/alpha/beta/rc packages from any branch + +name: Pre-Release + +on: + workflow_dispatch: + inputs: + stability: + description: 'Pre-release channel' + required: true + type: choice + options: + - development + - alpha + - beta + - release-candidate + +permissions: + contents: write + +env: + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +jobs: + build: + name: "Build Pre-Release (${{ inputs.stability }})" + runs-on: release + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GA_TOKEN }} + + - name: Setup PHP + run: | + if ! command -v php &> /dev/null; then + sudo apt-get update -qq + sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip >/dev/null 2>&1 + fi + + - name: Resolve metadata + id: meta + run: | + STABILITY="${{ inputs.stability }}" + + case "$STABILITY" in + development) SUFFIX="-dev"; TAG="development" ;; + alpha) SUFFIX="-alpha"; TAG="alpha" ;; + beta) SUFFIX="-beta"; TAG="beta" ;; + release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; + esac + + # Read and bump patch version (with rollover) + CURRENT=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1) + [ -z "$CURRENT" ] && CURRENT="00.00.00" + + MAJOR=$(echo "$CURRENT" | cut -d. -f1) + MINOR=$(echo "$CURRENT" | cut -d. -f2) + PATCH=$(echo "$CURRENT" | cut -d. -f3) + + # Patch bump with rollover: ZZ=99 → bump minor, YY=99 → bump major + NEW_PATCH=$((10#$PATCH + 1)) + NEW_MINOR=$((10#$MINOR)) + NEW_MAJOR=$((10#$MAJOR)) + + if [ $NEW_PATCH -gt 99 ]; then + NEW_PATCH=0 + NEW_MINOR=$((NEW_MINOR + 1)) + fi + if [ $NEW_MINOR -gt 99 ]; then + NEW_MINOR=0 + NEW_MAJOR=$((NEW_MAJOR + 1)) + fi + + VERSION=$(printf "%02d.%02d.%02d" $NEW_MAJOR $NEW_MINOR $NEW_PATCH) + TODAY=$(date +%Y-%m-%d) + + echo "Bumping: ${CURRENT} → ${VERSION} (patch)" + + # Update README.md + sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${VERSION}/" README.md + + # Update manifest + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) + if [ -n "$MANIFEST" ]; then + MANIFEST_VER=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1) + sed -i "s|${MANIFEST_VER}|${VERSION}|" "$MANIFEST" + sed -i "s|[^<]*|${TODAY}|" "$MANIFEST" + fi + + # Commit version bump + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git add -A + git diff --cached --quiet || { + git commit -m "chore(version): bump ${CURRENT} → ${VERSION} [skip ci]" + git push origin HEAD 2>&1 + } + + # Auto-detect element from manifest + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) + EXT_ELEMENT="" + if [ -n "$MANIFEST" ]; then + EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1) + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') + case "$EXT_ELEMENT" in + templatedetails|manifest) EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;; + esac + fi + else + EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + fi + + ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip" + + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" + echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" + echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" + echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT" + + echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ===" + + - name: Build package + run: | + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + if [ ! -d "$SOURCE_DIR" ]; then + echo "::error::No src/ or htdocs/ directory" + exit 1 + fi + + mkdir -p build/package + rsync -a \ + --exclude='sftp-config*' \ + --exclude='.ftpignore' \ + --exclude='*.ppk' \ + --exclude='*.pem' \ + --exclude='*.key' \ + --exclude='.env*' \ + --exclude='*.local' \ + --exclude='.build-trigger' \ + "${SOURCE_DIR}/" build/package/ + + - name: Create ZIP + id: zip + run: | + ZIP_NAME="${{ steps.meta.outputs.zip_name }}" + cd build/package + zip -r "../${ZIP_NAME}" . + cd .. + + SHA256=$(sha256sum "${ZIP_NAME}" | cut -d' ' -f1) + echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT" + echo "ZIP: ${ZIP_NAME} (SHA: ${SHA256:0:16}...)" + + - name: Create or replace Gitea release + id: release + run: | + TAG="${{ steps.meta.outputs.tag }}" + VERSION="${{ steps.meta.outputs.version }}" + STABILITY="${{ steps.meta.outputs.stability }}" + SHA256="${{ steps.zip.outputs.sha256 }}" + ZIP_NAME="${{ steps.meta.outputs.zip_name }}" + EXT_ELEMENT="${{ steps.meta.outputs.ext_element }}" + TOKEN="${{ secrets.GA_TOKEN }}" + API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + BRANCH=$(git branch --show-current) + + BODY="## ${VERSION} ($(date +%Y-%m-%d)) + **Channel:** ${STABILITY} + **SHA-256:** \`${SHA256}\`" + + # Delete existing release + EXISTING_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \ + "${API}/releases/tags/${TAG}" | jq -r '.id // empty' 2>/dev/null) + if [ -n "$EXISTING_ID" ]; then + curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API}/releases/${EXISTING_ID}" 2>/dev/null || true + curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API}/tags/${TAG}" 2>/dev/null || true + fi + + # Create release + RELEASE_ID=$(curl -sS -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/releases" \ + -d "$(jq -n \ + --arg tag "$TAG" \ + --arg target "$BRANCH" \ + --arg name "${EXT_ELEMENT} ${VERSION} (${STABILITY})" \ + --arg body "$BODY" \ + '{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: true}' + )" | jq -r '.id') + + echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT" + + # Upload ZIP + curl -sS -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/octet-stream" \ + "${API}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \ + --data-binary "@build/${ZIP_NAME}" + + echo "Released: ${EXT_ELEMENT} ${VERSION} (${STABILITY})" + + - name: Update updates.xml + run: | + STABILITY="${{ steps.meta.outputs.stability }}" + VERSION="${{ steps.meta.outputs.version }}" + SHA256="${{ steps.zip.outputs.sha256 }}" + ZIP_NAME="${{ steps.meta.outputs.zip_name }}" + TAG="${{ steps.meta.outputs.tag }}" + DATE=$(date +%Y-%m-%d) + + if [ ! -f "updates.xml" ]; then + echo "No updates.xml — skipping" + exit 0 + fi + + export PY_STABILITY="$STABILITY" PY_VERSION="$VERSION" PY_SHA256="$SHA256" \ + PY_ZIP_NAME="$ZIP_NAME" PY_TAG="$TAG" PY_DATE="$DATE" \ + PY_GITEA_ORG="$GITEA_ORG" PY_GITEA_REPO="$GITEA_REPO" + python3 << 'PYEOF' + import re, os + + stability = os.environ["PY_STABILITY"] + version = os.environ["PY_VERSION"] + sha256 = os.environ["PY_SHA256"] + zip_name = os.environ["PY_ZIP_NAME"] + tag = os.environ["PY_TAG"] + date = os.environ["PY_DATE"] + gitea_org = os.environ["PY_GITEA_ORG"] + gitea_repo = os.environ["PY_GITEA_REPO"] + download_url = f"https://git.mokoconsulting.tech/{gitea_org}/{gitea_repo}/releases/download/{tag}/{zip_name}" + + with open("updates.xml", "r") as f: + content = f.read() + + # Map stability to XML tag name + tag_map = {"development": "development", "alpha": "alpha", "beta": "beta", "release-candidate": "rc"} + xml_tag = tag_map.get(stability, stability) + + pattern = r"((?:(?!).)*?" + re.escape(xml_tag) + r".*?)" + match = re.search(pattern, content, re.DOTALL) + if match: + block = match.group(1) + updated = re.sub(r"[^<]*", f"{version}", block) + updated = re.sub(r"[^<]*", f"{date}", updated) + if "" in updated: + updated = re.sub(r"[^<]*", f"{sha256}", updated) + else: + updated = updated.replace("", f"\n {sha256}") + updated = re.sub(r"(]*>)[^<]*()", rf"\g<1>{download_url}\g<2>", updated) + content = content.replace(block, updated) + print(f"Updated {xml_tag} channel: version={version}") + else: + print(f"WARNING: No {xml_tag} block in updates.xml") + + with open("updates.xml", "w") as f: + f.write(content) + PYEOF + + # Commit and push to current branch + if ! git diff --quiet updates.xml 2>/dev/null; then + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git add updates.xml + git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]" + git push origin HEAD 2>&1 || echo "WARNING: push failed" + fi + + - name: "Sync updates.xml to all branches" + run: | + CURRENT_BRANCH="${{ github.ref_name }}" + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + + # Sync updates.xml to main and dev (whichever isn't current) + for BRANCH in main dev; do + [ "$BRANCH" = "$CURRENT_BRANCH" ] && continue + + echo "Syncing updates.xml → ${BRANCH}" + git fetch origin "${BRANCH}" 2>/dev/null || continue + git checkout "origin/${BRANCH}" -- . 2>/dev/null || continue + git checkout "${CURRENT_BRANCH}" -- updates.xml + if ! git diff --quiet updates.xml 2>/dev/null; then + git add updates.xml + git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]" + git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed" + fi + git checkout "${CURRENT_BRANCH}" 2>/dev/null + done + + - name: "Delete lesser pre-release channels (cascade)" + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.GA_TOKEN }}" + STABILITY="${{ steps.meta.outputs.stability }}" + + # Cascade: rc → beta,alpha,dev | beta → alpha,dev | alpha → dev | dev → nothing + case "$STABILITY" in + release-candidate) TAGS_TO_DELETE="beta alpha development" ;; + beta) TAGS_TO_DELETE="alpha development" ;; + alpha) TAGS_TO_DELETE="development" ;; + *) TAGS_TO_DELETE="" ;; + esac + + [ -z "$TAGS_TO_DELETE" ] && exit 0 + + for TAG in $TAGS_TO_DELETE; do + RELEASE_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/releases/tags/${TAG}" 2>/dev/null | \ + python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + + if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then + curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/releases/${RELEASE_ID}" 2>/dev/null || true + curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/tags/${TAG}" 2>/dev/null || true + echo "Deleted: ${TAG} (id: ${RELEASE_ID})" + fi + done -- 2.52.0 From fa8b4ff24897a83c71eae77da7eeabe869fd95c2 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:30 +0000 Subject: [PATCH 041/136] chore: remove .gitea/workflows/pre-release.yml (moved to .mokogitea/) [skip ci] --- .gitea/workflows/pre-release.yml | 341 ------------------------------- 1 file changed, 341 deletions(-) delete mode 100644 .gitea/workflows/pre-release.yml diff --git a/.gitea/workflows/pre-release.yml b/.gitea/workflows/pre-release.yml deleted file mode 100644 index 30c9bcf..0000000 --- a/.gitea/workflows/pre-release.yml +++ /dev/null @@ -1,341 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: MokoStandards.Release -# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards -# PATH: /.gitea/workflows/pre-release.yml -# VERSION: 01.00.00 -# BRIEF: Manual pre-release — builds dev/alpha/beta/rc packages from any branch - -name: Pre-Release - -on: - workflow_dispatch: - inputs: - stability: - description: 'Pre-release channel' - required: true - type: choice - options: - - development - - alpha - - beta - - release-candidate - -permissions: - contents: write - -env: - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} - -jobs: - build: - name: "Build Pre-Release (${{ inputs.stability }})" - runs-on: release - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.GA_TOKEN }} - - - name: Setup PHP - run: | - if ! command -v php &> /dev/null; then - sudo apt-get update -qq - sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip >/dev/null 2>&1 - fi - - - name: Resolve metadata - id: meta - run: | - STABILITY="${{ inputs.stability }}" - - case "$STABILITY" in - development) SUFFIX="-dev"; TAG="development" ;; - alpha) SUFFIX="-alpha"; TAG="alpha" ;; - beta) SUFFIX="-beta"; TAG="beta" ;; - release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; - esac - - # Read and bump patch version (with rollover) - CURRENT=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1) - [ -z "$CURRENT" ] && CURRENT="00.00.00" - - MAJOR=$(echo "$CURRENT" | cut -d. -f1) - MINOR=$(echo "$CURRENT" | cut -d. -f2) - PATCH=$(echo "$CURRENT" | cut -d. -f3) - - # Patch bump with rollover: ZZ=99 → bump minor, YY=99 → bump major - NEW_PATCH=$((10#$PATCH + 1)) - NEW_MINOR=$((10#$MINOR)) - NEW_MAJOR=$((10#$MAJOR)) - - if [ $NEW_PATCH -gt 99 ]; then - NEW_PATCH=0 - NEW_MINOR=$((NEW_MINOR + 1)) - fi - if [ $NEW_MINOR -gt 99 ]; then - NEW_MINOR=0 - NEW_MAJOR=$((NEW_MAJOR + 1)) - fi - - VERSION=$(printf "%02d.%02d.%02d" $NEW_MAJOR $NEW_MINOR $NEW_PATCH) - TODAY=$(date +%Y-%m-%d) - - echo "Bumping: ${CURRENT} → ${VERSION} (patch)" - - # Update README.md - sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${VERSION}/" README.md - - # Update manifest - MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) - if [ -n "$MANIFEST" ]; then - MANIFEST_VER=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1) - sed -i "s|${MANIFEST_VER}|${VERSION}|" "$MANIFEST" - sed -i "s|[^<]*|${TODAY}|" "$MANIFEST" - fi - - # Commit version bump - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - git add -A - git diff --cached --quiet || { - git commit -m "chore(version): bump ${CURRENT} → ${VERSION} [skip ci]" - git push origin HEAD 2>&1 - } - - # Auto-detect element from manifest - MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) - EXT_ELEMENT="" - if [ -n "$MANIFEST" ]; then - EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1) - if [ -z "$EXT_ELEMENT" ]; then - EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') - case "$EXT_ELEMENT" in - templatedetails|manifest) EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;; - esac - fi - else - EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') - fi - - ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip" - - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" - echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" - echo "tag=${TAG}" >> "$GITHUB_OUTPUT" - echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" - echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" - echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT" - - echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ===" - - - name: Build package - run: | - SOURCE_DIR="src" - [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" - if [ ! -d "$SOURCE_DIR" ]; then - echo "::error::No src/ or htdocs/ directory" - exit 1 - fi - - mkdir -p build/package - rsync -a \ - --exclude='sftp-config*' \ - --exclude='.ftpignore' \ - --exclude='*.ppk' \ - --exclude='*.pem' \ - --exclude='*.key' \ - --exclude='.env*' \ - --exclude='*.local' \ - --exclude='.build-trigger' \ - "${SOURCE_DIR}/" build/package/ - - - name: Create ZIP - id: zip - run: | - ZIP_NAME="${{ steps.meta.outputs.zip_name }}" - cd build/package - zip -r "../${ZIP_NAME}" . - cd .. - - SHA256=$(sha256sum "${ZIP_NAME}" | cut -d' ' -f1) - echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT" - echo "ZIP: ${ZIP_NAME} (SHA: ${SHA256:0:16}...)" - - - name: Create or replace Gitea release - id: release - run: | - TAG="${{ steps.meta.outputs.tag }}" - VERSION="${{ steps.meta.outputs.version }}" - STABILITY="${{ steps.meta.outputs.stability }}" - SHA256="${{ steps.zip.outputs.sha256 }}" - ZIP_NAME="${{ steps.meta.outputs.zip_name }}" - EXT_ELEMENT="${{ steps.meta.outputs.ext_element }}" - TOKEN="${{ secrets.GA_TOKEN }}" - API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - BRANCH=$(git branch --show-current) - - BODY="## ${VERSION} ($(date +%Y-%m-%d)) - **Channel:** ${STABILITY} - **SHA-256:** \`${SHA256}\`" - - # Delete existing release - EXISTING_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \ - "${API}/releases/tags/${TAG}" | jq -r '.id // empty' 2>/dev/null) - if [ -n "$EXISTING_ID" ]; then - curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API}/releases/${EXISTING_ID}" 2>/dev/null || true - curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API}/tags/${TAG}" 2>/dev/null || true - fi - - # Create release - RELEASE_ID=$(curl -sS -X POST -H "Authorization: token ${TOKEN}" \ - -H "Content-Type: application/json" \ - "${API}/releases" \ - -d "$(jq -n \ - --arg tag "$TAG" \ - --arg target "$BRANCH" \ - --arg name "${EXT_ELEMENT} ${VERSION} (${STABILITY})" \ - --arg body "$BODY" \ - '{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: true}' - )" | jq -r '.id') - - echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT" - - # Upload ZIP - curl -sS -X POST -H "Authorization: token ${TOKEN}" \ - -H "Content-Type: application/octet-stream" \ - "${API}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \ - --data-binary "@build/${ZIP_NAME}" - - echo "Released: ${EXT_ELEMENT} ${VERSION} (${STABILITY})" - - - name: Update updates.xml - run: | - STABILITY="${{ steps.meta.outputs.stability }}" - VERSION="${{ steps.meta.outputs.version }}" - SHA256="${{ steps.zip.outputs.sha256 }}" - ZIP_NAME="${{ steps.meta.outputs.zip_name }}" - TAG="${{ steps.meta.outputs.tag }}" - DATE=$(date +%Y-%m-%d) - - if [ ! -f "updates.xml" ]; then - echo "No updates.xml — skipping" - exit 0 - fi - - export PY_STABILITY="$STABILITY" PY_VERSION="$VERSION" PY_SHA256="$SHA256" \ - PY_ZIP_NAME="$ZIP_NAME" PY_TAG="$TAG" PY_DATE="$DATE" \ - PY_GITEA_ORG="$GITEA_ORG" PY_GITEA_REPO="$GITEA_REPO" - python3 << 'PYEOF' - import re, os - - stability = os.environ["PY_STABILITY"] - version = os.environ["PY_VERSION"] - sha256 = os.environ["PY_SHA256"] - zip_name = os.environ["PY_ZIP_NAME"] - tag = os.environ["PY_TAG"] - date = os.environ["PY_DATE"] - gitea_org = os.environ["PY_GITEA_ORG"] - gitea_repo = os.environ["PY_GITEA_REPO"] - download_url = f"https://git.mokoconsulting.tech/{gitea_org}/{gitea_repo}/releases/download/{tag}/{zip_name}" - - with open("updates.xml", "r") as f: - content = f.read() - - # Map stability to XML tag name - tag_map = {"development": "development", "alpha": "alpha", "beta": "beta", "release-candidate": "rc"} - xml_tag = tag_map.get(stability, stability) - - pattern = r"((?:(?!).)*?" + re.escape(xml_tag) + r".*?)" - match = re.search(pattern, content, re.DOTALL) - if match: - block = match.group(1) - updated = re.sub(r"[^<]*", f"{version}", block) - updated = re.sub(r"[^<]*", f"{date}", updated) - if "" in updated: - updated = re.sub(r"[^<]*", f"{sha256}", updated) - else: - updated = updated.replace("", f"\n {sha256}") - updated = re.sub(r"(]*>)[^<]*()", rf"\g<1>{download_url}\g<2>", updated) - content = content.replace(block, updated) - print(f"Updated {xml_tag} channel: version={version}") - else: - print(f"WARNING: No {xml_tag} block in updates.xml") - - with open("updates.xml", "w") as f: - f.write(content) - PYEOF - - # Commit and push to current branch - if ! git diff --quiet updates.xml 2>/dev/null; then - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git add updates.xml - git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]" - git push origin HEAD 2>&1 || echo "WARNING: push failed" - fi - - - name: "Sync updates.xml to all branches" - run: | - CURRENT_BRANCH="${{ github.ref_name }}" - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - - # Sync updates.xml to main and dev (whichever isn't current) - for BRANCH in main dev; do - [ "$BRANCH" = "$CURRENT_BRANCH" ] && continue - - echo "Syncing updates.xml → ${BRANCH}" - git fetch origin "${BRANCH}" 2>/dev/null || continue - git checkout "origin/${BRANCH}" -- . 2>/dev/null || continue - git checkout "${CURRENT_BRANCH}" -- updates.xml - if ! git diff --quiet updates.xml 2>/dev/null; then - git add updates.xml - git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]" - git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed" - fi - git checkout "${CURRENT_BRANCH}" 2>/dev/null - done - - - name: "Delete lesser pre-release channels (cascade)" - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.GA_TOKEN }}" - STABILITY="${{ steps.meta.outputs.stability }}" - - # Cascade: rc → beta,alpha,dev | beta → alpha,dev | alpha → dev | dev → nothing - case "$STABILITY" in - release-candidate) TAGS_TO_DELETE="beta alpha development" ;; - beta) TAGS_TO_DELETE="alpha development" ;; - alpha) TAGS_TO_DELETE="development" ;; - *) TAGS_TO_DELETE="" ;; - esac - - [ -z "$TAGS_TO_DELETE" ] && exit 0 - - for TAG in $TAGS_TO_DELETE; do - RELEASE_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/releases/tags/${TAG}" 2>/dev/null | \ - python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) - - if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then - curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/releases/${RELEASE_ID}" 2>/dev/null || true - curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/tags/${TAG}" 2>/dev/null || true - echo "Deleted: ${TAG} (id: ${RELEASE_ID})" - fi - done -- 2.52.0 From 3bd5a66f4f6a2e7f519e8aee8edca9dbb42e3bab Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:30 +0000 Subject: [PATCH 042/136] chore: move .gitea/workflows/release.yml to .mokogitea/release.yml [skip ci] --- .mokogitea/release.yml | 600 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 600 insertions(+) create mode 100644 .mokogitea/release.yml diff --git a/.mokogitea/release.yml b/.mokogitea/release.yml new file mode 100644 index 0000000..07d1b24 --- /dev/null +++ b/.mokogitea/release.yml @@ -0,0 +1,600 @@ +# Copyright (C) 2026 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: MokoStandards.Joomla +# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards +# PATH: /.gitea/workflows/release.yml +# VERSION: 02.00.00 +# BRIEF: Generic Joomla release — auto-detects element from manifest, stream tags, cascade + +name: Create Release + +on: + push: + tags: + - 'stable' + - 'release-candidate' + - 'beta' + - 'alpha' + - 'development' + workflow_dispatch: + inputs: + stability: + description: 'Stability tag' + required: true + default: 'stable' + type: choice + options: + - stable + - release-candidate + - beta + - alpha + - development + +permissions: + contents: write + +env: + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +jobs: + build: + name: Build Release Package + runs-on: release + + steps: + # Always checkout main for tag triggers (avoids detached HEAD). + # For workflow_dispatch, checkout whatever branch was selected. + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.event_name == 'push' && 'main' || github.ref }} + fetch-depth: 0 + token: ${{ secrets.GA_TOKEN }} + + - name: Setup PHP + run: | + if ! command -v php &> /dev/null; then + sudo apt-get update -qq + sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + echo "PHP: $(php -v | head -1)" + echo "Composer: $(composer --version 2>&1 | head -1)" + + - name: Get version and stability + id: meta + run: | + echo "=== Meta ===" + echo "event_name: ${{ github.event_name }}" + echo "ref: ${{ github.ref }}" + echo "ref_name: ${{ github.ref_name }}" + echo "sha: ${{ github.sha }}" + + # Derive stability from tag name or dispatch input + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + STABILITY="${{ inputs.stability }}" + else + TAG_PUSHED="${GITHUB_REF#refs/tags/}" + case "$TAG_PUSHED" in + stable) STABILITY="stable" ;; + release-candidate) STABILITY="rc" ;; + beta) STABILITY="beta" ;; + alpha) STABILITY="alpha" ;; + development) STABILITY="development" ;; + *) STABILITY="stable" ;; + esac + fi + + # Read version from README.md (will be bumped in next step) + VERSION=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1) + [ -z "$VERSION" ] && VERSION="00.00.00" + + # Auto-detect extension element from Joomla manifest + # Search depth 3 covers src/admin/com_xxx.xml and similar nested structures + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '/dev/null | head -1) + EXT_ELEMENT="" + if [ -n "$MANIFEST" ]; then + EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1) + # If no tag, derive from manifest filename or repo name + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') + case "$EXT_ELEMENT" in + templatedetails|manifest) EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;; + esac + fi + echo "Manifest: ${MANIFEST}, element: ${EXT_ELEMENT}" + else + EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + echo "No manifest found, using repo name: ${EXT_ELEMENT}" + fi + + case "$STABILITY" in + development) SUFFIX="-dev"; TAG_NAME="development" ;; + alpha) SUFFIX="-alpha"; TAG_NAME="alpha" ;; + beta) SUFFIX="-beta"; TAG_NAME="beta" ;; + rc) SUFFIX="-rc"; TAG_NAME="release-candidate" ;; + stable) SUFFIX=""; TAG_NAME="stable" ;; + *) SUFFIX="-dev"; TAG_NAME="development" ;; + esac + + PRERELEASE="true" + [ "$STABILITY" = "stable" ] && PRERELEASE="false" + + ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip" + + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" + echo "prerelease=${PRERELEASE}" >> "$GITHUB_OUTPUT" + echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" + echo "tag_name=${TAG_NAME}" >> "$GITHUB_OUTPUT" + echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" + echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" + + echo "=== Resolved ===" + echo "VERSION=${VERSION}" + echo "STABILITY=${STABILITY}" + echo "TAG_NAME=${TAG_NAME}" + echo "ZIP_NAME=${ZIP_NAME}" + echo "Branch: $(git branch --show-current)" + + - name: Auto-bump patch version + id: bump + env: + GA_TOKEN: ${{ secrets.GA_TOKEN }} + INPUT_VERSION: ${{ steps.meta.outputs.version }} + INPUT_STABILITY: ${{ steps.meta.outputs.stability }} + INPUT_SUFFIX: ${{ steps.meta.outputs.suffix }} + EXT_ELEMENT: ${{ steps.meta.outputs.ext_element }} + run: | + BRANCH=$(git branch --show-current) + GITEA_API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" + + echo "=== Version Bump ===" + echo "On branch: ${BRANCH}" + + # Read current version from README.md + CURRENT=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1) + echo "Current version in README: ${CURRENT}" + + if [ -z "$CURRENT" ]; then + echo "No VERSION in README.md — using input version" + echo "version=${INPUT_VERSION}" >> "$GITHUB_OUTPUT" + echo "zip_name=${EXT_ELEMENT}-${INPUT_VERSION}${INPUT_SUFFIX}.zip" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Bump patch: XX.YY.ZZ → XX.YY.(ZZ+1) + MAJOR=$(echo "$CURRENT" | cut -d. -f1) + MINOR=$(echo "$CURRENT" | cut -d. -f2) + PATCH=$(echo "$CURRENT" | cut -d. -f3) + NEW_PATCH=$(printf "%02d" $((10#$PATCH + 1))) + NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}" + TODAY=$(date +%Y-%m-%d) + + echo "Bumping: ${CURRENT} → ${NEW_VERSION} (date: ${TODAY})" + + # Update README.md + sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${NEW_VERSION}/" README.md + + # Update manifest (templateDetails.xml / *.xml with ) + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '/dev/null | head -1) + if [ -n "$MANIFEST" ]; then + echo "Manifest: ${MANIFEST}" + sed -i "s|${CURRENT}|${NEW_VERSION}|" "$MANIFEST" + sed -i "s|[^<]*|${TODAY}|" "$MANIFEST" + fi + + # Update matching stability channel in updates.xml + if [ -f "updates.xml" ]; then + export PY_OLD="$CURRENT" PY_NEW="$NEW_VERSION" PY_STABILITY="$INPUT_STABILITY" PY_DATE="$TODAY" + python3 << 'PYEOF' + import re, os + old = os.environ["PY_OLD"] + new = os.environ["PY_NEW"] + stability = os.environ["PY_STABILITY"] + date = os.environ["PY_DATE"] + with open("updates.xml") as f: + content = f.read() + pattern = r"((?:(?!).)*?" + re.escape(stability) + r".*?)" + match = re.search(pattern, content, re.DOTALL) + if match: + block = match.group(1) + updated = block.replace(old, new) + updated = re.sub(r"[^<]*", f"{date}", updated) + content = content.replace(block, updated) + print(f"Updated {stability} channel: {old} -> {new}") + else: + print(f"WARNING: No block found for {stability}") + with open("updates.xml", "w") as f: + f.write(content) + PYEOF + fi + + # Commit and push version bump + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://jmiller:${GA_TOKEN}@git.mokoconsulting.tech/${{ github.repository }}.git" + git add -A + git diff --cached --quiet && echo "No changes to commit" || { + git commit -m "chore(version): bump ${CURRENT} → ${NEW_VERSION} [skip ci]" \ + --author="gitea-actions[bot] " + echo "Pushing version bump to ${BRANCH}..." + git push origin HEAD:${BRANCH} 2>&1 + echo "Push exit code: $?" + } + + # For stable releases from non-main: merge to main via Gitea API + if [ "$INPUT_STABILITY" = "stable" ] && [ "$BRANCH" != "main" ]; then + echo "Merging ${BRANCH} → main via Gitea API..." + HTTP_CODE=$(curl -sS -o /tmp/merge_response.json -w "%{http_code}" \ + -X POST -H "Authorization: token ${GA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${GITEA_API}/merges" \ + -d "$(jq -n \ + --arg base "main" \ + --arg head "${BRANCH}" \ + --arg msg "chore(release): merge ${BRANCH} for stable ${NEW_VERSION} [skip ci]" \ + '{base: $base, head: $head, merge_message_field: $msg}' + )") + echo "Merge response (HTTP ${HTTP_CODE}):" + cat /tmp/merge_response.json 2>/dev/null; echo + fi + + echo "version=${NEW_VERSION}" >> "$GITHUB_OUTPUT" + echo "zip_name=${EXT_ELEMENT}-${NEW_VERSION}${INPUT_SUFFIX}.zip" >> "$GITHUB_OUTPUT" + echo "=== Bump complete: ${NEW_VERSION} ===" + + - name: Install dependencies + env: + COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_TOKEN }}"}}}' + run: | + if [ -f "composer.json" ]; then + echo "Installing composer dependencies..." + composer install --no-dev --optimize-autoloader --no-interaction 2>&1 + else + echo "No composer.json — skipping" + fi + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Minify CSS and JS + run: | + if [ -f "package.json" ] && [ -f "scripts/minify.js" ]; then + npm ci --ignore-scripts + node scripts/minify.js + else + echo "No minify setup — skipping" + fi + + - name: Create package + run: | + # Detect source directory (src/ or htdocs/) + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + if [ ! -d "$SOURCE_DIR" ]; then + echo "::error::No src/ or htdocs/ directory found" + exit 1 + fi + echo "Source directory: ${SOURCE_DIR}" + + mkdir -p build/package + rsync -av \ + --exclude='sftp-config*' \ + --exclude='.ftpignore' \ + --exclude='*.ppk' \ + --exclude='*.pem' \ + --exclude='*.key' \ + --exclude='.env*' \ + --exclude='*.local' \ + --exclude='.build-trigger' \ + --exclude='.beta-trigger' \ + --exclude='.rc-trigger' \ + "${SOURCE_DIR}/" build/package/ + echo "Package contents:" + ls -la build/package/ | head -20 + + - name: Build ZIP + id: zip + run: | + ZIP_NAME="${{ steps.bump.outputs.zip_name }}" + echo "Building: ${ZIP_NAME}" + cd build/package + zip -r "../${ZIP_NAME}" . + cd .. + + SHA256=$(sha256sum "${ZIP_NAME}" | cut -d' ' -f1) + SIZE=$(stat -c%s "${ZIP_NAME}") + + echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT" + echo "size=${SIZE}" >> "$GITHUB_OUTPUT" + echo "=== Package Built ===" + echo "ZIP: ${ZIP_NAME}" + echo "SHA-256: ${SHA256}" + echo "Size: ${SIZE} bytes" + + # ── Gitea Release (PRIMARY) ─────────────────────────���──────────── + - name: "Gitea: Create or update release" + id: gitea_release + env: + EXT_ELEMENT: ${{ steps.meta.outputs.ext_element }} + run: | + TAG="${{ steps.meta.outputs.tag_name }}" + VERSION="${{ steps.bump.outputs.version }}" + STABILITY="${{ steps.meta.outputs.stability }}" + SHA256="${{ steps.zip.outputs.sha256 }}" + TOKEN="${{ secrets.GA_TOKEN }}" + API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + BRANCH=$(git branch --show-current) + MAX_HISTORY=5 + + IS_PRE="true" + [ "$STABILITY" = "stable" ] && IS_PRE="false" + + # Build this version's entry + NEW_ENTRY="## ${VERSION} ($(date +%Y-%m-%d)) + **SHA-256:** \`${SHA256}\`" + + if [ -f "CHANGELOG.md" ]; then + NOTES=$(awk "/## \[${VERSION}\]/,/## \[/{if(/## \[${VERSION}\]/)next;if(/## \[/)exit;print}" CHANGELOG.md) + [ -n "$NOTES" ] && NEW_ENTRY="## ${VERSION} ($(date +%Y-%m-%d)) + ${NOTES} + **SHA-256:** \`${SHA256}\`" + fi + + # Check for existing release — keep last N versions in body + EXISTING_BODY="" + EXISTING_ID="" + RELEASE_JSON=$(curl -sS -H "Authorization: token ${TOKEN}" \ + "${API}/releases/tags/${TAG}" 2>/dev/null) + EXISTING_ID=$(echo "$RELEASE_JSON" | jq -r '.id // empty') + + if [ -n "$EXISTING_ID" ]; then + echo "Existing release found: id=${EXISTING_ID}" + EXISTING_BODY=$(echo "$RELEASE_JSON" | jq -r '.body // ""') + + # Keep only last (MAX_HISTORY - 1) version entries to make room for new one + TRIMMED_BODY=$(echo "$EXISTING_BODY" | python3 -c " + import sys, re + content = sys.stdin.read() + # Split on version headers (## XX.YY.ZZ) + parts = re.split(r'(?=^## \d)', content, flags=re.MULTILINE) + # Keep only version entries (skip any preamble) + versions = [p for p in parts if re.match(r'^## \d', p)] + # Keep last $((MAX_HISTORY - 1)) entries + kept = versions[:$((MAX_HISTORY - 1))] + print('\n---\n'.join(kept)) + " 2>/dev/null || echo "") + + # Delete old release and tag so we can recreate + curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API}/releases/${EXISTING_ID}" 2>/dev/null || true + curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API}/tags/${TAG}" 2>/dev/null || true + fi + + # Compose full body: new entry + previous entries + if [ -n "$TRIMMED_BODY" ]; then + FULL_BODY="${NEW_ENTRY} + + --- + + ${TRIMMED_BODY}" + else + FULL_BODY="${NEW_ENTRY}" + fi + + echo "=== Create Release ===" + echo "TAG=${TAG} VERSION=${VERSION} BRANCH=${BRANCH} PRE=${IS_PRE} HISTORY=${MAX_HISTORY}" + + HTTP_CODE=$(curl -sS -o /tmp/create_release.json -w "%{http_code}" \ + -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/releases" \ + -d "$(jq -n \ + --arg tag "$TAG" \ + --arg target "$BRANCH" \ + --arg name "${EXT_ELEMENT} ${VERSION} (${STABILITY})" \ + --arg body "$FULL_BODY" \ + --argjson pre "$IS_PRE" \ + '{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: $pre}' + )") + + echo "Response (HTTP ${HTTP_CODE}):" + cat /tmp/create_release.json | jq . 2>/dev/null || cat /tmp/create_release.json + echo + + RELEASE_ID=$(jq -r '.id // empty' /tmp/create_release.json) + if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then + echo "::error::Failed to create release (HTTP ${HTTP_CODE})" + exit 1 + fi + + echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT" + echo "Release created: id=${RELEASE_ID}" + + - name: "Gitea: Upload ZIP" + run: | + RELEASE_ID="${{ steps.gitea_release.outputs.release_id }}" + ZIP_NAME="${{ steps.bump.outputs.zip_name }}" + TOKEN="${{ secrets.GA_TOKEN }}" + API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + echo "Uploading ${ZIP_NAME} to release ${RELEASE_ID}..." + HTTP_CODE=$(curl -sS -o /tmp/upload_response.json -w "%{http_code}" \ + -X POST \ + -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/octet-stream" \ + "${API}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \ + --data-binary "@build/${ZIP_NAME}") + + echo "Upload response (HTTP ${HTTP_CODE}):" + cat /tmp/upload_response.json | jq . 2>/dev/null || cat /tmp/upload_response.json + echo + + if [ "$HTTP_CODE" -ge 400 ]; then + echo "::error::Upload failed (HTTP ${HTTP_CODE})" + exit 1 + fi + echo "Uploaded ${ZIP_NAME}" + + # ── Update updates.xml ────────────────────────────────────────── + - name: "Update updates.xml with SHA and sync to main" + run: | + STABILITY="${{ steps.meta.outputs.stability }}" + VERSION="${{ steps.bump.outputs.version }}" + SHA256="${{ steps.zip.outputs.sha256 }}" + ZIP_NAME="${{ steps.bump.outputs.zip_name }}" + TAG="${{ steps.meta.outputs.tag_name }}" + DATE=$(date +%Y-%m-%d) + BRANCH=$(git branch --show-current) + + echo "=== Update updates.xml ===" + echo "STABILITY=${STABILITY} VERSION=${VERSION} SHA=${SHA256:0:16}..." + + if [ ! -f "updates.xml" ] || [ -z "$SHA256" ]; then + echo "No updates.xml or no SHA — skipping" + exit 0 + fi + + # Cascade map: each stability level updates itself + all lower levels + # stable → all | rc → rc,beta,alpha,dev | beta → beta,alpha,dev | alpha → alpha,dev | dev → dev + case "$STABILITY" in + stable) CASCADE="development,alpha,beta,rc,stable" ;; + rc) CASCADE="development,alpha,beta,rc" ;; + beta) CASCADE="development,alpha,beta" ;; + alpha) CASCADE="development,alpha" ;; + development) CASCADE="development" ;; + *) CASCADE="$STABILITY" ;; + esac + + echo "Cascade: ${STABILITY} → ${CASCADE}" + + export PY_CASCADE="$CASCADE" PY_VERSION="$VERSION" PY_SHA256="$SHA256" \ + PY_ZIP_NAME="$ZIP_NAME" PY_TAG="$TAG" PY_DATE="$DATE" \ + PY_GITEA_ORG="$GITEA_ORG" PY_GITEA_REPO="$GITEA_REPO" + python3 << 'PYEOF' + import re, os + + cascade = os.environ["PY_CASCADE"].split(",") + version = os.environ["PY_VERSION"] + sha256 = os.environ["PY_SHA256"] + zip_name = os.environ["PY_ZIP_NAME"] + tag = os.environ["PY_TAG"] + date = os.environ["PY_DATE"] + gitea_org = os.environ["PY_GITEA_ORG"] + gitea_repo = os.environ["PY_GITEA_REPO"] + + gitea_url = f"https://git.mokoconsulting.tech/{gitea_org}/{gitea_repo}/releases/download/{tag}/{zip_name}" + + with open("updates.xml", "r") as f: + content = f.read() + + for xml_tag in cascade: + xml_tag = xml_tag.strip() + block_pattern = r"((?:(?!).)*?" + re.escape(xml_tag) + r".*?)" + match = re.search(block_pattern, content, re.DOTALL) + + if not match: + print(f" SKIP: no {xml_tag} block found") + continue + + block = match.group(1) + original_block = block + + # Update version and date + block = re.sub(r"[^<]*", f"{version}", block) + block = re.sub(r"[^<]*", f"{date}", block) + + # Set SHA — add if missing, update if present, never leave empty + if "" in block: + block = re.sub(r"[^<]*", f"{sha256}", block) + else: + block = block.replace("", f"\n {sha256}") + + # Update download URL + block = re.sub( + r"(]*>)https://git\.mokoconsulting\.tech/[^<]*()", + rf"\g<1>{gitea_url}\g<2>", + block + ) + + content = content.replace(original_block, block) + print(f" OK: {xml_tag} → version={version}, sha={sha256[:16]}...") + + with open("updates.xml", "w") as f: + f.write(content) + + print(f"Cascaded {len(cascade)} channel(s)") + PYEOF + + # Commit and push + if git diff --quiet updates.xml 2>/dev/null; then + echo "No changes to updates.xml" + exit 0 + fi + + git add updates.xml + git commit -m "chore: update ${STABILITY} SHA-256 for ${VERSION} [skip ci]" \ + --author="gitea-actions[bot] " + echo "Pushing updates.xml to ${BRANCH}..." + git push origin HEAD:${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed" + + # Always sync updates.xml to main via API (Joomla reads from main) + GA_TOKEN="${{ secrets.GA_TOKEN }}" + API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" + + echo "Syncing updates.xml to main via API..." + FILE_SHA=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \ + "${API}/contents/updates.xml?ref=main" | jq -r '.sha // empty') + + if [ -n "$FILE_SHA" ]; then + CONTENT=$(base64 -w0 updates.xml) + HTTP_CODE=$(curl -sS -o /tmp/sync_response.json -w "%{http_code}" \ + -X PUT -H "Authorization: token ${GA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/contents/updates.xml" \ + -d "$(jq -n \ + --arg content "$CONTENT" \ + --arg sha "$FILE_SHA" \ + --arg msg "chore: sync updates.xml ${STABILITY} ${VERSION} [skip ci]" \ + --arg branch "main" \ + '{content: $content, sha: $sha, message: $msg, branch: $branch}' + )") + echo "Sync response (HTTP ${HTTP_CODE}):" + cat /tmp/sync_response.json | jq -r '.content.name // .message // "unknown"' 2>/dev/null + if [ "$HTTP_CODE" -ge 400 ]; then + echo "::warning::Sync to main failed (HTTP ${HTTP_CODE})" + fi + else + echo "::warning::Could not get updates.xml SHA from main" + fi + + - name: Summary + if: always() + run: | + VERSION="${{ steps.bump.outputs.version }}" + STABILITY="${{ steps.meta.outputs.stability }}" + ZIP_NAME="${{ steps.bump.outputs.zip_name }}" + SHA256="${{ steps.zip.outputs.sha256 }}" + TAG="${{ steps.meta.outputs.tag_name }}" + + echo "### Release Created" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Stability | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY + echo "| Tag | \`${TAG}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY + echo "| SHA-256 | \`${SHA256}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Gitea | [Release](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${TAG}) |" >> $GITHUB_STEP_SUMMARY -- 2.52.0 From 0764f8728d71032fcda3a7c6c5a6fac55ec624cf Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:30 +0000 Subject: [PATCH 043/136] chore: remove .gitea/workflows/release.yml (moved to .mokogitea/) [skip ci] --- .gitea/workflows/release.yml | 600 ----------------------------------- 1 file changed, 600 deletions(-) delete mode 100644 .gitea/workflows/release.yml diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml deleted file mode 100644 index 07d1b24..0000000 --- a/.gitea/workflows/release.yml +++ /dev/null @@ -1,600 +0,0 @@ -# Copyright (C) 2026 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: MokoStandards.Joomla -# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards -# PATH: /.gitea/workflows/release.yml -# VERSION: 02.00.00 -# BRIEF: Generic Joomla release — auto-detects element from manifest, stream tags, cascade - -name: Create Release - -on: - push: - tags: - - 'stable' - - 'release-candidate' - - 'beta' - - 'alpha' - - 'development' - workflow_dispatch: - inputs: - stability: - description: 'Stability tag' - required: true - default: 'stable' - type: choice - options: - - stable - - release-candidate - - beta - - alpha - - development - -permissions: - contents: write - -env: - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} - -jobs: - build: - name: Build Release Package - runs-on: release - - steps: - # Always checkout main for tag triggers (avoids detached HEAD). - # For workflow_dispatch, checkout whatever branch was selected. - - name: Checkout repository - uses: actions/checkout@v4 - with: - ref: ${{ github.event_name == 'push' && 'main' || github.ref }} - fetch-depth: 0 - token: ${{ secrets.GA_TOKEN }} - - - name: Setup PHP - run: | - if ! command -v php &> /dev/null; then - sudo apt-get update -qq - sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - echo "PHP: $(php -v | head -1)" - echo "Composer: $(composer --version 2>&1 | head -1)" - - - name: Get version and stability - id: meta - run: | - echo "=== Meta ===" - echo "event_name: ${{ github.event_name }}" - echo "ref: ${{ github.ref }}" - echo "ref_name: ${{ github.ref_name }}" - echo "sha: ${{ github.sha }}" - - # Derive stability from tag name or dispatch input - if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then - STABILITY="${{ inputs.stability }}" - else - TAG_PUSHED="${GITHUB_REF#refs/tags/}" - case "$TAG_PUSHED" in - stable) STABILITY="stable" ;; - release-candidate) STABILITY="rc" ;; - beta) STABILITY="beta" ;; - alpha) STABILITY="alpha" ;; - development) STABILITY="development" ;; - *) STABILITY="stable" ;; - esac - fi - - # Read version from README.md (will be bumped in next step) - VERSION=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1) - [ -z "$VERSION" ] && VERSION="00.00.00" - - # Auto-detect extension element from Joomla manifest - # Search depth 3 covers src/admin/com_xxx.xml and similar nested structures - MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '/dev/null | head -1) - EXT_ELEMENT="" - if [ -n "$MANIFEST" ]; then - EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1) - # If no tag, derive from manifest filename or repo name - if [ -z "$EXT_ELEMENT" ]; then - EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') - case "$EXT_ELEMENT" in - templatedetails|manifest) EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;; - esac - fi - echo "Manifest: ${MANIFEST}, element: ${EXT_ELEMENT}" - else - EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') - echo "No manifest found, using repo name: ${EXT_ELEMENT}" - fi - - case "$STABILITY" in - development) SUFFIX="-dev"; TAG_NAME="development" ;; - alpha) SUFFIX="-alpha"; TAG_NAME="alpha" ;; - beta) SUFFIX="-beta"; TAG_NAME="beta" ;; - rc) SUFFIX="-rc"; TAG_NAME="release-candidate" ;; - stable) SUFFIX=""; TAG_NAME="stable" ;; - *) SUFFIX="-dev"; TAG_NAME="development" ;; - esac - - PRERELEASE="true" - [ "$STABILITY" = "stable" ] && PRERELEASE="false" - - ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip" - - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" - echo "prerelease=${PRERELEASE}" >> "$GITHUB_OUTPUT" - echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" - echo "tag_name=${TAG_NAME}" >> "$GITHUB_OUTPUT" - echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" - echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" - - echo "=== Resolved ===" - echo "VERSION=${VERSION}" - echo "STABILITY=${STABILITY}" - echo "TAG_NAME=${TAG_NAME}" - echo "ZIP_NAME=${ZIP_NAME}" - echo "Branch: $(git branch --show-current)" - - - name: Auto-bump patch version - id: bump - env: - GA_TOKEN: ${{ secrets.GA_TOKEN }} - INPUT_VERSION: ${{ steps.meta.outputs.version }} - INPUT_STABILITY: ${{ steps.meta.outputs.stability }} - INPUT_SUFFIX: ${{ steps.meta.outputs.suffix }} - EXT_ELEMENT: ${{ steps.meta.outputs.ext_element }} - run: | - BRANCH=$(git branch --show-current) - GITEA_API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" - - echo "=== Version Bump ===" - echo "On branch: ${BRANCH}" - - # Read current version from README.md - CURRENT=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1) - echo "Current version in README: ${CURRENT}" - - if [ -z "$CURRENT" ]; then - echo "No VERSION in README.md — using input version" - echo "version=${INPUT_VERSION}" >> "$GITHUB_OUTPUT" - echo "zip_name=${EXT_ELEMENT}-${INPUT_VERSION}${INPUT_SUFFIX}.zip" >> "$GITHUB_OUTPUT" - exit 0 - fi - - # Bump patch: XX.YY.ZZ → XX.YY.(ZZ+1) - MAJOR=$(echo "$CURRENT" | cut -d. -f1) - MINOR=$(echo "$CURRENT" | cut -d. -f2) - PATCH=$(echo "$CURRENT" | cut -d. -f3) - NEW_PATCH=$(printf "%02d" $((10#$PATCH + 1))) - NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}" - TODAY=$(date +%Y-%m-%d) - - echo "Bumping: ${CURRENT} → ${NEW_VERSION} (date: ${TODAY})" - - # Update README.md - sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${NEW_VERSION}/" README.md - - # Update manifest (templateDetails.xml / *.xml with ) - MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '/dev/null | head -1) - if [ -n "$MANIFEST" ]; then - echo "Manifest: ${MANIFEST}" - sed -i "s|${CURRENT}|${NEW_VERSION}|" "$MANIFEST" - sed -i "s|[^<]*|${TODAY}|" "$MANIFEST" - fi - - # Update matching stability channel in updates.xml - if [ -f "updates.xml" ]; then - export PY_OLD="$CURRENT" PY_NEW="$NEW_VERSION" PY_STABILITY="$INPUT_STABILITY" PY_DATE="$TODAY" - python3 << 'PYEOF' - import re, os - old = os.environ["PY_OLD"] - new = os.environ["PY_NEW"] - stability = os.environ["PY_STABILITY"] - date = os.environ["PY_DATE"] - with open("updates.xml") as f: - content = f.read() - pattern = r"((?:(?!).)*?" + re.escape(stability) + r".*?)" - match = re.search(pattern, content, re.DOTALL) - if match: - block = match.group(1) - updated = block.replace(old, new) - updated = re.sub(r"[^<]*", f"{date}", updated) - content = content.replace(block, updated) - print(f"Updated {stability} channel: {old} -> {new}") - else: - print(f"WARNING: No block found for {stability}") - with open("updates.xml", "w") as f: - f.write(content) - PYEOF - fi - - # Commit and push version bump - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://jmiller:${GA_TOKEN}@git.mokoconsulting.tech/${{ github.repository }}.git" - git add -A - git diff --cached --quiet && echo "No changes to commit" || { - git commit -m "chore(version): bump ${CURRENT} → ${NEW_VERSION} [skip ci]" \ - --author="gitea-actions[bot] " - echo "Pushing version bump to ${BRANCH}..." - git push origin HEAD:${BRANCH} 2>&1 - echo "Push exit code: $?" - } - - # For stable releases from non-main: merge to main via Gitea API - if [ "$INPUT_STABILITY" = "stable" ] && [ "$BRANCH" != "main" ]; then - echo "Merging ${BRANCH} → main via Gitea API..." - HTTP_CODE=$(curl -sS -o /tmp/merge_response.json -w "%{http_code}" \ - -X POST -H "Authorization: token ${GA_TOKEN}" \ - -H "Content-Type: application/json" \ - "${GITEA_API}/merges" \ - -d "$(jq -n \ - --arg base "main" \ - --arg head "${BRANCH}" \ - --arg msg "chore(release): merge ${BRANCH} for stable ${NEW_VERSION} [skip ci]" \ - '{base: $base, head: $head, merge_message_field: $msg}' - )") - echo "Merge response (HTTP ${HTTP_CODE}):" - cat /tmp/merge_response.json 2>/dev/null; echo - fi - - echo "version=${NEW_VERSION}" >> "$GITHUB_OUTPUT" - echo "zip_name=${EXT_ELEMENT}-${NEW_VERSION}${INPUT_SUFFIX}.zip" >> "$GITHUB_OUTPUT" - echo "=== Bump complete: ${NEW_VERSION} ===" - - - name: Install dependencies - env: - COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_TOKEN }}"}}}' - run: | - if [ -f "composer.json" ]; then - echo "Installing composer dependencies..." - composer install --no-dev --optimize-autoloader --no-interaction 2>&1 - else - echo "No composer.json — skipping" - fi - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Minify CSS and JS - run: | - if [ -f "package.json" ] && [ -f "scripts/minify.js" ]; then - npm ci --ignore-scripts - node scripts/minify.js - else - echo "No minify setup — skipping" - fi - - - name: Create package - run: | - # Detect source directory (src/ or htdocs/) - SOURCE_DIR="src" - [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" - if [ ! -d "$SOURCE_DIR" ]; then - echo "::error::No src/ or htdocs/ directory found" - exit 1 - fi - echo "Source directory: ${SOURCE_DIR}" - - mkdir -p build/package - rsync -av \ - --exclude='sftp-config*' \ - --exclude='.ftpignore' \ - --exclude='*.ppk' \ - --exclude='*.pem' \ - --exclude='*.key' \ - --exclude='.env*' \ - --exclude='*.local' \ - --exclude='.build-trigger' \ - --exclude='.beta-trigger' \ - --exclude='.rc-trigger' \ - "${SOURCE_DIR}/" build/package/ - echo "Package contents:" - ls -la build/package/ | head -20 - - - name: Build ZIP - id: zip - run: | - ZIP_NAME="${{ steps.bump.outputs.zip_name }}" - echo "Building: ${ZIP_NAME}" - cd build/package - zip -r "../${ZIP_NAME}" . - cd .. - - SHA256=$(sha256sum "${ZIP_NAME}" | cut -d' ' -f1) - SIZE=$(stat -c%s "${ZIP_NAME}") - - echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT" - echo "size=${SIZE}" >> "$GITHUB_OUTPUT" - echo "=== Package Built ===" - echo "ZIP: ${ZIP_NAME}" - echo "SHA-256: ${SHA256}" - echo "Size: ${SIZE} bytes" - - # ── Gitea Release (PRIMARY) ─────────────────────────���──────────── - - name: "Gitea: Create or update release" - id: gitea_release - env: - EXT_ELEMENT: ${{ steps.meta.outputs.ext_element }} - run: | - TAG="${{ steps.meta.outputs.tag_name }}" - VERSION="${{ steps.bump.outputs.version }}" - STABILITY="${{ steps.meta.outputs.stability }}" - SHA256="${{ steps.zip.outputs.sha256 }}" - TOKEN="${{ secrets.GA_TOKEN }}" - API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - BRANCH=$(git branch --show-current) - MAX_HISTORY=5 - - IS_PRE="true" - [ "$STABILITY" = "stable" ] && IS_PRE="false" - - # Build this version's entry - NEW_ENTRY="## ${VERSION} ($(date +%Y-%m-%d)) - **SHA-256:** \`${SHA256}\`" - - if [ -f "CHANGELOG.md" ]; then - NOTES=$(awk "/## \[${VERSION}\]/,/## \[/{if(/## \[${VERSION}\]/)next;if(/## \[/)exit;print}" CHANGELOG.md) - [ -n "$NOTES" ] && NEW_ENTRY="## ${VERSION} ($(date +%Y-%m-%d)) - ${NOTES} - **SHA-256:** \`${SHA256}\`" - fi - - # Check for existing release — keep last N versions in body - EXISTING_BODY="" - EXISTING_ID="" - RELEASE_JSON=$(curl -sS -H "Authorization: token ${TOKEN}" \ - "${API}/releases/tags/${TAG}" 2>/dev/null) - EXISTING_ID=$(echo "$RELEASE_JSON" | jq -r '.id // empty') - - if [ -n "$EXISTING_ID" ]; then - echo "Existing release found: id=${EXISTING_ID}" - EXISTING_BODY=$(echo "$RELEASE_JSON" | jq -r '.body // ""') - - # Keep only last (MAX_HISTORY - 1) version entries to make room for new one - TRIMMED_BODY=$(echo "$EXISTING_BODY" | python3 -c " - import sys, re - content = sys.stdin.read() - # Split on version headers (## XX.YY.ZZ) - parts = re.split(r'(?=^## \d)', content, flags=re.MULTILINE) - # Keep only version entries (skip any preamble) - versions = [p for p in parts if re.match(r'^## \d', p)] - # Keep last $((MAX_HISTORY - 1)) entries - kept = versions[:$((MAX_HISTORY - 1))] - print('\n---\n'.join(kept)) - " 2>/dev/null || echo "") - - # Delete old release and tag so we can recreate - curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API}/releases/${EXISTING_ID}" 2>/dev/null || true - curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API}/tags/${TAG}" 2>/dev/null || true - fi - - # Compose full body: new entry + previous entries - if [ -n "$TRIMMED_BODY" ]; then - FULL_BODY="${NEW_ENTRY} - - --- - - ${TRIMMED_BODY}" - else - FULL_BODY="${NEW_ENTRY}" - fi - - echo "=== Create Release ===" - echo "TAG=${TAG} VERSION=${VERSION} BRANCH=${BRANCH} PRE=${IS_PRE} HISTORY=${MAX_HISTORY}" - - HTTP_CODE=$(curl -sS -o /tmp/create_release.json -w "%{http_code}" \ - -X POST -H "Authorization: token ${TOKEN}" \ - -H "Content-Type: application/json" \ - "${API}/releases" \ - -d "$(jq -n \ - --arg tag "$TAG" \ - --arg target "$BRANCH" \ - --arg name "${EXT_ELEMENT} ${VERSION} (${STABILITY})" \ - --arg body "$FULL_BODY" \ - --argjson pre "$IS_PRE" \ - '{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: $pre}' - )") - - echo "Response (HTTP ${HTTP_CODE}):" - cat /tmp/create_release.json | jq . 2>/dev/null || cat /tmp/create_release.json - echo - - RELEASE_ID=$(jq -r '.id // empty' /tmp/create_release.json) - if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then - echo "::error::Failed to create release (HTTP ${HTTP_CODE})" - exit 1 - fi - - echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT" - echo "Release created: id=${RELEASE_ID}" - - - name: "Gitea: Upload ZIP" - run: | - RELEASE_ID="${{ steps.gitea_release.outputs.release_id }}" - ZIP_NAME="${{ steps.bump.outputs.zip_name }}" - TOKEN="${{ secrets.GA_TOKEN }}" - API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - - echo "Uploading ${ZIP_NAME} to release ${RELEASE_ID}..." - HTTP_CODE=$(curl -sS -o /tmp/upload_response.json -w "%{http_code}" \ - -X POST \ - -H "Authorization: token ${TOKEN}" \ - -H "Content-Type: application/octet-stream" \ - "${API}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \ - --data-binary "@build/${ZIP_NAME}") - - echo "Upload response (HTTP ${HTTP_CODE}):" - cat /tmp/upload_response.json | jq . 2>/dev/null || cat /tmp/upload_response.json - echo - - if [ "$HTTP_CODE" -ge 400 ]; then - echo "::error::Upload failed (HTTP ${HTTP_CODE})" - exit 1 - fi - echo "Uploaded ${ZIP_NAME}" - - # ── Update updates.xml ────────────────────────────────────────── - - name: "Update updates.xml with SHA and sync to main" - run: | - STABILITY="${{ steps.meta.outputs.stability }}" - VERSION="${{ steps.bump.outputs.version }}" - SHA256="${{ steps.zip.outputs.sha256 }}" - ZIP_NAME="${{ steps.bump.outputs.zip_name }}" - TAG="${{ steps.meta.outputs.tag_name }}" - DATE=$(date +%Y-%m-%d) - BRANCH=$(git branch --show-current) - - echo "=== Update updates.xml ===" - echo "STABILITY=${STABILITY} VERSION=${VERSION} SHA=${SHA256:0:16}..." - - if [ ! -f "updates.xml" ] || [ -z "$SHA256" ]; then - echo "No updates.xml or no SHA — skipping" - exit 0 - fi - - # Cascade map: each stability level updates itself + all lower levels - # stable → all | rc → rc,beta,alpha,dev | beta → beta,alpha,dev | alpha → alpha,dev | dev → dev - case "$STABILITY" in - stable) CASCADE="development,alpha,beta,rc,stable" ;; - rc) CASCADE="development,alpha,beta,rc" ;; - beta) CASCADE="development,alpha,beta" ;; - alpha) CASCADE="development,alpha" ;; - development) CASCADE="development" ;; - *) CASCADE="$STABILITY" ;; - esac - - echo "Cascade: ${STABILITY} → ${CASCADE}" - - export PY_CASCADE="$CASCADE" PY_VERSION="$VERSION" PY_SHA256="$SHA256" \ - PY_ZIP_NAME="$ZIP_NAME" PY_TAG="$TAG" PY_DATE="$DATE" \ - PY_GITEA_ORG="$GITEA_ORG" PY_GITEA_REPO="$GITEA_REPO" - python3 << 'PYEOF' - import re, os - - cascade = os.environ["PY_CASCADE"].split(",") - version = os.environ["PY_VERSION"] - sha256 = os.environ["PY_SHA256"] - zip_name = os.environ["PY_ZIP_NAME"] - tag = os.environ["PY_TAG"] - date = os.environ["PY_DATE"] - gitea_org = os.environ["PY_GITEA_ORG"] - gitea_repo = os.environ["PY_GITEA_REPO"] - - gitea_url = f"https://git.mokoconsulting.tech/{gitea_org}/{gitea_repo}/releases/download/{tag}/{zip_name}" - - with open("updates.xml", "r") as f: - content = f.read() - - for xml_tag in cascade: - xml_tag = xml_tag.strip() - block_pattern = r"((?:(?!).)*?" + re.escape(xml_tag) + r".*?)" - match = re.search(block_pattern, content, re.DOTALL) - - if not match: - print(f" SKIP: no {xml_tag} block found") - continue - - block = match.group(1) - original_block = block - - # Update version and date - block = re.sub(r"[^<]*", f"{version}", block) - block = re.sub(r"[^<]*", f"{date}", block) - - # Set SHA — add if missing, update if present, never leave empty - if "" in block: - block = re.sub(r"[^<]*", f"{sha256}", block) - else: - block = block.replace("", f"\n {sha256}") - - # Update download URL - block = re.sub( - r"(]*>)https://git\.mokoconsulting\.tech/[^<]*()", - rf"\g<1>{gitea_url}\g<2>", - block - ) - - content = content.replace(original_block, block) - print(f" OK: {xml_tag} → version={version}, sha={sha256[:16]}...") - - with open("updates.xml", "w") as f: - f.write(content) - - print(f"Cascaded {len(cascade)} channel(s)") - PYEOF - - # Commit and push - if git diff --quiet updates.xml 2>/dev/null; then - echo "No changes to updates.xml" - exit 0 - fi - - git add updates.xml - git commit -m "chore: update ${STABILITY} SHA-256 for ${VERSION} [skip ci]" \ - --author="gitea-actions[bot] " - echo "Pushing updates.xml to ${BRANCH}..." - git push origin HEAD:${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed" - - # Always sync updates.xml to main via API (Joomla reads from main) - GA_TOKEN="${{ secrets.GA_TOKEN }}" - API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" - - echo "Syncing updates.xml to main via API..." - FILE_SHA=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \ - "${API}/contents/updates.xml?ref=main" | jq -r '.sha // empty') - - if [ -n "$FILE_SHA" ]; then - CONTENT=$(base64 -w0 updates.xml) - HTTP_CODE=$(curl -sS -o /tmp/sync_response.json -w "%{http_code}" \ - -X PUT -H "Authorization: token ${GA_TOKEN}" \ - -H "Content-Type: application/json" \ - "${API}/contents/updates.xml" \ - -d "$(jq -n \ - --arg content "$CONTENT" \ - --arg sha "$FILE_SHA" \ - --arg msg "chore: sync updates.xml ${STABILITY} ${VERSION} [skip ci]" \ - --arg branch "main" \ - '{content: $content, sha: $sha, message: $msg, branch: $branch}' - )") - echo "Sync response (HTTP ${HTTP_CODE}):" - cat /tmp/sync_response.json | jq -r '.content.name // .message // "unknown"' 2>/dev/null - if [ "$HTTP_CODE" -ge 400 ]; then - echo "::warning::Sync to main failed (HTTP ${HTTP_CODE})" - fi - else - echo "::warning::Could not get updates.xml SHA from main" - fi - - - name: Summary - if: always() - run: | - VERSION="${{ steps.bump.outputs.version }}" - STABILITY="${{ steps.meta.outputs.stability }}" - ZIP_NAME="${{ steps.bump.outputs.zip_name }}" - SHA256="${{ steps.zip.outputs.sha256 }}" - TAG="${{ steps.meta.outputs.tag_name }}" - - echo "### Release Created" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY - echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Stability | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY - echo "| Tag | \`${TAG}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY - echo "| SHA-256 | \`${SHA256}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Gitea | [Release](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${TAG}) |" >> $GITHUB_STEP_SUMMARY -- 2.52.0 From d7c0a54f6cf68e31366eca151ed01d3f8e04581e Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:31 +0000 Subject: [PATCH 044/136] chore: move .gitea/workflows/repo-health.yml to .mokogitea/repo-health.yml [skip ci] --- .mokogitea/repo-health.yml | 766 +++++++++++++++++++++++++++++++++++++ 1 file changed, 766 insertions(+) create mode 100644 .mokogitea/repo-health.yml diff --git a/.mokogitea/repo-health.yml b/.mokogitea/repo-health.yml new file mode 100644 index 0000000..57b11ef --- /dev/null +++ b/.mokogitea/repo-health.yml @@ -0,0 +1,766 @@ +# ============================================================================ +# 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: MokoStandards.Validation +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# PATH: /templates/workflows/joomla/repo_health.yml.template +# VERSION: 04.06.00 +# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts. +# ============================================================================ + +name: Repo Health + +concurrency: + group: repo-health-${{ github.repository }}-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +on: + workflow_dispatch: + inputs: + profile: + description: 'Validation profile: all, release, scripts, or repo' + required: true + default: all + type: choice + options: + - all + - release + - scripts + - repo + pull_request: + push: + +permissions: + contents: read + +env: + # Release policy - Repository Variables Only + RELEASE_REQUIRED_REPO_VARS: RS_FTP_PATH_SUFFIX + RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX + + # 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,.gitea/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: .gitea/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.GA_TOKEN || secrets.GA_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 + + release_config: + name: Release configuration + 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: Guardrails release vars + env: + PROFILE_RAW: ${{ github.event.inputs.profile }} + RS_FTP_PATH_SUFFIX: ${{ vars.RS_FTP_PATH_SUFFIX }} + DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }} + run: | + set -euo pipefail + + profile="${PROFILE_RAW:-all}" + case "${profile}" in + all|release|scripts|repo) ;; + *) + printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" + exit 1 + ;; + esac + + if [ "${profile}" = 'scripts' ] || [ "${profile}" = 'repo' ]; then + { + printf '%s\n' '### Release configuration (Repository Variables)' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' 'Status: SKIPPED' + printf '%s\n' 'Reason: profile excludes release validation' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 0 + fi + + IFS=',' read -r -a required <<< "${RELEASE_REQUIRED_REPO_VARS}" + IFS=',' read -r -a optional <<< "${RELEASE_OPTIONAL_REPO_VARS}" + + missing=() + missing_optional=() + + for k in "${required[@]}"; do + v="${!k:-}" + [ -z "${v}" ] && missing+=("${k}") + done + + for k in "${optional[@]}"; do + v="${!k:-}" + [ -z "${v}" ] && missing_optional+=("${k}") + done + + { + printf '%s\n' '### Release configuration (Repository Variables)' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' '| Variable | Status |' + printf '%s\n' '|---|---|' + printf '%s\n' "| RS_FTP_PATH_SUFFIX | ${RS_FTP_PATH_SUFFIX:-NOT SET} |" + printf '%s\n' "| DEV_FTP_SUFFIX | ${DEV_FTP_SUFFIX:-NOT SET} |" + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + if [ "${#missing_optional[@]}" -gt 0 ]; then + { + printf '%s\n' '### Missing optional repository variables' + for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + if [ "${#missing[@]}" -gt 0 ]; then + { + printf '%s\n' '### Missing required repository variables' + for m in "${missing[@]}"; do printf '%s\n' "- ${m}"; done + printf '%s\n' 'ERROR: Guardrails failed. Missing required repository variables.' + } >> "${GITHUB_STEP_SUMMARY}" + exit 1 + fi + + { + printf '%s\n' '### Repository variables validation result' + printf '%s\n' 'Status: OK' + printf '%s\n' 'All required repository variables present.' + printf '%s\n' '' + printf '%s\n' '**Note**: Organization secrets (RS_FTP_HOST, RS_FTP_USER, etc.) are validated at deployment time, not in repository health checks.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + 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|release|scripts|repo) ;; + *) + printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" + exit 1 + ;; + esac + + if [ "${profile}" = 'release' ] || [ "${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 + + IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}" + 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|release|scripts|repo) ;; + *) + printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" + exit 1 + ;; + esac + + if [ "${profile}" = 'release' ] || [ "${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 + + # Source directory: src/ or htdocs/ (either is valid) + if [ -d "src" ]; then + SOURCE_DIR="src" + elif [ -d "htdocs" ]; then + SOURCE_DIR="htdocs" + else + missing_required+=("src/ or htdocs/ (source directory required)") + fi + + IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}" + IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}" + IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}" + IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES}" + + missing_required=() + missing_optional=() + + 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 ]; then + missing_required+=("dev/* branch (e.g. dev/01.00.00)") + fi + + if [ "${#dev_branches[@]}" -gt 0 ]; then + missing_required+=("invalid branch dev (must be dev/)") + 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="$(python3 - <<'PY' + import json + import os + + profile = os.environ.get('PROFILE_RAW') or 'all' + + missing_required = os.environ.get('MISSING_REQUIRED', '').splitlines() if os.environ.get('MISSING_REQUIRED') else [] + missing_optional = os.environ.get('MISSING_OPTIONAL', '').splitlines() if os.environ.get('MISSING_OPTIONAL') else [] + content_warnings = os.environ.get('CONTENT_WARNINGS', '').splitlines() if os.environ.get('CONTENT_WARNINGS') else [] + + out = { + 'profile': profile, + 'missing_required': [x for x in missing_required if x], + 'missing_optional': [x for x in missing_optional if x], + 'content_warnings': [x for x in content_warnings if x], + } + + print(json.dumps(out, indent=2)) + PY + )" + + { + 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 + + 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 + + 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="$(python3 - <<'PY' + import os + import re + + idx = os.environ.get('DOCS_INDEX', 'docs/docs-index.md') + base = os.getcwd() + + bad = [] + pat = re.compile(r'\[[^\]]+\]\(([^)]+)\)') + + with open(idx, 'r', encoding='utf-8') as f: + for line in f: + for m in pat.findall(line): + link = m.strip() + if link.startswith('http://') or link.startswith('https://') or link.startswith('#') or link.startswith('mailto:'): + continue + if link.startswith('/'): + rel = link.lstrip('/') + else: + rel = os.path.normpath(os.path.join(os.path.dirname(idx), link)) + rel = rel.split('#', 1)[0] + rel = rel.split('?', 1)[0] + if not rel: + continue + p = os.path.join(base, rel) + if not os.path.exists(p): + bad.append(rel) + + print('\n'.join(sorted(set(bad)))) + PY + )" + 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:' + while IFS= read -r l; do [ -n "${l}" ] && printf '%s\n' "- ${l}"; done <<< "${missing_links}" + 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 variables | OK | Repository variables validation |' + 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}" -- 2.52.0 From 9be3e26bd3951c470004e49c9f2fd7c2bd1656bc Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:31 +0000 Subject: [PATCH 045/136] chore: remove .gitea/workflows/repo-health.yml (moved to .mokogitea/) [skip ci] --- .gitea/workflows/repo-health.yml | 766 ------------------------------- 1 file changed, 766 deletions(-) delete mode 100644 .gitea/workflows/repo-health.yml diff --git a/.gitea/workflows/repo-health.yml b/.gitea/workflows/repo-health.yml deleted file mode 100644 index 57b11ef..0000000 --- a/.gitea/workflows/repo-health.yml +++ /dev/null @@ -1,766 +0,0 @@ -# ============================================================================ -# 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: MokoStandards.Validation -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API -# PATH: /templates/workflows/joomla/repo_health.yml.template -# VERSION: 04.06.00 -# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts. -# ============================================================================ - -name: Repo Health - -concurrency: - group: repo-health-${{ github.repository }}-${{ github.ref }} - cancel-in-progress: true - -defaults: - run: - shell: bash - -on: - workflow_dispatch: - inputs: - profile: - description: 'Validation profile: all, release, scripts, or repo' - required: true - default: all - type: choice - options: - - all - - release - - scripts - - repo - pull_request: - push: - -permissions: - contents: read - -env: - # Release policy - Repository Variables Only - RELEASE_REQUIRED_REPO_VARS: RS_FTP_PATH_SUFFIX - RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX - - # 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,.gitea/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: .gitea/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.GA_TOKEN || secrets.GA_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 - - release_config: - name: Release configuration - 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: Guardrails release vars - env: - PROFILE_RAW: ${{ github.event.inputs.profile }} - RS_FTP_PATH_SUFFIX: ${{ vars.RS_FTP_PATH_SUFFIX }} - DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }} - run: | - set -euo pipefail - - profile="${PROFILE_RAW:-all}" - case "${profile}" in - all|release|scripts|repo) ;; - *) - printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" - exit 1 - ;; - esac - - if [ "${profile}" = 'scripts' ] || [ "${profile}" = 'repo' ]; then - { - printf '%s\n' '### Release configuration (Repository Variables)' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' 'Status: SKIPPED' - printf '%s\n' 'Reason: profile excludes release validation' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - exit 0 - fi - - IFS=',' read -r -a required <<< "${RELEASE_REQUIRED_REPO_VARS}" - IFS=',' read -r -a optional <<< "${RELEASE_OPTIONAL_REPO_VARS}" - - missing=() - missing_optional=() - - for k in "${required[@]}"; do - v="${!k:-}" - [ -z "${v}" ] && missing+=("${k}") - done - - for k in "${optional[@]}"; do - v="${!k:-}" - [ -z "${v}" ] && missing_optional+=("${k}") - done - - { - printf '%s\n' '### Release configuration (Repository Variables)' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' '| Variable | Status |' - printf '%s\n' '|---|---|' - printf '%s\n' "| RS_FTP_PATH_SUFFIX | ${RS_FTP_PATH_SUFFIX:-NOT SET} |" - printf '%s\n' "| DEV_FTP_SUFFIX | ${DEV_FTP_SUFFIX:-NOT SET} |" - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - - if [ "${#missing_optional[@]}" -gt 0 ]; then - { - printf '%s\n' '### Missing optional repository variables' - for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - if [ "${#missing[@]}" -gt 0 ]; then - { - printf '%s\n' '### Missing required repository variables' - for m in "${missing[@]}"; do printf '%s\n' "- ${m}"; done - printf '%s\n' 'ERROR: Guardrails failed. Missing required repository variables.' - } >> "${GITHUB_STEP_SUMMARY}" - exit 1 - fi - - { - printf '%s\n' '### Repository variables validation result' - printf '%s\n' 'Status: OK' - printf '%s\n' 'All required repository variables present.' - printf '%s\n' '' - printf '%s\n' '**Note**: Organization secrets (RS_FTP_HOST, RS_FTP_USER, etc.) are validated at deployment time, not in repository health checks.' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - - 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|release|scripts|repo) ;; - *) - printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" - exit 1 - ;; - esac - - if [ "${profile}" = 'release' ] || [ "${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 - - IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}" - 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|release|scripts|repo) ;; - *) - printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" - exit 1 - ;; - esac - - if [ "${profile}" = 'release' ] || [ "${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 - - # Source directory: src/ or htdocs/ (either is valid) - if [ -d "src" ]; then - SOURCE_DIR="src" - elif [ -d "htdocs" ]; then - SOURCE_DIR="htdocs" - else - missing_required+=("src/ or htdocs/ (source directory required)") - fi - - IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}" - IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}" - IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}" - IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES}" - - missing_required=() - missing_optional=() - - 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 ]; then - missing_required+=("dev/* branch (e.g. dev/01.00.00)") - fi - - if [ "${#dev_branches[@]}" -gt 0 ]; then - missing_required+=("invalid branch dev (must be dev/)") - 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="$(python3 - <<'PY' - import json - import os - - profile = os.environ.get('PROFILE_RAW') or 'all' - - missing_required = os.environ.get('MISSING_REQUIRED', '').splitlines() if os.environ.get('MISSING_REQUIRED') else [] - missing_optional = os.environ.get('MISSING_OPTIONAL', '').splitlines() if os.environ.get('MISSING_OPTIONAL') else [] - content_warnings = os.environ.get('CONTENT_WARNINGS', '').splitlines() if os.environ.get('CONTENT_WARNINGS') else [] - - out = { - 'profile': profile, - 'missing_required': [x for x in missing_required if x], - 'missing_optional': [x for x in missing_optional if x], - 'content_warnings': [x for x in content_warnings if x], - } - - print(json.dumps(out, indent=2)) - PY - )" - - { - 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 - - 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 - - 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="$(python3 - <<'PY' - import os - import re - - idx = os.environ.get('DOCS_INDEX', 'docs/docs-index.md') - base = os.getcwd() - - bad = [] - pat = re.compile(r'\[[^\]]+\]\(([^)]+)\)') - - with open(idx, 'r', encoding='utf-8') as f: - for line in f: - for m in pat.findall(line): - link = m.strip() - if link.startswith('http://') or link.startswith('https://') or link.startswith('#') or link.startswith('mailto:'): - continue - if link.startswith('/'): - rel = link.lstrip('/') - else: - rel = os.path.normpath(os.path.join(os.path.dirname(idx), link)) - rel = rel.split('#', 1)[0] - rel = rel.split('?', 1)[0] - if not rel: - continue - p = os.path.join(base, rel) - if not os.path.exists(p): - bad.append(rel) - - print('\n'.join(sorted(set(bad)))) - PY - )" - 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:' - while IFS= read -r l; do [ -n "${l}" ] && printf '%s\n' "- ${l}"; done <<< "${missing_links}" - 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 variables | OK | Repository variables validation |' - 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}" -- 2.52.0 From e0f9a3f1b672e47b70c111753d32993412ae330f Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:32 +0000 Subject: [PATCH 046/136] chore: move .gitea/workflows/security-audit.yml to .mokogitea/security-audit.yml [skip ci] --- .mokogitea/security-audit.yml | 82 +++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 .mokogitea/security-audit.yml diff --git a/.mokogitea/security-audit.yml b/.mokogitea/security-audit.yml new file mode 100644 index 0000000..ff6de4c --- /dev/null +++ b/.mokogitea/security-audit.yml @@ -0,0 +1,82 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Security +# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards +# PATH: /.gitea/workflows/security-audit.yml +# VERSION: 01.00.00 +# BRIEF: Dependency vulnerability scanning for composer and npm packages + +name: Security Audit + +on: + schedule: + - cron: '0 6 * * 1' # Weekly on Monday at 06:00 UTC + pull_request: + branches: + - main + paths: + - 'composer.json' + - 'composer.lock' + - 'package.json' + - 'package-lock.json' + workflow_dispatch: + +permissions: + contents: read + +env: + NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }} + NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }} + +jobs: + audit: + name: Dependency Audit + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Composer audit + if: hashFiles('composer.lock') != '' + run: | + echo "=== Composer Security Audit ===" + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq + sudo apt-get install -y -qq php-cli composer >/dev/null 2>&1 + fi + composer audit --format=plain 2>&1 | tee /tmp/composer-audit.txt + RESULT=$? + if [ $RESULT -ne 0 ]; then + echo "::warning::Composer vulnerabilities found" + echo "composer_vulnerable=true" >> "$GITHUB_ENV" + else + echo "No known vulnerabilities in composer dependencies" + fi + + - name: NPM audit + if: hashFiles('package-lock.json') != '' + run: | + echo "=== NPM Security Audit ===" + npm audit --production 2>&1 | tee /tmp/npm-audit.txt || true + if npm audit --production 2>&1 | grep -q "found 0 vulnerabilities"; then + echo "No known vulnerabilities in npm dependencies" + else + echo "::warning::NPM vulnerabilities found" + echo "npm_vulnerable=true" >> "$GITHUB_ENV" + fi + + - name: Notify on vulnerabilities + if: env.composer_vulnerable == 'true' || env.npm_vulnerable == 'true' + run: | + REPO="${{ github.event.repository.name }}" + curl -sS \ + -H "Title: ${REPO} has vulnerable dependencies" \ + -H "Tags: lock,warning" \ + -H "Priority: high" \ + -d "Security audit found vulnerabilities. Review dependency updates." \ + "${NTFY_URL}/${NTFY_TOPIC}" || true -- 2.52.0 From 61d3d18b905cf9556de56727e13ab6aa9fc1e8db Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:32 +0000 Subject: [PATCH 047/136] chore: remove .gitea/workflows/security-audit.yml (moved to .mokogitea/) [skip ci] --- .gitea/workflows/security-audit.yml | 82 ----------------------------- 1 file changed, 82 deletions(-) delete mode 100644 .gitea/workflows/security-audit.yml diff --git a/.gitea/workflows/security-audit.yml b/.gitea/workflows/security-audit.yml deleted file mode 100644 index ff6de4c..0000000 --- a/.gitea/workflows/security-audit.yml +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: MokoStandards.Security -# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards -# PATH: /.gitea/workflows/security-audit.yml -# VERSION: 01.00.00 -# BRIEF: Dependency vulnerability scanning for composer and npm packages - -name: Security Audit - -on: - schedule: - - cron: '0 6 * * 1' # Weekly on Monday at 06:00 UTC - pull_request: - branches: - - main - paths: - - 'composer.json' - - 'composer.lock' - - 'package.json' - - 'package-lock.json' - workflow_dispatch: - -permissions: - contents: read - -env: - NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }} - NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }} - -jobs: - audit: - name: Dependency Audit - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Composer audit - if: hashFiles('composer.lock') != '' - run: | - echo "=== Composer Security Audit ===" - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq - sudo apt-get install -y -qq php-cli composer >/dev/null 2>&1 - fi - composer audit --format=plain 2>&1 | tee /tmp/composer-audit.txt - RESULT=$? - if [ $RESULT -ne 0 ]; then - echo "::warning::Composer vulnerabilities found" - echo "composer_vulnerable=true" >> "$GITHUB_ENV" - else - echo "No known vulnerabilities in composer dependencies" - fi - - - name: NPM audit - if: hashFiles('package-lock.json') != '' - run: | - echo "=== NPM Security Audit ===" - npm audit --production 2>&1 | tee /tmp/npm-audit.txt || true - if npm audit --production 2>&1 | grep -q "found 0 vulnerabilities"; then - echo "No known vulnerabilities in npm dependencies" - else - echo "::warning::NPM vulnerabilities found" - echo "npm_vulnerable=true" >> "$GITHUB_ENV" - fi - - - name: Notify on vulnerabilities - if: env.composer_vulnerable == 'true' || env.npm_vulnerable == 'true' - run: | - REPO="${{ github.event.repository.name }}" - curl -sS \ - -H "Title: ${REPO} has vulnerable dependencies" \ - -H "Tags: lock,warning" \ - -H "Priority: high" \ - -d "Security audit found vulnerabilities. Review dependency updates." \ - "${NTFY_URL}/${NTFY_TOPIC}" || true -- 2.52.0 From f75dfd1c5bf3758f30032c995595546f75364ee0 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:32 +0000 Subject: [PATCH 048/136] chore: move .gitea/workflows/update-server.yml to .mokogitea/update-server.yml [skip ci] --- .mokogitea/update-server.yml | 464 +++++++++++++++++++++++++++++++++++ 1 file changed, 464 insertions(+) create mode 100644 .mokogitea/update-server.yml diff --git a/.mokogitea/update-server.yml b/.mokogitea/update-server.yml new file mode 100644 index 0000000..e6a1924 --- /dev/null +++ b/.mokogitea/update-server.yml @@ -0,0 +1,464 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Joomla +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# PATH: /templates/workflows/joomla/update-server.yml.template +# VERSION: 04.06.00 +# BRIEF: Update Joomla update server XML feed with stable/rc/dev entries +# +# Writes updates.xml with multiple entries: +# - stable on push to main (from auto-release) +# - rc on push to rc/** +# - development on push to dev or dev/** +# +# Joomla filters by user's "Minimum Stability" setting. + +name: Update Joomla Update Server XML Feed + +on: + push: + branches: + - 'dev' + - 'dev/**' + - 'alpha/**' + - 'beta/**' + - 'rc/**' + paths: + - 'src/**' + - 'htdocs/**' + pull_request: + types: [closed] + branches: + - 'dev' + - 'dev/**' + - 'alpha/**' + - 'beta/**' + - 'rc/**' + paths: + - 'src/**' + - 'htdocs/**' + workflow_dispatch: + inputs: + stability: + description: 'Stability tag' + required: true + default: 'development' + type: choice + options: + - development + - alpha + - beta + - rc + - stable + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +permissions: + contents: write + +jobs: + update-xml: + name: Update updates.xml + runs-on: release + if: >- + github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push' + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.GA_TOKEN }} + fetch-depth: 0 + + - name: Setup MokoStandards tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_TOKEN }}"}}}' + run: | + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + 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: Generate updates.xml entry + id: update + run: | + BRANCH="${{ github.ref_name }}" + REPO="${{ github.repository }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "0.0.0") + + # Auto-bump patch on all branches (dev, alpha, beta, rc) + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + BUMPED=$(php /tmp/mokostandards-api/cli/version_bump.php --path . 2>/dev/null || true) + if [ -n "$BUMPED" ]; then + VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "$VERSION") + git add -A + git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \ + --author="gitea-actions[bot] " 2>/dev/null || true + git push 2>/dev/null || true + fi + + # Determine stability from branch or input + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + STABILITY="${{ inputs.stability }}" + elif [[ "$BRANCH" == rc/* ]]; then + STABILITY="rc" + elif [[ "$BRANCH" == beta/* ]]; then + STABILITY="beta" + elif [[ "$BRANCH" == alpha/* ]]; then + STABILITY="alpha" + elif [[ "$BRANCH" == dev/* ]] || [[ "$BRANCH" == "dev" ]]; then + STABILITY="development" + else + STABILITY="stable" + fi + + echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" + + # Parse manifest (portable — no grep -P) + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '/dev/null | head -1) + if [ -z "$MANIFEST" ]; then + echo "No Joomla manifest found — skipping" + exit 0 + fi + + # Extract fields using sed (works on all runners) + EXT_NAME=$(sed -n 's/.*\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1) + EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1) + EXT_CLIENT=$(sed -n 's/.*]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_FOLDER=$(sed -n 's/.*]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_VERSION=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1) + TARGET_PLATFORM=$(sed -n 's/.*\(\).*/\1/p' "$MANIFEST" | head -1) + PHP_MINIMUM=$(sed -n 's/.*\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1) + + # Fallbacks + [ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}" + [ -z "$EXT_TYPE" ] && EXT_TYPE="component" + + # Derive element if not in manifest: try XML filename, then repo name + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') + case "$EXT_ELEMENT" in + templatedetails|manifest|*.xml) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;; + esac + fi + + # Use manifest version if README version is empty + [ "$VERSION" = "0.0.0" ] && [ -n "$EXT_VERSION" ] && VERSION="$EXT_VERSION" + + [ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '' "/") + + CLIENT_TAG="" + [ -n "$EXT_CLIENT" ] && CLIENT_TAG="${EXT_CLIENT}" + [ -z "$CLIENT_TAG" ] && ([ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]) && CLIENT_TAG="site" + + FOLDER_TAG="" + [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ] && FOLDER_TAG="${EXT_FOLDER}" + + PHP_TAG="" + [ -n "$PHP_MINIMUM" ] && PHP_TAG="${PHP_MINIMUM}" + + # Version suffix for non-stable + DISPLAY_VERSION="$VERSION" + case "$STABILITY" in + development) DISPLAY_VERSION="${VERSION}-dev" ;; + alpha) DISPLAY_VERSION="${VERSION}-alpha" ;; + beta) DISPLAY_VERSION="${VERSION}-beta" ;; + rc) DISPLAY_VERSION="${VERSION}-rc" ;; + esac + + MAJOR=$(echo "$VERSION" | awk -F. '{print $1}') + + # Each stability level has its own release tag + case "$STABILITY" in + development) RELEASE_TAG="development" ;; + alpha) RELEASE_TAG="alpha" ;; + beta) RELEASE_TAG="beta" ;; + rc) RELEASE_TAG="release-candidate" ;; + *) RELEASE_TAG="v${MAJOR}" ;; + esac + + PACKAGE_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.zip" + DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}" + INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}" + + # -- Build install packages (ZIP + tar.gz) -------------------- + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + if [ -d "$SOURCE_DIR" ]; then + EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*" + TAR_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.tar.gz" + + cd "$SOURCE_DIR" + zip -r "/tmp/${PACKAGE_NAME}" . -x $EXCLUDES + cd .. + tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \ + --exclude='.ftpignore' --exclude='sftp-config*' \ + --exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' . + + SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1) + + # Ensure release exists on Gitea + RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true) + RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + + if [ -z "$RELEASE_ID" ]; then + # Create release + RELEASE_JSON=$(curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/releases" \ + -d "$(python3 -c "import json; print(json.dumps({ + 'tag_name': '${RELEASE_TAG}', + 'name': '${RELEASE_TAG} (${DISPLAY_VERSION})', + 'body': '${STABILITY} release', + 'prerelease': True, + 'target_commitish': 'main' + }))")" 2>/dev/null || true) + RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + fi + + if [ -n "$RELEASE_ID" ]; then + # Delete existing assets with same name before uploading + ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]") + for ASSET_FILE in "$PACKAGE_NAME" "$TAR_NAME"; do + ASSET_ID=$(echo "$ASSETS" | python3 -c " + import sys,json + assets = json.load(sys.stdin) + for a in assets: + if a['name'] == '${ASSET_FILE}': + print(a['id']); break + " 2>/dev/null || true) + if [ -n "$ASSET_ID" ]; then + curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true + fi + done + + # Upload both formats + curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @"/tmp/${PACKAGE_NAME}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets?name=${PACKAGE_NAME}" > /dev/null 2>&1 || true + + curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @"/tmp/${TAR_NAME}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets?name=${TAR_NAME}" > /dev/null 2>&1 || true + fi + + echo "Packages: ${PACKAGE_NAME} + ${TAR_NAME} (SHA: ${SHA256})" >> $GITHUB_STEP_SUMMARY + else + SHA256="" + fi + + # -- Build the new entry (canonical format matching release.yml) -- + NEW_ENTRY="" + NEW_ENTRY="${NEW_ENTRY} \n" + NEW_ENTRY="${NEW_ENTRY} ${EXT_NAME}\n" + NEW_ENTRY="${NEW_ENTRY} ${EXT_NAME} ${STABILITY} build.\n" + NEW_ENTRY="${NEW_ENTRY} ${EXT_ELEMENT}\n" + NEW_ENTRY="${NEW_ENTRY} ${EXT_TYPE}\n" + [ -n "$CLIENT_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${CLIENT_TAG}\n" + [ -n "$FOLDER_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${FOLDER_TAG}\n" + NEW_ENTRY="${NEW_ENTRY} ${VERSION}\n" + NEW_ENTRY="${NEW_ENTRY} $(date +%Y-%m-%d)\n" + NEW_ENTRY="${NEW_ENTRY} https://git.mokoconsulting.tech/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${RELEASE_TAG}\n" + NEW_ENTRY="${NEW_ENTRY} \n" + NEW_ENTRY="${NEW_ENTRY} ${DOWNLOAD_URL}\n" + NEW_ENTRY="${NEW_ENTRY} \n" + [ -n "$SHA256" ] && NEW_ENTRY="${NEW_ENTRY} ${SHA256}\n" + NEW_ENTRY="${NEW_ENTRY} ${STABILITY}\n" + NEW_ENTRY="${NEW_ENTRY} Moko Consulting\n" + NEW_ENTRY="${NEW_ENTRY} https://mokoconsulting.tech\n" + NEW_ENTRY="${NEW_ENTRY} \n" + [ -n "$PHP_MINIMUM" ] && NEW_ENTRY="${NEW_ENTRY} ${PHP_MINIMUM}\n" + NEW_ENTRY="${NEW_ENTRY} " + + # -- Write new entry to temp file -------------------------------- + printf '%b' "$NEW_ENTRY" > /tmp/new_entry.xml + + # -- Merge into updates.xml ---------------------------------------- + # Cascade: stable→all | rc→rc+lower | beta→beta+lower | alpha→alpha+dev | dev→dev + CASCADE_MAP="stable:development,alpha,beta,rc,stable rc:development,alpha,beta,rc beta:development,alpha,beta alpha:development,alpha development:development" + TARGETS="" + for entry in $CASCADE_MAP; do + key="${entry%%:*}" + vals="${entry#*:}" + if [ "$key" = "${STABILITY}" ]; then + TARGETS="$vals" + break + fi + done + [ -z "$TARGETS" ] && TARGETS="${STABILITY}" + + echo "Cascade: ${STABILITY} → ${TARGETS}" + + # Create updates.xml if missing + if [ ! -f "updates.xml" ]; then + printf '%s\n' "" > updates.xml + printf '%s\n' "" >> updates.xml + printf '%s\n' "" >> updates.xml + printf '%s\n' "" >> updates.xml + fi + + # Update existing blocks or create missing ones + export PY_TARGETS="$TARGETS" PY_VERSION="$VERSION" PY_DATE="$(date +%Y-%m-%d)" + python3 << 'PYEOF' + import re, os + + targets = os.environ["PY_TARGETS"].split(",") + version = os.environ["PY_VERSION"] + date = os.environ["PY_DATE"] + + with open("updates.xml") as f: + content = f.read() + with open("/tmp/new_entry.xml") as f: + new_entry_template = f.read() + + for tag in targets: + tag = tag.strip() + # Build entry with this tag's name + new_entry = re.sub(r"[^<]*", f"{tag}", new_entry_template) + + # Try to find existing block (handles both single-line and multi-line ) + block_pattern = r"((?:(?!).)*?" + re.escape(tag) + r".*?)" + match = re.search(block_pattern, content, re.DOTALL) + + if match: + # Update in place — replace entire block + content = content.replace(match.group(1), new_entry.strip()) + print(f" UPDATED: {tag} → {version}") + else: + # Create — insert before + content = content.replace("", "\n" + new_entry.strip() + "\n\n") + print(f" CREATED: {tag} → {version}") + + # Clean up excessive blank lines + content = re.sub(r"\n{3,}", "\n\n", content) + + with open("updates.xml", "w") as f: + f.write(content) + PYEOF + + # Commit + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git add updates.xml + git diff --cached --quiet || { + git commit -m "chore: update updates.xml (${STABILITY}: ${DISPLAY_VERSION}) [skip ci]" \ + --author="gitea-actions[bot] " + git push + } + + # -- Sync updates.xml to main (for non-main branches) ---------------------- + - name: Sync updates.xml to main + if: github.ref_name != 'main' + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + GA_TOKEN="${{ secrets.GA_TOKEN }}" + + FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \ + "${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true) + + if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then + CONTENT=$(base64 -w0 updates.xml) + curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/contents/updates.xml" \ + -d "$(python3 -c "import json; print(json.dumps({ + 'content': '${CONTENT}', + 'sha': '${FILE_SHA}', + 'message': 'chore: sync updates.xml from ${STABILITY} [skip ci]', + 'branch': 'main' + }))")" > /dev/null 2>&1 \ + && echo "updates.xml synced to main (${STABILITY})" >> $GITHUB_STEP_SUMMARY \ + || echo "WARNING: failed to sync updates.xml to main" >> $GITHUB_STEP_SUMMARY + else + echo "WARNING: could not get updates.xml SHA from main" >> $GITHUB_STEP_SUMMARY + fi + + - name: SFTP deploy to dev server + if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev' + env: + DEV_HOST: ${{ vars.DEV_FTP_HOST }} + DEV_PATH: ${{ vars.DEV_FTP_PATH }} + DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }} + DEV_USER: ${{ vars.DEV_FTP_USERNAME }} + DEV_PORT: ${{ vars.DEV_FTP_PORT }} + DEV_KEY: ${{ secrets.DEV_FTP_KEY }} + DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }} + run: | + # -- Permission check: admin or maintain role required -------- + ACTOR="${{ github.actor }}" + REPO="${{ github.repository }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \ + python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read") + case "$PERMISSION" in + admin|maintain|write) ;; + *) + echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write" + exit 0 + ;; + esac + + [ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; } + + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + [ ! -d "$SOURCE_DIR" ] && exit 0 + + PORT="${DEV_PORT:-22}" + REMOTE="${DEV_PATH%/}" + [ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}" + + printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \ + "$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json + if [ -n "$DEV_KEY" ]; then + echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key + printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json + else + printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json + fi + + PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true) + if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then + php /tmp/mokostandards-api/deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json + elif [ -f "/tmp/mokostandards-api/deploy/deploy-sftp.php" ]; then + php /tmp/mokostandards-api/deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json + fi + rm -f /tmp/deploy_key /tmp/sftp-config.json + echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY + + - name: Summary + if: always() + run: | + echo "## Joomla Update Server" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${DISPLAY_VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Element | \`${EXT_ELEMENT}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Download | [ZIP](${DOWNLOAD_URL}) |" >> $GITHUB_STEP_SUMMARY -- 2.52.0 From 8a7d9b4deba3946800858dd05df0d84bb5756b8b Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:33 +0000 Subject: [PATCH 049/136] chore: remove .gitea/workflows/update-server.yml (moved to .mokogitea/) [skip ci] --- .gitea/workflows/update-server.yml | 464 ----------------------------- 1 file changed, 464 deletions(-) delete mode 100644 .gitea/workflows/update-server.yml diff --git a/.gitea/workflows/update-server.yml b/.gitea/workflows/update-server.yml deleted file mode 100644 index e6a1924..0000000 --- a/.gitea/workflows/update-server.yml +++ /dev/null @@ -1,464 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: MokoStandards.Joomla -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API -# PATH: /templates/workflows/joomla/update-server.yml.template -# VERSION: 04.06.00 -# BRIEF: Update Joomla update server XML feed with stable/rc/dev entries -# -# Writes updates.xml with multiple entries: -# - stable on push to main (from auto-release) -# - rc on push to rc/** -# - development on push to dev or dev/** -# -# Joomla filters by user's "Minimum Stability" setting. - -name: Update Joomla Update Server XML Feed - -on: - push: - branches: - - 'dev' - - 'dev/**' - - 'alpha/**' - - 'beta/**' - - 'rc/**' - paths: - - 'src/**' - - 'htdocs/**' - pull_request: - types: [closed] - branches: - - 'dev' - - 'dev/**' - - 'alpha/**' - - 'beta/**' - - 'rc/**' - paths: - - 'src/**' - - 'htdocs/**' - workflow_dispatch: - inputs: - stability: - description: 'Stability tag' - required: true - default: 'development' - type: choice - options: - - development - - alpha - - beta - - rc - - stable - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} - -permissions: - contents: write - -jobs: - update-xml: - name: Update updates.xml - runs-on: release - if: >- - github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push' - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.GA_TOKEN }} - fetch-depth: 0 - - - name: Setup MokoStandards tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_TOKEN }}"}}}' - run: | - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - 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: Generate updates.xml entry - id: update - run: | - BRANCH="${{ github.ref_name }}" - REPO="${{ github.repository }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "0.0.0") - - # Auto-bump patch on all branches (dev, alpha, beta, rc) - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - BUMPED=$(php /tmp/mokostandards-api/cli/version_bump.php --path . 2>/dev/null || true) - if [ -n "$BUMPED" ]; then - VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "$VERSION") - git add -A - git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \ - --author="gitea-actions[bot] " 2>/dev/null || true - git push 2>/dev/null || true - fi - - # Determine stability from branch or input - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - STABILITY="${{ inputs.stability }}" - elif [[ "$BRANCH" == rc/* ]]; then - STABILITY="rc" - elif [[ "$BRANCH" == beta/* ]]; then - STABILITY="beta" - elif [[ "$BRANCH" == alpha/* ]]; then - STABILITY="alpha" - elif [[ "$BRANCH" == dev/* ]] || [[ "$BRANCH" == "dev" ]]; then - STABILITY="development" - else - STABILITY="stable" - fi - - echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" - - # Parse manifest (portable — no grep -P) - MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '/dev/null | head -1) - if [ -z "$MANIFEST" ]; then - echo "No Joomla manifest found — skipping" - exit 0 - fi - - # Extract fields using sed (works on all runners) - EXT_NAME=$(sed -n 's/.*\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1) - EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) - EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1) - EXT_CLIENT=$(sed -n 's/.*]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) - EXT_FOLDER=$(sed -n 's/.*]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) - EXT_VERSION=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1) - TARGET_PLATFORM=$(sed -n 's/.*\(\).*/\1/p' "$MANIFEST" | head -1) - PHP_MINIMUM=$(sed -n 's/.*\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1) - - # Fallbacks - [ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}" - [ -z "$EXT_TYPE" ] && EXT_TYPE="component" - - # Derive element if not in manifest: try XML filename, then repo name - if [ -z "$EXT_ELEMENT" ]; then - EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') - case "$EXT_ELEMENT" in - templatedetails|manifest|*.xml) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;; - esac - fi - - # Use manifest version if README version is empty - [ "$VERSION" = "0.0.0" ] && [ -n "$EXT_VERSION" ] && VERSION="$EXT_VERSION" - - [ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '' "/") - - CLIENT_TAG="" - [ -n "$EXT_CLIENT" ] && CLIENT_TAG="${EXT_CLIENT}" - [ -z "$CLIENT_TAG" ] && ([ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]) && CLIENT_TAG="site" - - FOLDER_TAG="" - [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ] && FOLDER_TAG="${EXT_FOLDER}" - - PHP_TAG="" - [ -n "$PHP_MINIMUM" ] && PHP_TAG="${PHP_MINIMUM}" - - # Version suffix for non-stable - DISPLAY_VERSION="$VERSION" - case "$STABILITY" in - development) DISPLAY_VERSION="${VERSION}-dev" ;; - alpha) DISPLAY_VERSION="${VERSION}-alpha" ;; - beta) DISPLAY_VERSION="${VERSION}-beta" ;; - rc) DISPLAY_VERSION="${VERSION}-rc" ;; - esac - - MAJOR=$(echo "$VERSION" | awk -F. '{print $1}') - - # Each stability level has its own release tag - case "$STABILITY" in - development) RELEASE_TAG="development" ;; - alpha) RELEASE_TAG="alpha" ;; - beta) RELEASE_TAG="beta" ;; - rc) RELEASE_TAG="release-candidate" ;; - *) RELEASE_TAG="v${MAJOR}" ;; - esac - - PACKAGE_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.zip" - DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}" - INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}" - - # -- Build install packages (ZIP + tar.gz) -------------------- - SOURCE_DIR="src" - [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" - if [ -d "$SOURCE_DIR" ]; then - EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*" - TAR_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.tar.gz" - - cd "$SOURCE_DIR" - zip -r "/tmp/${PACKAGE_NAME}" . -x $EXCLUDES - cd .. - tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \ - --exclude='.ftpignore' --exclude='sftp-config*' \ - --exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' . - - SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1) - - # Ensure release exists on Gitea - RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true) - RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) - - if [ -z "$RELEASE_ID" ]; then - # Create release - RELEASE_JSON=$(curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - -H "Content-Type: application/json" \ - "${API_BASE}/releases" \ - -d "$(python3 -c "import json; print(json.dumps({ - 'tag_name': '${RELEASE_TAG}', - 'name': '${RELEASE_TAG} (${DISPLAY_VERSION})', - 'body': '${STABILITY} release', - 'prerelease': True, - 'target_commitish': 'main' - }))")" 2>/dev/null || true) - RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) - fi - - if [ -n "$RELEASE_ID" ]; then - # Delete existing assets with same name before uploading - ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - "${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]") - for ASSET_FILE in "$PACKAGE_NAME" "$TAR_NAME"; do - ASSET_ID=$(echo "$ASSETS" | python3 -c " - import sys,json - assets = json.load(sys.stdin) - for a in assets: - if a['name'] == '${ASSET_FILE}': - print(a['id']); break - " 2>/dev/null || true) - if [ -n "$ASSET_ID" ]; then - curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - "${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true - fi - done - - # Upload both formats - curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - -H "Content-Type: application/octet-stream" \ - --data-binary @"/tmp/${PACKAGE_NAME}" \ - "${API_BASE}/releases/${RELEASE_ID}/assets?name=${PACKAGE_NAME}" > /dev/null 2>&1 || true - - curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - -H "Content-Type: application/octet-stream" \ - --data-binary @"/tmp/${TAR_NAME}" \ - "${API_BASE}/releases/${RELEASE_ID}/assets?name=${TAR_NAME}" > /dev/null 2>&1 || true - fi - - echo "Packages: ${PACKAGE_NAME} + ${TAR_NAME} (SHA: ${SHA256})" >> $GITHUB_STEP_SUMMARY - else - SHA256="" - fi - - # -- Build the new entry (canonical format matching release.yml) -- - NEW_ENTRY="" - NEW_ENTRY="${NEW_ENTRY} \n" - NEW_ENTRY="${NEW_ENTRY} ${EXT_NAME}\n" - NEW_ENTRY="${NEW_ENTRY} ${EXT_NAME} ${STABILITY} build.\n" - NEW_ENTRY="${NEW_ENTRY} ${EXT_ELEMENT}\n" - NEW_ENTRY="${NEW_ENTRY} ${EXT_TYPE}\n" - [ -n "$CLIENT_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${CLIENT_TAG}\n" - [ -n "$FOLDER_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${FOLDER_TAG}\n" - NEW_ENTRY="${NEW_ENTRY} ${VERSION}\n" - NEW_ENTRY="${NEW_ENTRY} $(date +%Y-%m-%d)\n" - NEW_ENTRY="${NEW_ENTRY} https://git.mokoconsulting.tech/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${RELEASE_TAG}\n" - NEW_ENTRY="${NEW_ENTRY} \n" - NEW_ENTRY="${NEW_ENTRY} ${DOWNLOAD_URL}\n" - NEW_ENTRY="${NEW_ENTRY} \n" - [ -n "$SHA256" ] && NEW_ENTRY="${NEW_ENTRY} ${SHA256}\n" - NEW_ENTRY="${NEW_ENTRY} ${STABILITY}\n" - NEW_ENTRY="${NEW_ENTRY} Moko Consulting\n" - NEW_ENTRY="${NEW_ENTRY} https://mokoconsulting.tech\n" - NEW_ENTRY="${NEW_ENTRY} \n" - [ -n "$PHP_MINIMUM" ] && NEW_ENTRY="${NEW_ENTRY} ${PHP_MINIMUM}\n" - NEW_ENTRY="${NEW_ENTRY} " - - # -- Write new entry to temp file -------------------------------- - printf '%b' "$NEW_ENTRY" > /tmp/new_entry.xml - - # -- Merge into updates.xml ---------------------------------------- - # Cascade: stable→all | rc→rc+lower | beta→beta+lower | alpha→alpha+dev | dev→dev - CASCADE_MAP="stable:development,alpha,beta,rc,stable rc:development,alpha,beta,rc beta:development,alpha,beta alpha:development,alpha development:development" - TARGETS="" - for entry in $CASCADE_MAP; do - key="${entry%%:*}" - vals="${entry#*:}" - if [ "$key" = "${STABILITY}" ]; then - TARGETS="$vals" - break - fi - done - [ -z "$TARGETS" ] && TARGETS="${STABILITY}" - - echo "Cascade: ${STABILITY} → ${TARGETS}" - - # Create updates.xml if missing - if [ ! -f "updates.xml" ]; then - printf '%s\n' "" > updates.xml - printf '%s\n' "" >> updates.xml - printf '%s\n' "" >> updates.xml - printf '%s\n' "" >> updates.xml - fi - - # Update existing blocks or create missing ones - export PY_TARGETS="$TARGETS" PY_VERSION="$VERSION" PY_DATE="$(date +%Y-%m-%d)" - python3 << 'PYEOF' - import re, os - - targets = os.environ["PY_TARGETS"].split(",") - version = os.environ["PY_VERSION"] - date = os.environ["PY_DATE"] - - with open("updates.xml") as f: - content = f.read() - with open("/tmp/new_entry.xml") as f: - new_entry_template = f.read() - - for tag in targets: - tag = tag.strip() - # Build entry with this tag's name - new_entry = re.sub(r"[^<]*", f"{tag}", new_entry_template) - - # Try to find existing block (handles both single-line and multi-line ) - block_pattern = r"((?:(?!).)*?" + re.escape(tag) + r".*?)" - match = re.search(block_pattern, content, re.DOTALL) - - if match: - # Update in place — replace entire block - content = content.replace(match.group(1), new_entry.strip()) - print(f" UPDATED: {tag} → {version}") - else: - # Create — insert before - content = content.replace("", "\n" + new_entry.strip() + "\n\n") - print(f" CREATED: {tag} → {version}") - - # Clean up excessive blank lines - content = re.sub(r"\n{3,}", "\n\n", content) - - with open("updates.xml", "w") as f: - f.write(content) - PYEOF - - # Commit - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git add updates.xml - git diff --cached --quiet || { - git commit -m "chore: update updates.xml (${STABILITY}: ${DISPLAY_VERSION}) [skip ci]" \ - --author="gitea-actions[bot] " - git push - } - - # -- Sync updates.xml to main (for non-main branches) ---------------------- - - name: Sync updates.xml to main - if: github.ref_name != 'main' - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - GA_TOKEN="${{ secrets.GA_TOKEN }}" - - FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \ - "${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true) - - if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then - CONTENT=$(base64 -w0 updates.xml) - curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \ - -H "Content-Type: application/json" \ - "${API_BASE}/contents/updates.xml" \ - -d "$(python3 -c "import json; print(json.dumps({ - 'content': '${CONTENT}', - 'sha': '${FILE_SHA}', - 'message': 'chore: sync updates.xml from ${STABILITY} [skip ci]', - 'branch': 'main' - }))")" > /dev/null 2>&1 \ - && echo "updates.xml synced to main (${STABILITY})" >> $GITHUB_STEP_SUMMARY \ - || echo "WARNING: failed to sync updates.xml to main" >> $GITHUB_STEP_SUMMARY - else - echo "WARNING: could not get updates.xml SHA from main" >> $GITHUB_STEP_SUMMARY - fi - - - name: SFTP deploy to dev server - if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev' - env: - DEV_HOST: ${{ vars.DEV_FTP_HOST }} - DEV_PATH: ${{ vars.DEV_FTP_PATH }} - DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }} - DEV_USER: ${{ vars.DEV_FTP_USERNAME }} - DEV_PORT: ${{ vars.DEV_FTP_PORT }} - DEV_KEY: ${{ secrets.DEV_FTP_KEY }} - DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }} - run: | - # -- Permission check: admin or maintain role required -------- - ACTOR="${{ github.actor }}" - REPO="${{ github.repository }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - - PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ - "${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \ - python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read") - case "$PERMISSION" in - admin|maintain|write) ;; - *) - echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write" - exit 0 - ;; - esac - - [ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; } - - SOURCE_DIR="src" - [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" - [ ! -d "$SOURCE_DIR" ] && exit 0 - - PORT="${DEV_PORT:-22}" - REMOTE="${DEV_PATH%/}" - [ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}" - - printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \ - "$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json - if [ -n "$DEV_KEY" ]; then - echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key - printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json - else - printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json - fi - - PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true) - if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then - php /tmp/mokostandards-api/deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json - elif [ -f "/tmp/mokostandards-api/deploy/deploy-sftp.php" ]; then - php /tmp/mokostandards-api/deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json - fi - rm -f /tmp/deploy_key /tmp/sftp-config.json - echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY - - - name: Summary - if: always() - run: | - echo "## Joomla Update Server" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY - echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Version | \`${DISPLAY_VERSION}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Element | \`${EXT_ELEMENT}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Download | [ZIP](${DOWNLOAD_URL}) |" >> $GITHUB_STEP_SUMMARY -- 2.52.0 From 14e43e7319da1bdb3651a9145e18050d0cfb259c Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:33 +0000 Subject: [PATCH 050/136] chore: move .github/CLAUDE.md to .mokogitea/CLAUDE.md [skip ci] --- .mokogitea/CLAUDE.md | 309 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 309 insertions(+) create mode 100644 .mokogitea/CLAUDE.md diff --git a/.mokogitea/CLAUDE.md b/.mokogitea/CLAUDE.md new file mode 100644 index 0000000..6b0c918 --- /dev/null +++ b/.mokogitea/CLAUDE.md @@ -0,0 +1,309 @@ + + +> [!IMPORTANT] +> **🔧 AI Self-Update Required on First Use** +> +> This file may contain unfilled `{{TOKEN}}` placeholders. The MokoStandards bulk-sync system +> replaces them automatically at sync time; if you can still see them, sync has not yet run for +> this repository — or this is a brand-new repo. +> +> **On your very first interaction in this repository, before answering any other question:** +> +> 1. Check whether any `{{TOKEN}}` placeholders remain in this file. +> 2. If they do, locate the values as described in the table below and replace every occurrence +> in **both** `.github/copilot-instructions.md` and `.github/CLAUDE.md`. +> 3. After replacing all tokens, **delete this entire notice block** (from `> [!IMPORTANT]` +> through the closing `> ---` line) in both files. +> 4. Tell the developer: *"I've self-updated the AI context files with this repository's +> values — all set."* +> +> | Placeholder | Where to find the value | +> |---|---| +> | `MokoJoomHero` | The GitHub repository name (visible in the URL, `README.md` heading, or `git remote -v`) | +> | `https://github.com/mokoconsulting-tech/MokoJoomHero` | Full GitHub URL, e.g. `https://github.com/mokoconsulting-tech/` | +> | `PHP` | The dominant programming language (check file extensions in the repository) | +> | `Generic project` | The project type: `PHP library`, `Joomla extension`, `Dolibarr module`, `WaaS site`, etc. — infer from repo structure | +> +> --- + +# MokoJoomHero — GitHub Copilot Custom Instructions + +## What This Repo Is + +This is a **Moko Consulting** repository governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). All coding standards, workflows, and policies are defined there and enforced here via bulk sync. + +Repository URL: https://github.com/mokoconsulting-tech/MokoJoomHero +Primary language: **PHP** +Platform type: **Generic project** + +--- + +## Primary Language + +**PHP is the primary language for this repository.** Follow the conventions documented in [MokoStandards coding-style-guide](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md). + +YAML uses 2-space indentation (spaces, not tabs). All other text files use tabs per `.editorconfig`. + +--- + +## File Header — Always Required on New Files + +Every new file needs a copyright header as its first content. Use the minimal form unless the file is a policy doc, README, or public API. + +**PHP:** +```php + + * + * This file is part of a Moko Consulting project. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: MokoJoomHero.Module + * INGROUP: MokoJoomHero + * REPO: https://github.com/mokoconsulting-tech/MokoJoomHero + * PATH: /path/to/file.php + * VERSION: XX.YY.ZZ + * BRIEF: One-line description of purpose + */ + +declare(strict_types=1); +``` + +**Markdown:** +```markdown + +``` + +**YAML / Shell:** Use `#` comments with the same fields. JSON files are exempt. + +--- + +## Version Management + +**`README.md` is the single source of truth for the repository version.** + +- **Bump the patch version on every PR** — increment `XX.YY.ZZ` (e.g. `01.02.03` → `01.02.04`) in `README.md` before opening the PR; the `sync-version-on-merge` workflow propagates it automatically to all badges and `FILE INFORMATION` headers on merge to `main`. +- The `VERSION: XX.YY.ZZ` field in the README.md `FILE INFORMATION` block governs all other version references. +- Update the version in `README.md` only — the `sync-version-on-merge` workflow propagates it automatically to all badges and `FILE INFORMATION` headers on merge to `main`. +- Version format is zero-padded semver: `XX.YY.ZZ` (e.g. `04.00.04`). +- Never hardcode a specific version in document body text — use the badge or FILE INFORMATION header only. + +--- + +## GitHub Actions — Token Usage + +Every workflow must use **`secrets.GH_TOKEN`** (the org-level Personal Access Token). This applies to all `actions/checkout`, `gh` CLI calls, and any step that talks to the GitHub API. + +```yaml +# ✅ Correct +- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.GH_TOKEN }} + +env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} +``` + +```yaml +# ❌ Wrong — never use these in workflows +token: ${{ github.token }} +token: ${{ secrets.GITHUB_TOKEN }} +``` + +PHP scripts read the token with: `getenv('GH_TOKEN') ?: getenv('GITHUB_TOKEN')` — `GH_TOKEN` is always preferred; `GITHUB_TOKEN` is accepted only as a local-dev fallback. + +--- + +## Composer Package (PHP repositories) + +This repository requires the MokoStandards enterprise library. The `composer.json` must include: + +```json +{ + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/mokoconsulting-tech/MokoStandards" + } + ], + "require": { + "mokoconsulting/mokostandards": "^4.0" + } +} +``` + +Run `composer install` after adding the dependency. See [package-installation.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/package-installation.md) for full instructions. + +--- + +## PHP Script Pattern + +All PHP scripts **must** extend `MokoStandards\Enterprise\CliFramework`. Never write standalone classes or extend the legacy `CliBase`. + +```php +#!/usr/bin/env php +setDescription('One-line description'); + $this->addArgument('--path', 'Repository root', '.'); + $this->addArgument('--dry-run', 'Preview without writing', false); + } + + protected function run(): int + { + $path = $this->getArgument('--path'); + $dryRun = (bool) $this->getArgument('--dry-run'); + + $this->log('INFO', "Processing: {$path}"); + return 0; + } +} + +$script = new MyScript('my_script', 'One-line description'); +exit($script->execute()); +``` + +**Key rules:** +- Abstract methods to implement: `configure()` and `run()` — **not** `execute()` +- `execute()` is the **public entry point** that orchestrates setup (arg parsing, `initialize()`) and then calls your `run()` implementation; call it at the bottom with `exit($script->execute())` +- Entry point at the bottom: `$script->execute()` — **not** `$script->run()` +- Constructor always takes `(string $name, string $description = '')`; pass the description here — `setDescription()` inside `configure()` is only needed to override it +- `log(string $level, string $message)` — level is the **first** argument (INFO / SUCCESS / WARNING / ERROR) +- `$this->dryRun` and `$this->verbose` are set automatically from `--dry-run` / `--verbose` + +--- + +## Naming Conventions + +| Context | Convention | Example | +|---------|-----------|---------| +| PHP class | `PascalCase` | `MyService` | +| PHP method / function | `camelCase` | `getUserData()` | +| PHP variable | `$snake_case` | `$repo_path` | +| PHP constant | `UPPER_SNAKE_CASE` | `DEFAULT_THRESHOLD` | +| PHP class file | `PascalCase.php` | `ApiClient.php` | +| PHP script file | `snake_case.php` | `check_health.php` | +| YAML workflow | `kebab-case.yml` | `bulk-repo-sync.yml` | +| Markdown doc | `kebab-case.md` | `coding-style-guide.md` | + +--- + +## Commit Messages + +Format: `(): ` — imperative, lower-case subject, no trailing period. + +Valid types: `feat` · `fix` · `docs` · `chore` · `ci` · `refactor` · `style` · `test` · `perf` · `revert` · `build` + +Examples: +- `feat(module): add user preference caching` +- `fix(api): handle null response from external service` +- `docs(readme): update installation instructions` +- `chore(deps): bump phpunit to 11.x` + +--- + +## Branch Naming + +Approved prefixes: `dev/` · `rc/` · `version/` · `copilot/` · `dependabot/` + +- `dev/XX.YY` or `dev/feature-name` — development (version optional) +- `rc/XX.YY.ZZ` — release candidate (three-part required) +- `version/XX.YY` — archive branch (auto-created, two-part) +- Release tags: `vXX` (major only — one release per major version) +- Patch `00` = development (no release), first release = `01` + +Examples: +- ✅ `dev/04.06` · `dev/new-dashboard` · `rc/04.06.01` +- ❌ `feature/my-thing` — rejected by branch protection + +--- + +## Keeping Documentation Current + +Whenever you make code changes, update the corresponding documentation in the same commit or PR. Do not leave docs stale. + +| Change type | Documentation to update | +|-------------|------------------------| +| New or renamed public PHP method | PHPDoc block on the method; `docs/api/` index for that class | +| New or changed CLI script argument | Script's own `--help` text; `docs/api/` or equivalent | +| New or changed GitHub Actions workflow | `docs/workflows/.md` | +| New or changed policy | Corresponding file under `docs/policy/` | +| New library class or major feature | `CHANGELOG.md` entry under `Added` | +| Bug fix | `CHANGELOG.md` entry under `Fixed` | +| Breaking change | `CHANGELOG.md` entry under `Changed`; update `CONTRIBUTING.md` if contributor steps change | +| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block | +| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it to all headers and badges on merge | + +If your code change makes any existing doc sentence false or incomplete, fix the doc before closing the PR. + +--- + +## Key Constraints + +- Never commit directly to `main` — all changes go via PR, squash-merged +- Never skip the FILE INFORMATION block on a new file +- Never use bare `catch (\Throwable $e) {}` without logging or re-throwing +- Never hardcode version numbers in body text — update `README.md` and let automation propagate +- Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows — always use `secrets.GH_TOKEN` +- Never extend `CliBase` in PHP scripts — extend `MokoStandards\Enterprise\CliFramework` +- Never call `$script->run()` as the entry point — call `$script->execute()` +- Policy documents and guides must not be mixed + +--- + +## MokoStandards Reference + +This repository is governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). Authoritative policies: + +| Document | Purpose | +|----------|---------| +| [file-header-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/file-header-standards.md) | Copyright-header rules for every file type | +| [coding-style-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md) | Naming and formatting conventions | +| [branching-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/branching-strategy.md) | Branch naming, hierarchy, and release workflow | +| [merge-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR title/body conventions | +| [changelog-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md | +| [scripting-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/scripting-standards.md) | PHP script requirements and CliFramework usage | +| [package-installation.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/package-installation.md) | Installing `mokoconsulting/mokostandards` via Composer | -- 2.52.0 From 84446e18e9e80294f0a580cfe95fa2654ca3fe08 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:34 +0000 Subject: [PATCH 051/136] chore: remove .github/CLAUDE.md (moved to .mokogitea/) [skip ci] --- .github/CLAUDE.md | 309 ---------------------------------------------- 1 file changed, 309 deletions(-) delete mode 100644 .github/CLAUDE.md diff --git a/.github/CLAUDE.md b/.github/CLAUDE.md deleted file mode 100644 index 6b0c918..0000000 --- a/.github/CLAUDE.md +++ /dev/null @@ -1,309 +0,0 @@ - - -> [!IMPORTANT] -> **🔧 AI Self-Update Required on First Use** -> -> This file may contain unfilled `{{TOKEN}}` placeholders. The MokoStandards bulk-sync system -> replaces them automatically at sync time; if you can still see them, sync has not yet run for -> this repository — or this is a brand-new repo. -> -> **On your very first interaction in this repository, before answering any other question:** -> -> 1. Check whether any `{{TOKEN}}` placeholders remain in this file. -> 2. If they do, locate the values as described in the table below and replace every occurrence -> in **both** `.github/copilot-instructions.md` and `.github/CLAUDE.md`. -> 3. After replacing all tokens, **delete this entire notice block** (from `> [!IMPORTANT]` -> through the closing `> ---` line) in both files. -> 4. Tell the developer: *"I've self-updated the AI context files with this repository's -> values — all set."* -> -> | Placeholder | Where to find the value | -> |---|---| -> | `MokoJoomHero` | The GitHub repository name (visible in the URL, `README.md` heading, or `git remote -v`) | -> | `https://github.com/mokoconsulting-tech/MokoJoomHero` | Full GitHub URL, e.g. `https://github.com/mokoconsulting-tech/` | -> | `PHP` | The dominant programming language (check file extensions in the repository) | -> | `Generic project` | The project type: `PHP library`, `Joomla extension`, `Dolibarr module`, `WaaS site`, etc. — infer from repo structure | -> -> --- - -# MokoJoomHero — GitHub Copilot Custom Instructions - -## What This Repo Is - -This is a **Moko Consulting** repository governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). All coding standards, workflows, and policies are defined there and enforced here via bulk sync. - -Repository URL: https://github.com/mokoconsulting-tech/MokoJoomHero -Primary language: **PHP** -Platform type: **Generic project** - ---- - -## Primary Language - -**PHP is the primary language for this repository.** Follow the conventions documented in [MokoStandards coding-style-guide](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md). - -YAML uses 2-space indentation (spaces, not tabs). All other text files use tabs per `.editorconfig`. - ---- - -## File Header — Always Required on New Files - -Every new file needs a copyright header as its first content. Use the minimal form unless the file is a policy doc, README, or public API. - -**PHP:** -```php - - * - * This file is part of a Moko Consulting project. - * - * SPDX-License-Identifier: GPL-3.0-or-later - * - * FILE INFORMATION - * DEFGROUP: MokoJoomHero.Module - * INGROUP: MokoJoomHero - * REPO: https://github.com/mokoconsulting-tech/MokoJoomHero - * PATH: /path/to/file.php - * VERSION: XX.YY.ZZ - * BRIEF: One-line description of purpose - */ - -declare(strict_types=1); -``` - -**Markdown:** -```markdown - -``` - -**YAML / Shell:** Use `#` comments with the same fields. JSON files are exempt. - ---- - -## Version Management - -**`README.md` is the single source of truth for the repository version.** - -- **Bump the patch version on every PR** — increment `XX.YY.ZZ` (e.g. `01.02.03` → `01.02.04`) in `README.md` before opening the PR; the `sync-version-on-merge` workflow propagates it automatically to all badges and `FILE INFORMATION` headers on merge to `main`. -- The `VERSION: XX.YY.ZZ` field in the README.md `FILE INFORMATION` block governs all other version references. -- Update the version in `README.md` only — the `sync-version-on-merge` workflow propagates it automatically to all badges and `FILE INFORMATION` headers on merge to `main`. -- Version format is zero-padded semver: `XX.YY.ZZ` (e.g. `04.00.04`). -- Never hardcode a specific version in document body text — use the badge or FILE INFORMATION header only. - ---- - -## GitHub Actions — Token Usage - -Every workflow must use **`secrets.GH_TOKEN`** (the org-level Personal Access Token). This applies to all `actions/checkout`, `gh` CLI calls, and any step that talks to the GitHub API. - -```yaml -# ✅ Correct -- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.GH_TOKEN }} - -env: - GH_TOKEN: ${{ secrets.GH_TOKEN }} -``` - -```yaml -# ❌ Wrong — never use these in workflows -token: ${{ github.token }} -token: ${{ secrets.GITHUB_TOKEN }} -``` - -PHP scripts read the token with: `getenv('GH_TOKEN') ?: getenv('GITHUB_TOKEN')` — `GH_TOKEN` is always preferred; `GITHUB_TOKEN` is accepted only as a local-dev fallback. - ---- - -## Composer Package (PHP repositories) - -This repository requires the MokoStandards enterprise library. The `composer.json` must include: - -```json -{ - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/mokoconsulting-tech/MokoStandards" - } - ], - "require": { - "mokoconsulting/mokostandards": "^4.0" - } -} -``` - -Run `composer install` after adding the dependency. See [package-installation.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/package-installation.md) for full instructions. - ---- - -## PHP Script Pattern - -All PHP scripts **must** extend `MokoStandards\Enterprise\CliFramework`. Never write standalone classes or extend the legacy `CliBase`. - -```php -#!/usr/bin/env php -setDescription('One-line description'); - $this->addArgument('--path', 'Repository root', '.'); - $this->addArgument('--dry-run', 'Preview without writing', false); - } - - protected function run(): int - { - $path = $this->getArgument('--path'); - $dryRun = (bool) $this->getArgument('--dry-run'); - - $this->log('INFO', "Processing: {$path}"); - return 0; - } -} - -$script = new MyScript('my_script', 'One-line description'); -exit($script->execute()); -``` - -**Key rules:** -- Abstract methods to implement: `configure()` and `run()` — **not** `execute()` -- `execute()` is the **public entry point** that orchestrates setup (arg parsing, `initialize()`) and then calls your `run()` implementation; call it at the bottom with `exit($script->execute())` -- Entry point at the bottom: `$script->execute()` — **not** `$script->run()` -- Constructor always takes `(string $name, string $description = '')`; pass the description here — `setDescription()` inside `configure()` is only needed to override it -- `log(string $level, string $message)` — level is the **first** argument (INFO / SUCCESS / WARNING / ERROR) -- `$this->dryRun` and `$this->verbose` are set automatically from `--dry-run` / `--verbose` - ---- - -## Naming Conventions - -| Context | Convention | Example | -|---------|-----------|---------| -| PHP class | `PascalCase` | `MyService` | -| PHP method / function | `camelCase` | `getUserData()` | -| PHP variable | `$snake_case` | `$repo_path` | -| PHP constant | `UPPER_SNAKE_CASE` | `DEFAULT_THRESHOLD` | -| PHP class file | `PascalCase.php` | `ApiClient.php` | -| PHP script file | `snake_case.php` | `check_health.php` | -| YAML workflow | `kebab-case.yml` | `bulk-repo-sync.yml` | -| Markdown doc | `kebab-case.md` | `coding-style-guide.md` | - ---- - -## Commit Messages - -Format: `(): ` — imperative, lower-case subject, no trailing period. - -Valid types: `feat` · `fix` · `docs` · `chore` · `ci` · `refactor` · `style` · `test` · `perf` · `revert` · `build` - -Examples: -- `feat(module): add user preference caching` -- `fix(api): handle null response from external service` -- `docs(readme): update installation instructions` -- `chore(deps): bump phpunit to 11.x` - ---- - -## Branch Naming - -Approved prefixes: `dev/` · `rc/` · `version/` · `copilot/` · `dependabot/` - -- `dev/XX.YY` or `dev/feature-name` — development (version optional) -- `rc/XX.YY.ZZ` — release candidate (three-part required) -- `version/XX.YY` — archive branch (auto-created, two-part) -- Release tags: `vXX` (major only — one release per major version) -- Patch `00` = development (no release), first release = `01` - -Examples: -- ✅ `dev/04.06` · `dev/new-dashboard` · `rc/04.06.01` -- ❌ `feature/my-thing` — rejected by branch protection - ---- - -## Keeping Documentation Current - -Whenever you make code changes, update the corresponding documentation in the same commit or PR. Do not leave docs stale. - -| Change type | Documentation to update | -|-------------|------------------------| -| New or renamed public PHP method | PHPDoc block on the method; `docs/api/` index for that class | -| New or changed CLI script argument | Script's own `--help` text; `docs/api/` or equivalent | -| New or changed GitHub Actions workflow | `docs/workflows/.md` | -| New or changed policy | Corresponding file under `docs/policy/` | -| New library class or major feature | `CHANGELOG.md` entry under `Added` | -| Bug fix | `CHANGELOG.md` entry under `Fixed` | -| Breaking change | `CHANGELOG.md` entry under `Changed`; update `CONTRIBUTING.md` if contributor steps change | -| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block | -| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it to all headers and badges on merge | - -If your code change makes any existing doc sentence false or incomplete, fix the doc before closing the PR. - ---- - -## Key Constraints - -- Never commit directly to `main` — all changes go via PR, squash-merged -- Never skip the FILE INFORMATION block on a new file -- Never use bare `catch (\Throwable $e) {}` without logging or re-throwing -- Never hardcode version numbers in body text — update `README.md` and let automation propagate -- Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows — always use `secrets.GH_TOKEN` -- Never extend `CliBase` in PHP scripts — extend `MokoStandards\Enterprise\CliFramework` -- Never call `$script->run()` as the entry point — call `$script->execute()` -- Policy documents and guides must not be mixed - ---- - -## MokoStandards Reference - -This repository is governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). Authoritative policies: - -| Document | Purpose | -|----------|---------| -| [file-header-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/file-header-standards.md) | Copyright-header rules for every file type | -| [coding-style-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md) | Naming and formatting conventions | -| [branching-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/branching-strategy.md) | Branch naming, hierarchy, and release workflow | -| [merge-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR title/body conventions | -| [changelog-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md | -| [scripting-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/scripting-standards.md) | PHP script requirements and CliFramework usage | -| [package-installation.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/package-installation.md) | Installing `mokoconsulting/mokostandards` via Composer | -- 2.52.0 From fb384dac8e897afb0a3052973bac65ca6efd7bbc Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:34 +0000 Subject: [PATCH 052/136] chore: move .github/CODEOWNERS to .mokogitea/CODEOWNERS [skip ci] --- .mokogitea/CODEOWNERS | 54 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .mokogitea/CODEOWNERS diff --git a/.mokogitea/CODEOWNERS b/.mokogitea/CODEOWNERS new file mode 100644 index 0000000..0108cc2 --- /dev/null +++ b/.mokogitea/CODEOWNERS @@ -0,0 +1,54 @@ +# Copyright (C) 2026 Moko Consulting +# SPDX-License-Identifier: GPL-3.0-or-later +# +# CODEOWNERS — require approval from jmiller-moko for protected paths +# Synced from MokoStandards. Do not edit manually. +# +# Changes to these paths require review from the listed owners before merge. +# Combined with branch protection (require PR reviews), this prevents +# unauthorized modifications to workflows, configs, and governance files. + +# ── Synced workflows (managed by MokoStandards — do not edit manually) ──── +/.github/workflows/deploy-dev.yml @jmiller-moko +/.github/workflows/deploy-demo.yml @jmiller-moko +/.github/workflows/deploy-manual.yml @jmiller-moko +/.github/workflows/auto-release.yml @jmiller-moko +/.github/workflows/auto-dev-issue.yml @jmiller-moko +/.github/workflows/auto-assign.yml @jmiller-moko +/.github/workflows/sync-version-on-merge.yml @jmiller-moko +/.github/workflows/enterprise-firewall-setup.yml @jmiller-moko +/.github/workflows/repository-cleanup.yml @jmiller-moko +/.github/workflows/standards-compliance.yml @jmiller-moko +/.github/workflows/codeql-analysis.yml @jmiller-moko +/.github/workflows/repo_health.yml @jmiller-moko +/.github/workflows/ci-joomla.yml @jmiller-moko +/.github/workflows/update-server.yml @jmiller-moko +/.github/workflows/deploy-manual.yml @jmiller-moko +/.github/workflows/ci-dolibarr.yml @jmiller-moko +/.github/workflows/publish-to-mokodolimods.yml @jmiller-moko +/.github/workflows/changelog-validation.yml @jmiller-moko +# Custom workflows in .github/workflows/ not listed above are repo-owned. + +# ── GitHub configuration ───────────────────────────────────────────────── +/.github/ISSUE_TEMPLATE/ @jmiller-moko +/.github/CODEOWNERS @jmiller-moko +/.github/copilot.yml @jmiller-moko +/.github/copilot-instructions.md @jmiller-moko +/.github/CLAUDE.md @jmiller-moko +/.github/.mokostandards @jmiller-moko + +# ── Build and config files ─────────────────────────────────────────────── +/composer.json @jmiller-moko +/phpstan.neon @jmiller-moko +/Makefile @jmiller-moko +/.ftpignore @jmiller-moko +/.gitignore @jmiller-moko +/.gitattributes @jmiller-moko +/.editorconfig @jmiller-moko + +# ── Governance documents ───────────────────────────────────────────────── +/LICENSE @jmiller-moko +/CONTRIBUTING.md @jmiller-moko +/SECURITY.md @jmiller-moko +/GOVERNANCE.md @jmiller-moko +/CODE_OF_CONDUCT.md @jmiller-moko -- 2.52.0 From e2381a56b70a52d61774f7c2510589f892da2483 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:34 +0000 Subject: [PATCH 053/136] chore: remove .github/CODEOWNERS (moved to .mokogitea/) [skip ci] --- .github/CODEOWNERS | 54 ---------------------------------------------- 1 file changed, 54 deletions(-) delete mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index 0108cc2..0000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# SPDX-License-Identifier: GPL-3.0-or-later -# -# CODEOWNERS — require approval from jmiller-moko for protected paths -# Synced from MokoStandards. Do not edit manually. -# -# Changes to these paths require review from the listed owners before merge. -# Combined with branch protection (require PR reviews), this prevents -# unauthorized modifications to workflows, configs, and governance files. - -# ── Synced workflows (managed by MokoStandards — do not edit manually) ──── -/.github/workflows/deploy-dev.yml @jmiller-moko -/.github/workflows/deploy-demo.yml @jmiller-moko -/.github/workflows/deploy-manual.yml @jmiller-moko -/.github/workflows/auto-release.yml @jmiller-moko -/.github/workflows/auto-dev-issue.yml @jmiller-moko -/.github/workflows/auto-assign.yml @jmiller-moko -/.github/workflows/sync-version-on-merge.yml @jmiller-moko -/.github/workflows/enterprise-firewall-setup.yml @jmiller-moko -/.github/workflows/repository-cleanup.yml @jmiller-moko -/.github/workflows/standards-compliance.yml @jmiller-moko -/.github/workflows/codeql-analysis.yml @jmiller-moko -/.github/workflows/repo_health.yml @jmiller-moko -/.github/workflows/ci-joomla.yml @jmiller-moko -/.github/workflows/update-server.yml @jmiller-moko -/.github/workflows/deploy-manual.yml @jmiller-moko -/.github/workflows/ci-dolibarr.yml @jmiller-moko -/.github/workflows/publish-to-mokodolimods.yml @jmiller-moko -/.github/workflows/changelog-validation.yml @jmiller-moko -# Custom workflows in .github/workflows/ not listed above are repo-owned. - -# ── GitHub configuration ───────────────────────────────────────────────── -/.github/ISSUE_TEMPLATE/ @jmiller-moko -/.github/CODEOWNERS @jmiller-moko -/.github/copilot.yml @jmiller-moko -/.github/copilot-instructions.md @jmiller-moko -/.github/CLAUDE.md @jmiller-moko -/.github/.mokostandards @jmiller-moko - -# ── Build and config files ─────────────────────────────────────────────── -/composer.json @jmiller-moko -/phpstan.neon @jmiller-moko -/Makefile @jmiller-moko -/.ftpignore @jmiller-moko -/.gitignore @jmiller-moko -/.gitattributes @jmiller-moko -/.editorconfig @jmiller-moko - -# ── Governance documents ───────────────────────────────────────────────── -/LICENSE @jmiller-moko -/CONTRIBUTING.md @jmiller-moko -/SECURITY.md @jmiller-moko -/GOVERNANCE.md @jmiller-moko -/CODE_OF_CONDUCT.md @jmiller-moko -- 2.52.0 From da505fe31bef221c341dc724bab88cc2a5f99b29 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:35 +0000 Subject: [PATCH 054/136] chore: move .github/ISSUE_TEMPLATE/adr.md to .mokogitea/adr.md [skip ci] --- .mokogitea/adr.md | 110 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 .mokogitea/adr.md diff --git a/.mokogitea/adr.md b/.mokogitea/adr.md new file mode 100644 index 0000000..eb40760 --- /dev/null +++ b/.mokogitea/adr.md @@ -0,0 +1,110 @@ +--- +name: Architecture Decision Record (ADR) +about: Propose or document an architectural decision +title: '[ADR] ' +labels: 'architecture, decision' +assignees: '' + +--- + + +## ADR Number +ADR-XXXX + +## Status +- [ ] Proposed +- [ ] Accepted +- [ ] Deprecated +- [ ] Superseded by ADR-XXXX + +## Context +Describe the issue or problem that motivates this decision. + +## Decision +State the architecture decision and provide rationale. + +## Consequences +### Positive +- List positive consequences + +### Negative +- List negative consequences or trade-offs + +### Neutral +- List neutral aspects + +## Alternatives Considered +### Alternative 1 +- Description +- Pros +- Cons +- Why not chosen + +### Alternative 2 +- Description +- Pros +- Cons +- Why not chosen + +## Implementation Plan +1. Step 1 +2. Step 2 +3. Step 3 + +## Stakeholders +- **Decision Makers**: @user1, @user2 +- **Consulted**: @user3, @user4 +- **Informed**: team-name + +## Technical Details +### Architecture Diagram +``` +[Add diagram or link] +``` + +### Dependencies +- Dependency 1 +- Dependency 2 + +### Impact Analysis +- **Performance**: [Impact description] +- **Security**: [Impact description] +- **Scalability**: [Impact description] +- **Maintainability**: [Impact description] + +## Testing Strategy +- [ ] Unit tests +- [ ] Integration tests +- [ ] Performance tests +- [ ] Security tests + +## Documentation +- [ ] Architecture documentation updated +- [ ] API documentation updated +- [ ] Developer guide updated +- [ ] Runbook created + +## Migration Path +Describe how to migrate from current state to new architecture. + +## Rollback Plan +Describe how to rollback if issues occur. + +## Timeline +- **Proposal Date**: +- **Decision Date**: +- **Implementation Start**: +- **Expected Completion**: + +## References +- Related ADRs: +- External resources: +- RFCs: + +## Review Checklist +- [ ] Aligns with enterprise architecture principles +- [ ] Security implications reviewed +- [ ] Performance implications reviewed +- [ ] Cost implications reviewed +- [ ] Compliance requirements met +- [ ] Team consensus achieved -- 2.52.0 From e00f1d9e601ee385a29fc3ef5aa7f80941ee711a Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:35 +0000 Subject: [PATCH 055/136] chore: remove .github/ISSUE_TEMPLATE/adr.md (moved to .mokogitea/) [skip ci] --- .github/ISSUE_TEMPLATE/adr.md | 110 ---------------------------------- 1 file changed, 110 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/adr.md diff --git a/.github/ISSUE_TEMPLATE/adr.md b/.github/ISSUE_TEMPLATE/adr.md deleted file mode 100644 index eb40760..0000000 --- a/.github/ISSUE_TEMPLATE/adr.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -name: Architecture Decision Record (ADR) -about: Propose or document an architectural decision -title: '[ADR] ' -labels: 'architecture, decision' -assignees: '' - ---- - - -## ADR Number -ADR-XXXX - -## Status -- [ ] Proposed -- [ ] Accepted -- [ ] Deprecated -- [ ] Superseded by ADR-XXXX - -## Context -Describe the issue or problem that motivates this decision. - -## Decision -State the architecture decision and provide rationale. - -## Consequences -### Positive -- List positive consequences - -### Negative -- List negative consequences or trade-offs - -### Neutral -- List neutral aspects - -## Alternatives Considered -### Alternative 1 -- Description -- Pros -- Cons -- Why not chosen - -### Alternative 2 -- Description -- Pros -- Cons -- Why not chosen - -## Implementation Plan -1. Step 1 -2. Step 2 -3. Step 3 - -## Stakeholders -- **Decision Makers**: @user1, @user2 -- **Consulted**: @user3, @user4 -- **Informed**: team-name - -## Technical Details -### Architecture Diagram -``` -[Add diagram or link] -``` - -### Dependencies -- Dependency 1 -- Dependency 2 - -### Impact Analysis -- **Performance**: [Impact description] -- **Security**: [Impact description] -- **Scalability**: [Impact description] -- **Maintainability**: [Impact description] - -## Testing Strategy -- [ ] Unit tests -- [ ] Integration tests -- [ ] Performance tests -- [ ] Security tests - -## Documentation -- [ ] Architecture documentation updated -- [ ] API documentation updated -- [ ] Developer guide updated -- [ ] Runbook created - -## Migration Path -Describe how to migrate from current state to new architecture. - -## Rollback Plan -Describe how to rollback if issues occur. - -## Timeline -- **Proposal Date**: -- **Decision Date**: -- **Implementation Start**: -- **Expected Completion**: - -## References -- Related ADRs: -- External resources: -- RFCs: - -## Review Checklist -- [ ] Aligns with enterprise architecture principles -- [ ] Security implications reviewed -- [ ] Performance implications reviewed -- [ ] Cost implications reviewed -- [ ] Compliance requirements met -- [ ] Team consensus achieved -- 2.52.0 From ddb167ad9cd229612e2b1c786dd9e6f6e8526223 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:36 +0000 Subject: [PATCH 056/136] chore: move .github/ISSUE_TEMPLATE/bug_report.md to .mokogitea/bug_report.md [skip ci] --- .mokogitea/bug_report.md | 48 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .mokogitea/bug_report.md diff --git a/.mokogitea/bug_report.md b/.mokogitea/bug_report.md new file mode 100644 index 0000000..38a16a7 --- /dev/null +++ b/.mokogitea/bug_report.md @@ -0,0 +1,48 @@ +--- +name: Bug Report +about: Report a bug or issue with the project +title: '[BUG] ' +labels: 'bug' +assignees: '' + +--- + + +## Bug Description +A clear and concise description of what the bug is. + +## Steps to Reproduce +1. Go to '...' +2. Click on '...' +3. Scroll down to '...' +4. See error + +## Expected Behavior +A clear and concise description of what you expected to happen. + +## Actual Behavior +A clear and concise description of what actually happened. + +## Screenshots +If applicable, add screenshots to help explain your problem. + +## Environment +- **Project**: [e.g., MokoDoliTools, moko-cassiopeia] +- **Version**: [e.g., 1.2.3] +- **Platform**: [e.g., Dolibarr 18.0, Joomla 5.0] +- **PHP Version**: [e.g., 8.1] +- **Database**: [e.g., MySQL 8.0, PostgreSQL 14] +- **Browser** (if applicable): [e.g., Chrome 120, Firefox 121] +- **OS**: [e.g., Ubuntu 22.04, Windows 11] + +## Additional Context +Add any other context about the problem here. + +## Possible Solution +If you have suggestions on how to fix the issue, please describe them here. + +## Checklist +- [ ] I have searched for similar issues before creating this one +- [ ] I have provided all the requested information +- [ ] I have tested this on the latest stable version +- [ ] I have checked the documentation and couldn't find a solution -- 2.52.0 From 9740dfcb976c76df7ec5b65a9041539e397ae4e5 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:36 +0000 Subject: [PATCH 057/136] chore: remove .github/ISSUE_TEMPLATE/bug_report.md (moved to .mokogitea/) [skip ci] --- .github/ISSUE_TEMPLATE/bug_report.md | 48 ---------------------------- 1 file changed, 48 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 38a16a7..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -name: Bug Report -about: Report a bug or issue with the project -title: '[BUG] ' -labels: 'bug' -assignees: '' - ---- - - -## Bug Description -A clear and concise description of what the bug is. - -## Steps to Reproduce -1. Go to '...' -2. Click on '...' -3. Scroll down to '...' -4. See error - -## Expected Behavior -A clear and concise description of what you expected to happen. - -## Actual Behavior -A clear and concise description of what actually happened. - -## Screenshots -If applicable, add screenshots to help explain your problem. - -## Environment -- **Project**: [e.g., MokoDoliTools, moko-cassiopeia] -- **Version**: [e.g., 1.2.3] -- **Platform**: [e.g., Dolibarr 18.0, Joomla 5.0] -- **PHP Version**: [e.g., 8.1] -- **Database**: [e.g., MySQL 8.0, PostgreSQL 14] -- **Browser** (if applicable): [e.g., Chrome 120, Firefox 121] -- **OS**: [e.g., Ubuntu 22.04, Windows 11] - -## Additional Context -Add any other context about the problem here. - -## Possible Solution -If you have suggestions on how to fix the issue, please describe them here. - -## Checklist -- [ ] I have searched for similar issues before creating this one -- [ ] I have provided all the requested information -- [ ] I have tested this on the latest stable version -- [ ] I have checked the documentation and couldn't find a solution -- 2.52.0 From d6104f2baa1050c0014ace48337a017c154521f5 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:36 +0000 Subject: [PATCH 058/136] chore: move .github/ISSUE_TEMPLATE/config.yml to .mokogitea/config.yml [skip ci] --- .mokogitea/config.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .mokogitea/config.yml diff --git a/.mokogitea/config.yml b/.mokogitea/config.yml new file mode 100644 index 0000000..7edc8bc --- /dev/null +++ b/.mokogitea/config.yml @@ -0,0 +1,18 @@ +--- +blank_issues_enabled: true +contact_links: + - name: 💼 Enterprise Support + url: https://mokoconsulting.tech/enterprise + about: Enterprise-level support and consultation services + - name: 💬 Ask a Question + url: https://mokoconsulting.tech/ + about: Get help or ask questions through our website + - name: 📚 MokoStandards Documentation + url: https://github.com/mokoconsulting-tech/MokoStandards + about: View our coding standards and best practices + - name: 🔒 Report a Security Vulnerability + url: https://github.com/mokoconsulting-tech/.github-private/security/advisories/new + about: Report security vulnerabilities privately (for critical issues) + - name: 💡 Community Discussions + url: https://github.com/orgs/mokoconsulting-tech/discussions + about: Join community discussions and Q&A -- 2.52.0 From 1b81eb2c17cb077f6fc75c964be0ebc7e86881c7 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:37 +0000 Subject: [PATCH 059/136] chore: remove .github/ISSUE_TEMPLATE/config.yml (moved to .mokogitea/) [skip ci] --- .github/ISSUE_TEMPLATE/config.yml | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 7edc8bc..0000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,18 +0,0 @@ ---- -blank_issues_enabled: true -contact_links: - - name: 💼 Enterprise Support - url: https://mokoconsulting.tech/enterprise - about: Enterprise-level support and consultation services - - name: 💬 Ask a Question - url: https://mokoconsulting.tech/ - about: Get help or ask questions through our website - - name: 📚 MokoStandards Documentation - url: https://github.com/mokoconsulting-tech/MokoStandards - about: View our coding standards and best practices - - name: 🔒 Report a Security Vulnerability - url: https://github.com/mokoconsulting-tech/.github-private/security/advisories/new - about: Report security vulnerabilities privately (for critical issues) - - name: 💡 Community Discussions - url: https://github.com/orgs/mokoconsulting-tech/discussions - about: Join community discussions and Q&A -- 2.52.0 From 51dec067ef37bbd48935aef9e05866480aaf91dc Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:37 +0000 Subject: [PATCH 060/136] chore: move .github/ISSUE_TEMPLATE/documentation.md to .mokogitea/documentation.md [skip ci] --- .mokogitea/documentation.md | 52 +++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .mokogitea/documentation.md diff --git a/.mokogitea/documentation.md b/.mokogitea/documentation.md new file mode 100644 index 0000000..ed4dabc --- /dev/null +++ b/.mokogitea/documentation.md @@ -0,0 +1,52 @@ +--- +name: Documentation Issue +about: Report an issue with documentation +title: '[DOCS] ' +labels: 'documentation' +assignees: '' + +--- + + +## Documentation Issue + +**Location**: + + +## Issue Type + +- [ ] Typo or grammar error +- [ ] Outdated information +- [ ] Missing documentation +- [ ] Unclear explanation +- [ ] Broken links +- [ ] Missing examples +- [ ] Other (specify below) + +## Description + + +## Current Content + +``` +Current text here +``` + +## Suggested Improvement + +``` +Suggested text here +``` + +## Additional Context + + +## Standards Alignment +- [ ] Follows MokoStandards documentation guidelines +- [ ] Uses en_US/en_GB localization +- [ ] Includes proper SPDX headers where applicable + +## Checklist +- [ ] I have searched for similar documentation issues +- [ ] I have provided a clear description +- [ ] I have suggested an improvement (if applicable) -- 2.52.0 From d48caa0367ad72b81f568a28811ae4938c73315b Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:37 +0000 Subject: [PATCH 061/136] chore: remove .github/ISSUE_TEMPLATE/documentation.md (moved to .mokogitea/) [skip ci] --- .github/ISSUE_TEMPLATE/documentation.md | 52 ------------------------- 1 file changed, 52 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/documentation.md diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.md deleted file mode 100644 index ed4dabc..0000000 --- a/.github/ISSUE_TEMPLATE/documentation.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -name: Documentation Issue -about: Report an issue with documentation -title: '[DOCS] ' -labels: 'documentation' -assignees: '' - ---- - - -## Documentation Issue - -**Location**: - - -## Issue Type - -- [ ] Typo or grammar error -- [ ] Outdated information -- [ ] Missing documentation -- [ ] Unclear explanation -- [ ] Broken links -- [ ] Missing examples -- [ ] Other (specify below) - -## Description - - -## Current Content - -``` -Current text here -``` - -## Suggested Improvement - -``` -Suggested text here -``` - -## Additional Context - - -## Standards Alignment -- [ ] Follows MokoStandards documentation guidelines -- [ ] Uses en_US/en_GB localization -- [ ] Includes proper SPDX headers where applicable - -## Checklist -- [ ] I have searched for similar documentation issues -- [ ] I have provided a clear description -- [ ] I have suggested an improvement (if applicable) -- 2.52.0 From e2cb5cb0ac650063e898d75839d86427fbd55c33 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:38 +0000 Subject: [PATCH 062/136] chore: move .github/ISSUE_TEMPLATE/enterprise_support.md to .mokogitea/enterprise_support.md [skip ci] --- .mokogitea/enterprise_support.md | 85 ++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 .mokogitea/enterprise_support.md diff --git a/.mokogitea/enterprise_support.md b/.mokogitea/enterprise_support.md new file mode 100644 index 0000000..4c3f0b4 --- /dev/null +++ b/.mokogitea/enterprise_support.md @@ -0,0 +1,85 @@ +--- +name: Enterprise Support Request +about: Request enterprise-level support or consultation +title: '[ENTERPRISE] ' +labels: 'enterprise, support' +assignees: '' + +--- + + +## Support Request Type +- [ ] Critical Production Issue +- [ ] Performance Optimization +- [ ] Security Audit +- [ ] Architecture Review +- [ ] Custom Development +- [ ] Migration Support +- [ ] Training & Onboarding +- [ ] Other (please specify) + +## Priority Level +- [ ] P0 - Critical (Production Down) +- [ ] P1 - High (Major Feature Broken) +- [ ] P2 - Medium (Non-Critical Issue) +- [ ] P3 - Low (Enhancement/Question) + +## Organization Details +- **Company Name**: +- **Contact Person**: +- **Email**: +- **Phone** (for P0/P1 issues): +- **Timezone**: + +## Issue Description +Provide a clear and detailed description of your request or issue. + +## Business Impact +Describe the impact on your business operations: +- Number of users affected: +- Revenue impact (if applicable): +- Deadline/SLA requirements: + +## Environment Details +- **Deployment Type**: [On-Premise / Cloud / Hybrid] +- **Platform**: [Joomla / Dolibarr / Custom] +- **Version**: +- **Infrastructure**: [AWS / Azure / GCP / Other] +- **Scale**: [Users / Transactions / Data Volume] + +## Current Configuration +```yaml +# Paste relevant configuration (sanitize sensitive data) +``` + +## Logs and Diagnostics +``` +# Paste relevant logs (sanitize sensitive data) +``` + +## Attempted Solutions +Describe any troubleshooting steps already taken. + +## Expected Resolution +Describe your expected outcome or resolution. + +## Additional Resources +- **Documentation Links**: +- **Related Issues**: +- **Screenshots/Videos**: + +## Enterprise SLA +- [ ] Standard Support (initial response within 1–3 weeks) +- [ ] Premium Support (initial response within 5 business days) +- [ ] Critical Support (initial response within 72 hours) +- [ ] Custom SLA (specify): + +## Compliance Requirements +- [ ] GDPR +- [ ] HIPAA +- [ ] SOC 2 +- [ ] ISO 27001 +- [ ] Other (specify): + +--- +**Note**: Enterprise support requests require an active support contract. If you don't have one, please contact us at enterprise@mokoconsulting.tech -- 2.52.0 From 20d00db44c9ca4895d80f33a19b9579518d3e738 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:38 +0000 Subject: [PATCH 063/136] chore: remove .github/ISSUE_TEMPLATE/enterprise_support.md (moved to .mokogitea/) [skip ci] --- .github/ISSUE_TEMPLATE/enterprise_support.md | 85 -------------------- 1 file changed, 85 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/enterprise_support.md diff --git a/.github/ISSUE_TEMPLATE/enterprise_support.md b/.github/ISSUE_TEMPLATE/enterprise_support.md deleted file mode 100644 index 4c3f0b4..0000000 --- a/.github/ISSUE_TEMPLATE/enterprise_support.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -name: Enterprise Support Request -about: Request enterprise-level support or consultation -title: '[ENTERPRISE] ' -labels: 'enterprise, support' -assignees: '' - ---- - - -## Support Request Type -- [ ] Critical Production Issue -- [ ] Performance Optimization -- [ ] Security Audit -- [ ] Architecture Review -- [ ] Custom Development -- [ ] Migration Support -- [ ] Training & Onboarding -- [ ] Other (please specify) - -## Priority Level -- [ ] P0 - Critical (Production Down) -- [ ] P1 - High (Major Feature Broken) -- [ ] P2 - Medium (Non-Critical Issue) -- [ ] P3 - Low (Enhancement/Question) - -## Organization Details -- **Company Name**: -- **Contact Person**: -- **Email**: -- **Phone** (for P0/P1 issues): -- **Timezone**: - -## Issue Description -Provide a clear and detailed description of your request or issue. - -## Business Impact -Describe the impact on your business operations: -- Number of users affected: -- Revenue impact (if applicable): -- Deadline/SLA requirements: - -## Environment Details -- **Deployment Type**: [On-Premise / Cloud / Hybrid] -- **Platform**: [Joomla / Dolibarr / Custom] -- **Version**: -- **Infrastructure**: [AWS / Azure / GCP / Other] -- **Scale**: [Users / Transactions / Data Volume] - -## Current Configuration -```yaml -# Paste relevant configuration (sanitize sensitive data) -``` - -## Logs and Diagnostics -``` -# Paste relevant logs (sanitize sensitive data) -``` - -## Attempted Solutions -Describe any troubleshooting steps already taken. - -## Expected Resolution -Describe your expected outcome or resolution. - -## Additional Resources -- **Documentation Links**: -- **Related Issues**: -- **Screenshots/Videos**: - -## Enterprise SLA -- [ ] Standard Support (initial response within 1–3 weeks) -- [ ] Premium Support (initial response within 5 business days) -- [ ] Critical Support (initial response within 72 hours) -- [ ] Custom SLA (specify): - -## Compliance Requirements -- [ ] GDPR -- [ ] HIPAA -- [ ] SOC 2 -- [ ] ISO 27001 -- [ ] Other (specify): - ---- -**Note**: Enterprise support requests require an active support contract. If you don't have one, please contact us at enterprise@mokoconsulting.tech -- 2.52.0 From f53369673a901b25c5a41d4ed85731e33a756b36 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:39 +0000 Subject: [PATCH 064/136] chore: move .github/ISSUE_TEMPLATE/feature_request.md to .mokogitea/feature_request.md [skip ci] --- .mokogitea/feature_request.md | 51 +++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .mokogitea/feature_request.md diff --git a/.mokogitea/feature_request.md b/.mokogitea/feature_request.md new file mode 100644 index 0000000..4c5fd3c --- /dev/null +++ b/.mokogitea/feature_request.md @@ -0,0 +1,51 @@ +--- +name: Feature Request +about: Suggest a new feature or enhancement +title: '[FEATURE] ' +labels: 'enhancement' +assignees: '' + +--- + + +## Feature Description +A clear and concise description of the feature you'd like to see. + +## Problem or Use Case +Describe the problem this feature would solve or the use case it addresses. +Ex. I'm always frustrated when [...] + +## Proposed Solution +A clear and concise description of what you want to happen. + +## Alternative Solutions +A clear and concise description of any alternative solutions or features you've considered. + +## Benefits +Describe how this feature would benefit users: +- Who would use this feature? +- What problems does it solve? +- What value does it add? + +## Implementation Details (Optional) +If you have ideas about how this could be implemented, share them here: +- Technical approach +- Files/components that might need changes +- Any concerns or challenges you foresee + +## Additional Context +Add any other context, mockups, or screenshots about the feature request here. + +## Relevant Standards +Does this relate to any standards in [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards)? +- [ ] Accessibility (WCAG 2.1 AA) +- [ ] Localization (en_US/en_GB) +- [ ] Security best practices +- [ ] Code quality standards +- [ ] Other: [specify] + +## Checklist +- [ ] I have searched for similar feature requests before creating this one +- [ ] I have clearly described the use case and benefits +- [ ] I have considered alternative solutions +- [ ] This feature aligns with the project's goals and scope -- 2.52.0 From 370b791e9d15ed7dc3acf2c3b047e4fdc02d8f91 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:39 +0000 Subject: [PATCH 065/136] chore: remove .github/ISSUE_TEMPLATE/feature_request.md (moved to .mokogitea/) [skip ci] --- .github/ISSUE_TEMPLATE/feature_request.md | 51 ----------------------- 1 file changed, 51 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 4c5fd3c..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -name: Feature Request -about: Suggest a new feature or enhancement -title: '[FEATURE] ' -labels: 'enhancement' -assignees: '' - ---- - - -## Feature Description -A clear and concise description of the feature you'd like to see. - -## Problem or Use Case -Describe the problem this feature would solve or the use case it addresses. -Ex. I'm always frustrated when [...] - -## Proposed Solution -A clear and concise description of what you want to happen. - -## Alternative Solutions -A clear and concise description of any alternative solutions or features you've considered. - -## Benefits -Describe how this feature would benefit users: -- Who would use this feature? -- What problems does it solve? -- What value does it add? - -## Implementation Details (Optional) -If you have ideas about how this could be implemented, share them here: -- Technical approach -- Files/components that might need changes -- Any concerns or challenges you foresee - -## Additional Context -Add any other context, mockups, or screenshots about the feature request here. - -## Relevant Standards -Does this relate to any standards in [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards)? -- [ ] Accessibility (WCAG 2.1 AA) -- [ ] Localization (en_US/en_GB) -- [ ] Security best practices -- [ ] Code quality standards -- [ ] Other: [specify] - -## Checklist -- [ ] I have searched for similar feature requests before creating this one -- [ ] I have clearly described the use case and benefits -- [ ] I have considered alternative solutions -- [ ] This feature aligns with the project's goals and scope -- 2.52.0 From 5e451d4520614a70e42b07e9ef92de581af0c3b5 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:39 +0000 Subject: [PATCH 066/136] chore: move .github/ISSUE_TEMPLATE/firewall-request.md to .mokogitea/firewall-request.md [skip ci] --- .mokogitea/firewall-request.md | 190 +++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 .mokogitea/firewall-request.md diff --git a/.mokogitea/firewall-request.md b/.mokogitea/firewall-request.md new file mode 100644 index 0000000..4a43395 --- /dev/null +++ b/.mokogitea/firewall-request.md @@ -0,0 +1,190 @@ +--- +name: Firewall Request +about: Request firewall rule changes or access to external resources +title: '[FIREWALL] [Resource Name] - [Brief Description]' +labels: ['firewall-request', 'infrastructure', 'security'] +assignees: [] +--- + + +## Firewall Request + +### Request Type +- [ ] Allow outbound access to external service/API +- [ ] Allow inbound access from external source +- [ ] Modify existing firewall rule +- [ ] Remove/revoke firewall rule +- [ ] Other (specify): + +### Resource Information +**Service/Domain Name**: +**IP Address(es)**: +**Port(s)**: +**Protocol**: +- [ ] HTTP (80) +- [ ] HTTPS (443) +- [ ] SSH (22) +- [ ] FTP (21) +- [ ] SFTP (22) +- [ ] Custom (specify): _______________ + +### Requestor Information +**Name**: +**GitHub Username**: @ +**Email**: @mokoconsulting.tech +**Team/Department**: +**Manager**: @ + +### Business Justification +**Why is this access needed?** + +**Which project(s) require this access?** + +**What functionality will break without this access?** + +**Is there an alternative solution?** +- [ ] Yes (explain): +- [ ] No + +### Security Considerations +**Data Classification**: +- [ ] Public +- [ ] Internal +- [ ] Confidential +- [ ] Restricted + +**Sensitive Data Transmission**: +- [ ] No sensitive data will be transmitted +- [ ] Sensitive data will be transmitted (encryption required) +- [ ] Authentication credentials will be transmitted (secure storage required) + +**Third-Party Service**: +- [ ] This is a trusted/verified third-party service +- [ ] This is a new/unverified service (security review required) + +**Service Documentation**: +(Provide link to service documentation or API specs) + +### Access Scope +**Affected Systems**: +- [ ] Development environment only +- [ ] Staging environment only +- [ ] Production environment +- [ ] All environments + +**Access Duration**: +- [ ] Permanent (ongoing business need) +- [ ] Temporary (specify end date): _______________ +- [ ] Testing only (specify duration): _______________ + +### Technical Details +**Source System(s)**: +(Which internal systems need access?) + +**Destination System(s)**: +(Which external systems need to be accessed?) + +**Expected Traffic Volume**: +(e.g., requests per hour/day) + +**Traffic Pattern**: +- [ ] Continuous +- [ ] Periodic (specify frequency): _______________ +- [ ] On-demand/manual +- [ ] Scheduled (specify schedule): _______________ + +### Testing Requirements +**Pre-Production Testing**: +- [ ] Request includes dev/staging access for testing +- [ ] Testing can be done with production access only +- [ ] No testing required (modify existing rule) + +**Testing Plan**: + +**Rollback Plan**: +(What happens if access needs to be revoked?) + +### Compliance & Audit +**Compliance Requirements**: +- [ ] GDPR considerations +- [ ] SOC 2 compliance required +- [ ] PCI DSS considerations +- [ ] Other regulatory requirements: _______________ +- [ ] No specific compliance requirements + +**Audit/Logging Requirements**: +- [ ] Standard logging sufficient +- [ ] Enhanced logging/monitoring required +- [ ] Real-time alerting required + +### Urgency +- [ ] Critical (production down, immediate access needed) +- [ ] High (needed within 24 hours) +- [ ] Normal (needed within 1 week) +- [ ] Low priority (needed within 1 month) + +**If critical/high urgency, explain why:** + +### Approvals +**Manager Approval**: +- [ ] Manager has been notified and approves this request + +**Security Team Review Required**: +- [ ] Yes (new external service, sensitive data) +- [ ] No (minor change, established service) + +### Additional Information + +**Related Documentation**: +(Links to relevant docs, RFCs, tickets, etc.) + +**Dependencies**: +(Other systems or changes this depends on) + +**Comments/Questions**: + +--- + +## For Infrastructure/Security Team Use Only + +**Do not edit below this line** + +### Security Review +- [ ] Security team review completed +- [ ] Risk assessment: Low / Medium / High +- [ ] Encryption required: Yes / No +- [ ] VPN required: Yes / No +- [ ] Additional security controls: _______________ + +**Reviewed By**: @_______________ +**Review Date**: _______________ +**Review Notes**: + +### Implementation +- [ ] Firewall rule created/modified +- [ ] Rule tested in dev/staging +- [ ] Rule deployed to production +- [ ] Monitoring/alerting configured +- [ ] Documentation updated + +**Firewall Rule ID**: _______________ +**Implementation Date**: _______________ +**Implemented By**: @_______________ + +**Configuration Details**: +``` +Source: +Destination: +Port/Protocol: +Action: Allow/Deny +``` + +### Verification +- [ ] Requestor confirmed access working +- [ ] Logs reviewed (no anomalies) +- [ ] Security scan completed (if applicable) + +**Verification Date**: _______________ +**Verified By**: @_______________ + +### Notes -- 2.52.0 From d3bd675383f075d42bd24a0351c88b2e6b062178 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:40 +0000 Subject: [PATCH 067/136] chore: remove .github/ISSUE_TEMPLATE/firewall-request.md (moved to .mokogitea/) [skip ci] --- .github/ISSUE_TEMPLATE/firewall-request.md | 190 --------------------- 1 file changed, 190 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/firewall-request.md diff --git a/.github/ISSUE_TEMPLATE/firewall-request.md b/.github/ISSUE_TEMPLATE/firewall-request.md deleted file mode 100644 index 4a43395..0000000 --- a/.github/ISSUE_TEMPLATE/firewall-request.md +++ /dev/null @@ -1,190 +0,0 @@ ---- -name: Firewall Request -about: Request firewall rule changes or access to external resources -title: '[FIREWALL] [Resource Name] - [Brief Description]' -labels: ['firewall-request', 'infrastructure', 'security'] -assignees: [] ---- - - -## Firewall Request - -### Request Type -- [ ] Allow outbound access to external service/API -- [ ] Allow inbound access from external source -- [ ] Modify existing firewall rule -- [ ] Remove/revoke firewall rule -- [ ] Other (specify): - -### Resource Information -**Service/Domain Name**: -**IP Address(es)**: -**Port(s)**: -**Protocol**: -- [ ] HTTP (80) -- [ ] HTTPS (443) -- [ ] SSH (22) -- [ ] FTP (21) -- [ ] SFTP (22) -- [ ] Custom (specify): _______________ - -### Requestor Information -**Name**: -**GitHub Username**: @ -**Email**: @mokoconsulting.tech -**Team/Department**: -**Manager**: @ - -### Business Justification -**Why is this access needed?** - -**Which project(s) require this access?** - -**What functionality will break without this access?** - -**Is there an alternative solution?** -- [ ] Yes (explain): -- [ ] No - -### Security Considerations -**Data Classification**: -- [ ] Public -- [ ] Internal -- [ ] Confidential -- [ ] Restricted - -**Sensitive Data Transmission**: -- [ ] No sensitive data will be transmitted -- [ ] Sensitive data will be transmitted (encryption required) -- [ ] Authentication credentials will be transmitted (secure storage required) - -**Third-Party Service**: -- [ ] This is a trusted/verified third-party service -- [ ] This is a new/unverified service (security review required) - -**Service Documentation**: -(Provide link to service documentation or API specs) - -### Access Scope -**Affected Systems**: -- [ ] Development environment only -- [ ] Staging environment only -- [ ] Production environment -- [ ] All environments - -**Access Duration**: -- [ ] Permanent (ongoing business need) -- [ ] Temporary (specify end date): _______________ -- [ ] Testing only (specify duration): _______________ - -### Technical Details -**Source System(s)**: -(Which internal systems need access?) - -**Destination System(s)**: -(Which external systems need to be accessed?) - -**Expected Traffic Volume**: -(e.g., requests per hour/day) - -**Traffic Pattern**: -- [ ] Continuous -- [ ] Periodic (specify frequency): _______________ -- [ ] On-demand/manual -- [ ] Scheduled (specify schedule): _______________ - -### Testing Requirements -**Pre-Production Testing**: -- [ ] Request includes dev/staging access for testing -- [ ] Testing can be done with production access only -- [ ] No testing required (modify existing rule) - -**Testing Plan**: - -**Rollback Plan**: -(What happens if access needs to be revoked?) - -### Compliance & Audit -**Compliance Requirements**: -- [ ] GDPR considerations -- [ ] SOC 2 compliance required -- [ ] PCI DSS considerations -- [ ] Other regulatory requirements: _______________ -- [ ] No specific compliance requirements - -**Audit/Logging Requirements**: -- [ ] Standard logging sufficient -- [ ] Enhanced logging/monitoring required -- [ ] Real-time alerting required - -### Urgency -- [ ] Critical (production down, immediate access needed) -- [ ] High (needed within 24 hours) -- [ ] Normal (needed within 1 week) -- [ ] Low priority (needed within 1 month) - -**If critical/high urgency, explain why:** - -### Approvals -**Manager Approval**: -- [ ] Manager has been notified and approves this request - -**Security Team Review Required**: -- [ ] Yes (new external service, sensitive data) -- [ ] No (minor change, established service) - -### Additional Information - -**Related Documentation**: -(Links to relevant docs, RFCs, tickets, etc.) - -**Dependencies**: -(Other systems or changes this depends on) - -**Comments/Questions**: - ---- - -## For Infrastructure/Security Team Use Only - -**Do not edit below this line** - -### Security Review -- [ ] Security team review completed -- [ ] Risk assessment: Low / Medium / High -- [ ] Encryption required: Yes / No -- [ ] VPN required: Yes / No -- [ ] Additional security controls: _______________ - -**Reviewed By**: @_______________ -**Review Date**: _______________ -**Review Notes**: - -### Implementation -- [ ] Firewall rule created/modified -- [ ] Rule tested in dev/staging -- [ ] Rule deployed to production -- [ ] Monitoring/alerting configured -- [ ] Documentation updated - -**Firewall Rule ID**: _______________ -**Implementation Date**: _______________ -**Implemented By**: @_______________ - -**Configuration Details**: -``` -Source: -Destination: -Port/Protocol: -Action: Allow/Deny -``` - -### Verification -- [ ] Requestor confirmed access working -- [ ] Logs reviewed (no anomalies) -- [ ] Security scan completed (if applicable) - -**Verification Date**: _______________ -**Verified By**: @_______________ - -### Notes -- 2.52.0 From 7e51b9457507e5aefbbd40b866244ddb08283cae Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:40 +0000 Subject: [PATCH 068/136] chore: move .github/ISSUE_TEMPLATE/joomla_issue.md to .mokogitea/joomla_issue.md [skip ci] --- .mokogitea/joomla_issue.md | 87 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 .mokogitea/joomla_issue.md diff --git a/.mokogitea/joomla_issue.md b/.mokogitea/joomla_issue.md new file mode 100644 index 0000000..d808f79 --- /dev/null +++ b/.mokogitea/joomla_issue.md @@ -0,0 +1,87 @@ +--- +name: Joomla Extension Issue +about: Report an issue with a Joomla extension +title: '[JOOMLA] ' +labels: 'joomla' +assignees: '' + +--- + + +## Issue Type +- [ ] Component Issue +- [ ] Module Issue +- [ ] Plugin Issue +- [ ] Template Issue + +## Extension Details +- **Extension Name**: [e.g., moko-cassiopeia] +- **Extension Version**: [e.g., 1.2.3] +- **Extension Type**: [Component / Module / Plugin / Template] + +## Joomla Environment +- **Joomla Version**: [e.g., 4.4.0, 5.0.0] +- **PHP Version**: [e.g., 8.1.0] +- **Database**: [MySQL / PostgreSQL / MariaDB] +- **Database Version**: [e.g., 8.0] +- **Server**: [Apache / Nginx / IIS] +- **Hosting**: [Shared / VPS / Dedicated / Cloud] + +## Issue Description +Provide a clear and detailed description of the issue. + +## Steps to Reproduce +1. Go to '...' +2. Click on '...' +3. Configure '...' +4. See error + +## Expected Behavior +What you expected to happen. + +## Actual Behavior +What actually happened. + +## Error Messages +``` +# Paste any error messages from Joomla error logs +# Location: administrator/logs/error.php +``` + +## Browser Console Errors +```javascript +// Paste any JavaScript console errors (F12 in browser) +``` + +## Screenshots +Add screenshots to help explain the issue. + +## Configuration +```ini +# Paste extension configuration (sanitize sensitive data) +``` + +## Installed Extensions +List other installed extensions that might conflict: +- Extension 1 (version) +- Extension 2 (version) + +## Template Overrides +- [ ] Using template overrides +- [ ] Custom CSS +- [ ] Custom JavaScript + +## Additional Context +- **Multilingual Site**: [Yes / No] +- **Cache Enabled**: [Yes / No] +- **Debug Mode**: [Yes / No] +- **SEF URLs**: [Yes / No] + +## Checklist +- [ ] I have cleared Joomla cache +- [ ] I have disabled other extensions to test for conflicts +- [ ] I have checked Joomla error logs +- [ ] I have tested with a default Joomla template +- [ ] I have checked browser console for JavaScript errors +- [ ] I have searched for similar issues +- [ ] I am using a supported Joomla version -- 2.52.0 From ec3a3fd960b546430e5427b0610362aa3b80e6ea Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:41 +0000 Subject: [PATCH 069/136] chore: remove .github/ISSUE_TEMPLATE/joomla_issue.md (moved to .mokogitea/) [skip ci] --- .github/ISSUE_TEMPLATE/joomla_issue.md | 87 -------------------------- 1 file changed, 87 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/joomla_issue.md diff --git a/.github/ISSUE_TEMPLATE/joomla_issue.md b/.github/ISSUE_TEMPLATE/joomla_issue.md deleted file mode 100644 index d808f79..0000000 --- a/.github/ISSUE_TEMPLATE/joomla_issue.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -name: Joomla Extension Issue -about: Report an issue with a Joomla extension -title: '[JOOMLA] ' -labels: 'joomla' -assignees: '' - ---- - - -## Issue Type -- [ ] Component Issue -- [ ] Module Issue -- [ ] Plugin Issue -- [ ] Template Issue - -## Extension Details -- **Extension Name**: [e.g., moko-cassiopeia] -- **Extension Version**: [e.g., 1.2.3] -- **Extension Type**: [Component / Module / Plugin / Template] - -## Joomla Environment -- **Joomla Version**: [e.g., 4.4.0, 5.0.0] -- **PHP Version**: [e.g., 8.1.0] -- **Database**: [MySQL / PostgreSQL / MariaDB] -- **Database Version**: [e.g., 8.0] -- **Server**: [Apache / Nginx / IIS] -- **Hosting**: [Shared / VPS / Dedicated / Cloud] - -## Issue Description -Provide a clear and detailed description of the issue. - -## Steps to Reproduce -1. Go to '...' -2. Click on '...' -3. Configure '...' -4. See error - -## Expected Behavior -What you expected to happen. - -## Actual Behavior -What actually happened. - -## Error Messages -``` -# Paste any error messages from Joomla error logs -# Location: administrator/logs/error.php -``` - -## Browser Console Errors -```javascript -// Paste any JavaScript console errors (F12 in browser) -``` - -## Screenshots -Add screenshots to help explain the issue. - -## Configuration -```ini -# Paste extension configuration (sanitize sensitive data) -``` - -## Installed Extensions -List other installed extensions that might conflict: -- Extension 1 (version) -- Extension 2 (version) - -## Template Overrides -- [ ] Using template overrides -- [ ] Custom CSS -- [ ] Custom JavaScript - -## Additional Context -- **Multilingual Site**: [Yes / No] -- **Cache Enabled**: [Yes / No] -- **Debug Mode**: [Yes / No] -- **SEF URLs**: [Yes / No] - -## Checklist -- [ ] I have cleared Joomla cache -- [ ] I have disabled other extensions to test for conflicts -- [ ] I have checked Joomla error logs -- [ ] I have tested with a default Joomla template -- [ ] I have checked browser console for JavaScript errors -- [ ] I have searched for similar issues -- [ ] I am using a supported Joomla version -- 2.52.0 From 6f9648a761b593c18aa861992f06a3fdb3abd5b6 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:41 +0000 Subject: [PATCH 070/136] chore: move .github/ISSUE_TEMPLATE/question.md to .mokogitea/question.md [skip ci] --- .mokogitea/question.md | 82 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 .mokogitea/question.md diff --git a/.mokogitea/question.md b/.mokogitea/question.md new file mode 100644 index 0000000..e17850b --- /dev/null +++ b/.mokogitea/question.md @@ -0,0 +1,82 @@ +--- +name: Question +about: Ask a question about usage, features, or best practices +title: '[QUESTION] ' +labels: ['question'] +assignees: [] +--- + + +## Question + +**Your question:** + + +## Context + +**What are you trying to accomplish?** + + +**What have you already tried?** + + +**Category**: +- [ ] Script usage +- [ ] Configuration +- [ ] Workflow setup +- [ ] Documentation interpretation +- [ ] Best practices +- [ ] Integration +- [ ] Other: __________ + +## Environment (if relevant) + +**Your setup**: +- Operating System: +- Version: + +## What You've Researched + +**Documentation reviewed**: +- [ ] README.md +- [ ] Project documentation +- [ ] Other (specify): __________ + +**Similar issues/questions found**: +- # +- # + +## Expected Outcome + +**What result are you hoping for?** + + +## Code/Configuration Samples + +**Relevant code or configuration** (if applicable): + +```bash +# Your code here +``` + +## Additional Context + +**Any other relevant information:** + + +**Screenshots** (if helpful): + + +## Urgency + +- [ ] Urgent (blocking work) +- [ ] Normal (can work on other things meanwhile) +- [ ] Low priority (just curious) + +## Checklist + +- [ ] I have searched existing issues and discussions +- [ ] I have reviewed relevant documentation +- [ ] I have provided sufficient context +- [ ] I have included code/configuration samples if relevant +- [ ] This is a genuine question (not a bug report or feature request) -- 2.52.0 From fc78823fc2375bf71d55d23a3f3837f6b09cdfb8 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:41 +0000 Subject: [PATCH 071/136] chore: remove .github/ISSUE_TEMPLATE/question.md (moved to .mokogitea/) [skip ci] --- .github/ISSUE_TEMPLATE/question.md | 82 ------------------------------ 1 file changed, 82 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/question.md diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md deleted file mode 100644 index e17850b..0000000 --- a/.github/ISSUE_TEMPLATE/question.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -name: Question -about: Ask a question about usage, features, or best practices -title: '[QUESTION] ' -labels: ['question'] -assignees: [] ---- - - -## Question - -**Your question:** - - -## Context - -**What are you trying to accomplish?** - - -**What have you already tried?** - - -**Category**: -- [ ] Script usage -- [ ] Configuration -- [ ] Workflow setup -- [ ] Documentation interpretation -- [ ] Best practices -- [ ] Integration -- [ ] Other: __________ - -## Environment (if relevant) - -**Your setup**: -- Operating System: -- Version: - -## What You've Researched - -**Documentation reviewed**: -- [ ] README.md -- [ ] Project documentation -- [ ] Other (specify): __________ - -**Similar issues/questions found**: -- # -- # - -## Expected Outcome - -**What result are you hoping for?** - - -## Code/Configuration Samples - -**Relevant code or configuration** (if applicable): - -```bash -# Your code here -``` - -## Additional Context - -**Any other relevant information:** - - -**Screenshots** (if helpful): - - -## Urgency - -- [ ] Urgent (blocking work) -- [ ] Normal (can work on other things meanwhile) -- [ ] Low priority (just curious) - -## Checklist - -- [ ] I have searched existing issues and discussions -- [ ] I have reviewed relevant documentation -- [ ] I have provided sufficient context -- [ ] I have included code/configuration samples if relevant -- [ ] This is a genuine question (not a bug report or feature request) -- 2.52.0 From 6f5cf33263c1cea801fbba526a797b7a27768bd4 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:42 +0000 Subject: [PATCH 072/136] chore: move .github/ISSUE_TEMPLATE/request-license.md to .mokogitea/request-license.md [skip ci] --- .mokogitea/request-license.md | 107 ++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 .mokogitea/request-license.md diff --git a/.mokogitea/request-license.md b/.mokogitea/request-license.md new file mode 100644 index 0000000..52c3b74 --- /dev/null +++ b/.mokogitea/request-license.md @@ -0,0 +1,107 @@ +--- +name: License Request +about: Request an organization license for Sublime Text +title: '[LICENSE REQUEST] Sublime Text - [Your Name]' +labels: ['license-request', 'admin'] +assignees: [] +--- + + +## License Request + +### Tool Information +**Tool Name**: Sublime Text + +**License Type Requested**: Organization Pool + +**Personal Purchase**: +- [ ] I prefer to purchase my own license ($99 USD - recommended, immediate access) +- [ ] I prefer an organization license (1-2 business days, organization use only) +- [ ] I have already purchased my own license (registration only for support) + +### Requestor Information +**Name**: +**GitHub Username**: @ +**Email**: @mokoconsulting.tech +**Team/Department**: +**Manager**: @ + +### Justification +**Why do you need this license?** + +**Primary use case**: +- [ ] Remote development (SFTP to servers) +- [ ] Local development +- [ ] Code review +- [ ] Documentation editing +- [ ] Other (specify): + +**Which projects/repositories will you work on?** + +**Have you evaluated the free trial?** +- [ ] Yes, I've used the trial and Sublime Text meets my needs +- [ ] No, requesting license before trial + +**Alternative tools considered**: +- [ ] VS Code (free alternative) +- [ ] Vim/Neovim (free, terminal-based) +- [ ] Other: _______________ + +### Platform +- [ ] Windows +- [ ] macOS +- [ ] Linux (distribution: ________) + +### Urgency +- [ ] Urgent (needed within 24 hours - please justify) +- [ ] Normal (1-2 business days) +- [ ] Low priority (when available) + +**If urgent, please explain why:** + +### SFTP Plugin +**Note**: Sublime SFTP plugin ($16 USD) is a **separate personal purchase** and is NOT provided by the organization. + +- [ ] I understand SFTP plugin requires separate personal purchase +- [ ] I have already purchased SFTP plugin +- [ ] I will purchase SFTP plugin if needed for my work +- [ ] I don't need SFTP plugin (local development only) + +### Acknowledgments +- [ ] I have read the License Management Policy (/docs/github-private/LICENSE_MANAGEMENT.md) +- [ ] I understand organization licenses are for work use only +- [ ] I understand organization licenses must be returned upon leaving +- [ ] I understand personal purchases ($99) are an alternative with lifetime access +- [ ] I understand SFTP plugin ($16) requires separate personal purchase +- [ ] I agree to the terms of use + +### Additional Information + +**Expected daily usage hours**: _____ hours/day + +**Duration of need**: +- [ ] Permanent (ongoing role) +- [ ] Temporary project (_____ months) +- [ ] Trial/Evaluation (_____ weeks) + +**Comments/Questions**: + +--- + +## For Admin Use Only + +**Do not edit below this line** + +- [ ] Manager approval received (@manager-username) +- [ ] License available in pool (current: __/20) +- [ ] License type confirmed (Organization / Personal registration) +- [ ] License key sent via encrypted email +- [ ] Activation confirmed by user +- [ ] Added to license tracking sheet +- [ ] User notified of SFTP plugin requirement + +**License Key ID**: _____________ +**Date Issued**: _____________ +**Issued By**: @_____________ + +**Notes**: -- 2.52.0 From eab3804fdc6e744a794176077f07bd5f7a3b35bb Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:42 +0000 Subject: [PATCH 073/136] chore: remove .github/ISSUE_TEMPLATE/request-license.md (moved to .mokogitea/) [skip ci] --- .github/ISSUE_TEMPLATE/request-license.md | 107 ---------------------- 1 file changed, 107 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/request-license.md diff --git a/.github/ISSUE_TEMPLATE/request-license.md b/.github/ISSUE_TEMPLATE/request-license.md deleted file mode 100644 index 52c3b74..0000000 --- a/.github/ISSUE_TEMPLATE/request-license.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -name: License Request -about: Request an organization license for Sublime Text -title: '[LICENSE REQUEST] Sublime Text - [Your Name]' -labels: ['license-request', 'admin'] -assignees: [] ---- - - -## License Request - -### Tool Information -**Tool Name**: Sublime Text - -**License Type Requested**: Organization Pool - -**Personal Purchase**: -- [ ] I prefer to purchase my own license ($99 USD - recommended, immediate access) -- [ ] I prefer an organization license (1-2 business days, organization use only) -- [ ] I have already purchased my own license (registration only for support) - -### Requestor Information -**Name**: -**GitHub Username**: @ -**Email**: @mokoconsulting.tech -**Team/Department**: -**Manager**: @ - -### Justification -**Why do you need this license?** - -**Primary use case**: -- [ ] Remote development (SFTP to servers) -- [ ] Local development -- [ ] Code review -- [ ] Documentation editing -- [ ] Other (specify): - -**Which projects/repositories will you work on?** - -**Have you evaluated the free trial?** -- [ ] Yes, I've used the trial and Sublime Text meets my needs -- [ ] No, requesting license before trial - -**Alternative tools considered**: -- [ ] VS Code (free alternative) -- [ ] Vim/Neovim (free, terminal-based) -- [ ] Other: _______________ - -### Platform -- [ ] Windows -- [ ] macOS -- [ ] Linux (distribution: ________) - -### Urgency -- [ ] Urgent (needed within 24 hours - please justify) -- [ ] Normal (1-2 business days) -- [ ] Low priority (when available) - -**If urgent, please explain why:** - -### SFTP Plugin -**Note**: Sublime SFTP plugin ($16 USD) is a **separate personal purchase** and is NOT provided by the organization. - -- [ ] I understand SFTP plugin requires separate personal purchase -- [ ] I have already purchased SFTP plugin -- [ ] I will purchase SFTP plugin if needed for my work -- [ ] I don't need SFTP plugin (local development only) - -### Acknowledgments -- [ ] I have read the License Management Policy (/docs/github-private/LICENSE_MANAGEMENT.md) -- [ ] I understand organization licenses are for work use only -- [ ] I understand organization licenses must be returned upon leaving -- [ ] I understand personal purchases ($99) are an alternative with lifetime access -- [ ] I understand SFTP plugin ($16) requires separate personal purchase -- [ ] I agree to the terms of use - -### Additional Information - -**Expected daily usage hours**: _____ hours/day - -**Duration of need**: -- [ ] Permanent (ongoing role) -- [ ] Temporary project (_____ months) -- [ ] Trial/Evaluation (_____ weeks) - -**Comments/Questions**: - ---- - -## For Admin Use Only - -**Do not edit below this line** - -- [ ] Manager approval received (@manager-username) -- [ ] License available in pool (current: __/20) -- [ ] License type confirmed (Organization / Personal registration) -- [ ] License key sent via encrypted email -- [ ] Activation confirmed by user -- [ ] Added to license tracking sheet -- [ ] User notified of SFTP plugin requirement - -**License Key ID**: _____________ -**Date Issued**: _____________ -**Issued By**: @_____________ - -**Notes**: -- 2.52.0 From c856cd191dbcc956bd2026831931d7ae11d373c2 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:42 +0000 Subject: [PATCH 074/136] chore: move .github/ISSUE_TEMPLATE/rfc.md to .mokogitea/rfc.md [skip ci] --- .mokogitea/rfc.md | 126 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 .mokogitea/rfc.md diff --git a/.mokogitea/rfc.md b/.mokogitea/rfc.md new file mode 100644 index 0000000..6f09af7 --- /dev/null +++ b/.mokogitea/rfc.md @@ -0,0 +1,126 @@ +--- +name: Request for Comments (RFC) +about: Propose a significant change for community discussion +title: '[RFC] ' +labels: 'rfc, discussion' +assignees: '' + +--- + + +## RFC Summary +One-paragraph summary of the proposal. + +## Motivation +Why are we doing this? What use cases does it support? What is the expected outcome? + +## Detailed Design +### Overview +Provide a detailed explanation of the proposed change. + +### API Changes (if applicable) +```php +// Before +function oldApi($param1) { } + +// After +function newApi($param1, $param2) { } +``` + +### User Experience Changes +Describe how users will interact with this change. + +### Implementation Approach +High-level implementation strategy. + +## Drawbacks +Why should we *not* do this? + +## Alternatives +What other designs have been considered? What is the impact of not doing this? + +### Alternative 1 +- Description +- Trade-offs + +### Alternative 2 +- Description +- Trade-offs + +## Adoption Strategy +How will existing users adopt this? Is this a breaking change? + +### Migration Guide +```bash +# Steps to migrate +``` + +### Deprecation Timeline +- **Announcement**: +- **Deprecation**: +- **Removal**: + +## Unresolved Questions +- Question 1 +- Question 2 + +## Future Possibilities +What future work does this enable? + +## Impact Assessment +### Performance +Expected performance impact. + +### Security +Security considerations and implications. + +### Compatibility +- **Backward Compatible**: [Yes / No] +- **Breaking Changes**: [List] + +### Maintenance +Long-term maintenance considerations. + +## Community Input +### Stakeholders +- [ ] Core team +- [ ] Module developers +- [ ] End users +- [ ] Enterprise customers + +### Feedback Period +**Duration**: [e.g., 2 weeks] +**Deadline**: [date] + +## Implementation Timeline +### Phase 1: Design +- [ ] RFC discussion +- [ ] Design finalization +- [ ] Approval + +### Phase 2: Implementation +- [ ] Core implementation +- [ ] Tests +- [ ] Documentation + +### Phase 3: Release +- [ ] Beta release +- [ ] Feedback collection +- [ ] Stable release + +## Success Metrics +How will we measure success? +- Metric 1 +- Metric 2 + +## References +- Related RFCs: +- External documentation: +- Prior art: + +## Open Questions for Community +1. Question 1? +2. Question 2? + +--- +**Note**: This RFC is open for community discussion. Please provide feedback in the comments below. -- 2.52.0 From 3594df1d7573e6b273c04bd81ee49b94d3d61c41 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:43 +0000 Subject: [PATCH 075/136] chore: remove .github/ISSUE_TEMPLATE/rfc.md (moved to .mokogitea/) [skip ci] --- .github/ISSUE_TEMPLATE/rfc.md | 126 ---------------------------------- 1 file changed, 126 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/rfc.md diff --git a/.github/ISSUE_TEMPLATE/rfc.md b/.github/ISSUE_TEMPLATE/rfc.md deleted file mode 100644 index 6f09af7..0000000 --- a/.github/ISSUE_TEMPLATE/rfc.md +++ /dev/null @@ -1,126 +0,0 @@ ---- -name: Request for Comments (RFC) -about: Propose a significant change for community discussion -title: '[RFC] ' -labels: 'rfc, discussion' -assignees: '' - ---- - - -## RFC Summary -One-paragraph summary of the proposal. - -## Motivation -Why are we doing this? What use cases does it support? What is the expected outcome? - -## Detailed Design -### Overview -Provide a detailed explanation of the proposed change. - -### API Changes (if applicable) -```php -// Before -function oldApi($param1) { } - -// After -function newApi($param1, $param2) { } -``` - -### User Experience Changes -Describe how users will interact with this change. - -### Implementation Approach -High-level implementation strategy. - -## Drawbacks -Why should we *not* do this? - -## Alternatives -What other designs have been considered? What is the impact of not doing this? - -### Alternative 1 -- Description -- Trade-offs - -### Alternative 2 -- Description -- Trade-offs - -## Adoption Strategy -How will existing users adopt this? Is this a breaking change? - -### Migration Guide -```bash -# Steps to migrate -``` - -### Deprecation Timeline -- **Announcement**: -- **Deprecation**: -- **Removal**: - -## Unresolved Questions -- Question 1 -- Question 2 - -## Future Possibilities -What future work does this enable? - -## Impact Assessment -### Performance -Expected performance impact. - -### Security -Security considerations and implications. - -### Compatibility -- **Backward Compatible**: [Yes / No] -- **Breaking Changes**: [List] - -### Maintenance -Long-term maintenance considerations. - -## Community Input -### Stakeholders -- [ ] Core team -- [ ] Module developers -- [ ] End users -- [ ] Enterprise customers - -### Feedback Period -**Duration**: [e.g., 2 weeks] -**Deadline**: [date] - -## Implementation Timeline -### Phase 1: Design -- [ ] RFC discussion -- [ ] Design finalization -- [ ] Approval - -### Phase 2: Implementation -- [ ] Core implementation -- [ ] Tests -- [ ] Documentation - -### Phase 3: Release -- [ ] Beta release -- [ ] Feedback collection -- [ ] Stable release - -## Success Metrics -How will we measure success? -- Metric 1 -- Metric 2 - -## References -- Related RFCs: -- External documentation: -- Prior art: - -## Open Questions for Community -1. Question 1? -2. Question 2? - ---- -**Note**: This RFC is open for community discussion. Please provide feedback in the comments below. -- 2.52.0 From ca45a60141e32bc913b37fffb1ab88ace6876854 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:43 +0000 Subject: [PATCH 076/136] chore: move .github/ISSUE_TEMPLATE/security.md to .mokogitea/security.md [skip ci] --- .mokogitea/security.md | 51 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .mokogitea/security.md diff --git a/.mokogitea/security.md b/.mokogitea/security.md new file mode 100644 index 0000000..973e961 --- /dev/null +++ b/.mokogitea/security.md @@ -0,0 +1,51 @@ +--- +name: Security Vulnerability Report +about: Report a security vulnerability (use only for non-critical issues) +title: '[SECURITY] ' +labels: 'security' +assignees: '' + +--- + + +## ⚠️ IMPORTANT: Private Disclosure Required + +**For critical security vulnerabilities, DO NOT use this template.** +Follow the process in [SECURITY.md](../SECURITY.md) for responsible disclosure. + +Use this template only for: +- Security improvements +- Non-critical security suggestions +- Security documentation updates + +--- + +## Security Issue + +**Severity**: + + +## Description + + +## Affected Components + + +## Suggested Mitigation + + +## Standards Reference +Does this relate to security standards in [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards)? +- [ ] SPDX license identifiers +- [ ] Secret management +- [ ] Dependency security +- [ ] Access control +- [ ] Other: [specify] + +## Additional Context + + +## Checklist +- [ ] This is NOT a critical vulnerability requiring private disclosure +- [ ] I have reviewed the SECURITY.md policy +- [ ] I have provided sufficient detail for evaluation -- 2.52.0 From 827d7d3e39831e98b0c620c8c6e7429a0fa058bc Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:43 +0000 Subject: [PATCH 077/136] chore: remove .github/ISSUE_TEMPLATE/security.md (moved to .mokogitea/) [skip ci] --- .github/ISSUE_TEMPLATE/security.md | 51 ------------------------------ 1 file changed, 51 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/security.md diff --git a/.github/ISSUE_TEMPLATE/security.md b/.github/ISSUE_TEMPLATE/security.md deleted file mode 100644 index 973e961..0000000 --- a/.github/ISSUE_TEMPLATE/security.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -name: Security Vulnerability Report -about: Report a security vulnerability (use only for non-critical issues) -title: '[SECURITY] ' -labels: 'security' -assignees: '' - ---- - - -## ⚠️ IMPORTANT: Private Disclosure Required - -**For critical security vulnerabilities, DO NOT use this template.** -Follow the process in [SECURITY.md](../SECURITY.md) for responsible disclosure. - -Use this template only for: -- Security improvements -- Non-critical security suggestions -- Security documentation updates - ---- - -## Security Issue - -**Severity**: - - -## Description - - -## Affected Components - - -## Suggested Mitigation - - -## Standards Reference -Does this relate to security standards in [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards)? -- [ ] SPDX license identifiers -- [ ] Secret management -- [ ] Dependency security -- [ ] Access control -- [ ] Other: [specify] - -## Additional Context - - -## Checklist -- [ ] This is NOT a critical vulnerability requiring private disclosure -- [ ] I have reviewed the SECURITY.md policy -- [ ] I have provided sufficient detail for evaluation -- 2.52.0 From 114447c6938f52db07416d787102723f0a3aa6f2 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:44 +0000 Subject: [PATCH 078/136] chore: move .github/copilot-instructions.md to .mokogitea/copilot-instructions.md [skip ci] --- .mokogitea/copilot-instructions.md | 309 +++++++++++++++++++++++++++++ 1 file changed, 309 insertions(+) create mode 100644 .mokogitea/copilot-instructions.md diff --git a/.mokogitea/copilot-instructions.md b/.mokogitea/copilot-instructions.md new file mode 100644 index 0000000..6b0c918 --- /dev/null +++ b/.mokogitea/copilot-instructions.md @@ -0,0 +1,309 @@ + + +> [!IMPORTANT] +> **🔧 AI Self-Update Required on First Use** +> +> This file may contain unfilled `{{TOKEN}}` placeholders. The MokoStandards bulk-sync system +> replaces them automatically at sync time; if you can still see them, sync has not yet run for +> this repository — or this is a brand-new repo. +> +> **On your very first interaction in this repository, before answering any other question:** +> +> 1. Check whether any `{{TOKEN}}` placeholders remain in this file. +> 2. If they do, locate the values as described in the table below and replace every occurrence +> in **both** `.github/copilot-instructions.md` and `.github/CLAUDE.md`. +> 3. After replacing all tokens, **delete this entire notice block** (from `> [!IMPORTANT]` +> through the closing `> ---` line) in both files. +> 4. Tell the developer: *"I've self-updated the AI context files with this repository's +> values — all set."* +> +> | Placeholder | Where to find the value | +> |---|---| +> | `MokoJoomHero` | The GitHub repository name (visible in the URL, `README.md` heading, or `git remote -v`) | +> | `https://github.com/mokoconsulting-tech/MokoJoomHero` | Full GitHub URL, e.g. `https://github.com/mokoconsulting-tech/` | +> | `PHP` | The dominant programming language (check file extensions in the repository) | +> | `Generic project` | The project type: `PHP library`, `Joomla extension`, `Dolibarr module`, `WaaS site`, etc. — infer from repo structure | +> +> --- + +# MokoJoomHero — GitHub Copilot Custom Instructions + +## What This Repo Is + +This is a **Moko Consulting** repository governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). All coding standards, workflows, and policies are defined there and enforced here via bulk sync. + +Repository URL: https://github.com/mokoconsulting-tech/MokoJoomHero +Primary language: **PHP** +Platform type: **Generic project** + +--- + +## Primary Language + +**PHP is the primary language for this repository.** Follow the conventions documented in [MokoStandards coding-style-guide](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md). + +YAML uses 2-space indentation (spaces, not tabs). All other text files use tabs per `.editorconfig`. + +--- + +## File Header — Always Required on New Files + +Every new file needs a copyright header as its first content. Use the minimal form unless the file is a policy doc, README, or public API. + +**PHP:** +```php + + * + * This file is part of a Moko Consulting project. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: MokoJoomHero.Module + * INGROUP: MokoJoomHero + * REPO: https://github.com/mokoconsulting-tech/MokoJoomHero + * PATH: /path/to/file.php + * VERSION: XX.YY.ZZ + * BRIEF: One-line description of purpose + */ + +declare(strict_types=1); +``` + +**Markdown:** +```markdown + +``` + +**YAML / Shell:** Use `#` comments with the same fields. JSON files are exempt. + +--- + +## Version Management + +**`README.md` is the single source of truth for the repository version.** + +- **Bump the patch version on every PR** — increment `XX.YY.ZZ` (e.g. `01.02.03` → `01.02.04`) in `README.md` before opening the PR; the `sync-version-on-merge` workflow propagates it automatically to all badges and `FILE INFORMATION` headers on merge to `main`. +- The `VERSION: XX.YY.ZZ` field in the README.md `FILE INFORMATION` block governs all other version references. +- Update the version in `README.md` only — the `sync-version-on-merge` workflow propagates it automatically to all badges and `FILE INFORMATION` headers on merge to `main`. +- Version format is zero-padded semver: `XX.YY.ZZ` (e.g. `04.00.04`). +- Never hardcode a specific version in document body text — use the badge or FILE INFORMATION header only. + +--- + +## GitHub Actions — Token Usage + +Every workflow must use **`secrets.GH_TOKEN`** (the org-level Personal Access Token). This applies to all `actions/checkout`, `gh` CLI calls, and any step that talks to the GitHub API. + +```yaml +# ✅ Correct +- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.GH_TOKEN }} + +env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} +``` + +```yaml +# ❌ Wrong — never use these in workflows +token: ${{ github.token }} +token: ${{ secrets.GITHUB_TOKEN }} +``` + +PHP scripts read the token with: `getenv('GH_TOKEN') ?: getenv('GITHUB_TOKEN')` — `GH_TOKEN` is always preferred; `GITHUB_TOKEN` is accepted only as a local-dev fallback. + +--- + +## Composer Package (PHP repositories) + +This repository requires the MokoStandards enterprise library. The `composer.json` must include: + +```json +{ + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/mokoconsulting-tech/MokoStandards" + } + ], + "require": { + "mokoconsulting/mokostandards": "^4.0" + } +} +``` + +Run `composer install` after adding the dependency. See [package-installation.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/package-installation.md) for full instructions. + +--- + +## PHP Script Pattern + +All PHP scripts **must** extend `MokoStandards\Enterprise\CliFramework`. Never write standalone classes or extend the legacy `CliBase`. + +```php +#!/usr/bin/env php +setDescription('One-line description'); + $this->addArgument('--path', 'Repository root', '.'); + $this->addArgument('--dry-run', 'Preview without writing', false); + } + + protected function run(): int + { + $path = $this->getArgument('--path'); + $dryRun = (bool) $this->getArgument('--dry-run'); + + $this->log('INFO', "Processing: {$path}"); + return 0; + } +} + +$script = new MyScript('my_script', 'One-line description'); +exit($script->execute()); +``` + +**Key rules:** +- Abstract methods to implement: `configure()` and `run()` — **not** `execute()` +- `execute()` is the **public entry point** that orchestrates setup (arg parsing, `initialize()`) and then calls your `run()` implementation; call it at the bottom with `exit($script->execute())` +- Entry point at the bottom: `$script->execute()` — **not** `$script->run()` +- Constructor always takes `(string $name, string $description = '')`; pass the description here — `setDescription()` inside `configure()` is only needed to override it +- `log(string $level, string $message)` — level is the **first** argument (INFO / SUCCESS / WARNING / ERROR) +- `$this->dryRun` and `$this->verbose` are set automatically from `--dry-run` / `--verbose` + +--- + +## Naming Conventions + +| Context | Convention | Example | +|---------|-----------|---------| +| PHP class | `PascalCase` | `MyService` | +| PHP method / function | `camelCase` | `getUserData()` | +| PHP variable | `$snake_case` | `$repo_path` | +| PHP constant | `UPPER_SNAKE_CASE` | `DEFAULT_THRESHOLD` | +| PHP class file | `PascalCase.php` | `ApiClient.php` | +| PHP script file | `snake_case.php` | `check_health.php` | +| YAML workflow | `kebab-case.yml` | `bulk-repo-sync.yml` | +| Markdown doc | `kebab-case.md` | `coding-style-guide.md` | + +--- + +## Commit Messages + +Format: `(): ` — imperative, lower-case subject, no trailing period. + +Valid types: `feat` · `fix` · `docs` · `chore` · `ci` · `refactor` · `style` · `test` · `perf` · `revert` · `build` + +Examples: +- `feat(module): add user preference caching` +- `fix(api): handle null response from external service` +- `docs(readme): update installation instructions` +- `chore(deps): bump phpunit to 11.x` + +--- + +## Branch Naming + +Approved prefixes: `dev/` · `rc/` · `version/` · `copilot/` · `dependabot/` + +- `dev/XX.YY` or `dev/feature-name` — development (version optional) +- `rc/XX.YY.ZZ` — release candidate (three-part required) +- `version/XX.YY` — archive branch (auto-created, two-part) +- Release tags: `vXX` (major only — one release per major version) +- Patch `00` = development (no release), first release = `01` + +Examples: +- ✅ `dev/04.06` · `dev/new-dashboard` · `rc/04.06.01` +- ❌ `feature/my-thing` — rejected by branch protection + +--- + +## Keeping Documentation Current + +Whenever you make code changes, update the corresponding documentation in the same commit or PR. Do not leave docs stale. + +| Change type | Documentation to update | +|-------------|------------------------| +| New or renamed public PHP method | PHPDoc block on the method; `docs/api/` index for that class | +| New or changed CLI script argument | Script's own `--help` text; `docs/api/` or equivalent | +| New or changed GitHub Actions workflow | `docs/workflows/.md` | +| New or changed policy | Corresponding file under `docs/policy/` | +| New library class or major feature | `CHANGELOG.md` entry under `Added` | +| Bug fix | `CHANGELOG.md` entry under `Fixed` | +| Breaking change | `CHANGELOG.md` entry under `Changed`; update `CONTRIBUTING.md` if contributor steps change | +| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block | +| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it to all headers and badges on merge | + +If your code change makes any existing doc sentence false or incomplete, fix the doc before closing the PR. + +--- + +## Key Constraints + +- Never commit directly to `main` — all changes go via PR, squash-merged +- Never skip the FILE INFORMATION block on a new file +- Never use bare `catch (\Throwable $e) {}` without logging or re-throwing +- Never hardcode version numbers in body text — update `README.md` and let automation propagate +- Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows — always use `secrets.GH_TOKEN` +- Never extend `CliBase` in PHP scripts — extend `MokoStandards\Enterprise\CliFramework` +- Never call `$script->run()` as the entry point — call `$script->execute()` +- Policy documents and guides must not be mixed + +--- + +## MokoStandards Reference + +This repository is governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). Authoritative policies: + +| Document | Purpose | +|----------|---------| +| [file-header-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/file-header-standards.md) | Copyright-header rules for every file type | +| [coding-style-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md) | Naming and formatting conventions | +| [branching-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/branching-strategy.md) | Branch naming, hierarchy, and release workflow | +| [merge-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR title/body conventions | +| [changelog-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md | +| [scripting-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/scripting-standards.md) | PHP script requirements and CliFramework usage | +| [package-installation.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/package-installation.md) | Installing `mokoconsulting/mokostandards` via Composer | -- 2.52.0 From 57c2887c6d6eb93c197b74ed69344e1b7a38ae29 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:44 +0000 Subject: [PATCH 079/136] chore: remove .github/copilot-instructions.md (moved to .mokogitea/) [skip ci] --- .github/copilot-instructions.md | 309 -------------------------------- 1 file changed, 309 deletions(-) delete mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index 6b0c918..0000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,309 +0,0 @@ - - -> [!IMPORTANT] -> **🔧 AI Self-Update Required on First Use** -> -> This file may contain unfilled `{{TOKEN}}` placeholders. The MokoStandards bulk-sync system -> replaces them automatically at sync time; if you can still see them, sync has not yet run for -> this repository — or this is a brand-new repo. -> -> **On your very first interaction in this repository, before answering any other question:** -> -> 1. Check whether any `{{TOKEN}}` placeholders remain in this file. -> 2. If they do, locate the values as described in the table below and replace every occurrence -> in **both** `.github/copilot-instructions.md` and `.github/CLAUDE.md`. -> 3. After replacing all tokens, **delete this entire notice block** (from `> [!IMPORTANT]` -> through the closing `> ---` line) in both files. -> 4. Tell the developer: *"I've self-updated the AI context files with this repository's -> values — all set."* -> -> | Placeholder | Where to find the value | -> |---|---| -> | `MokoJoomHero` | The GitHub repository name (visible in the URL, `README.md` heading, or `git remote -v`) | -> | `https://github.com/mokoconsulting-tech/MokoJoomHero` | Full GitHub URL, e.g. `https://github.com/mokoconsulting-tech/` | -> | `PHP` | The dominant programming language (check file extensions in the repository) | -> | `Generic project` | The project type: `PHP library`, `Joomla extension`, `Dolibarr module`, `WaaS site`, etc. — infer from repo structure | -> -> --- - -# MokoJoomHero — GitHub Copilot Custom Instructions - -## What This Repo Is - -This is a **Moko Consulting** repository governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). All coding standards, workflows, and policies are defined there and enforced here via bulk sync. - -Repository URL: https://github.com/mokoconsulting-tech/MokoJoomHero -Primary language: **PHP** -Platform type: **Generic project** - ---- - -## Primary Language - -**PHP is the primary language for this repository.** Follow the conventions documented in [MokoStandards coding-style-guide](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md). - -YAML uses 2-space indentation (spaces, not tabs). All other text files use tabs per `.editorconfig`. - ---- - -## File Header — Always Required on New Files - -Every new file needs a copyright header as its first content. Use the minimal form unless the file is a policy doc, README, or public API. - -**PHP:** -```php - - * - * This file is part of a Moko Consulting project. - * - * SPDX-License-Identifier: GPL-3.0-or-later - * - * FILE INFORMATION - * DEFGROUP: MokoJoomHero.Module - * INGROUP: MokoJoomHero - * REPO: https://github.com/mokoconsulting-tech/MokoJoomHero - * PATH: /path/to/file.php - * VERSION: XX.YY.ZZ - * BRIEF: One-line description of purpose - */ - -declare(strict_types=1); -``` - -**Markdown:** -```markdown - -``` - -**YAML / Shell:** Use `#` comments with the same fields. JSON files are exempt. - ---- - -## Version Management - -**`README.md` is the single source of truth for the repository version.** - -- **Bump the patch version on every PR** — increment `XX.YY.ZZ` (e.g. `01.02.03` → `01.02.04`) in `README.md` before opening the PR; the `sync-version-on-merge` workflow propagates it automatically to all badges and `FILE INFORMATION` headers on merge to `main`. -- The `VERSION: XX.YY.ZZ` field in the README.md `FILE INFORMATION` block governs all other version references. -- Update the version in `README.md` only — the `sync-version-on-merge` workflow propagates it automatically to all badges and `FILE INFORMATION` headers on merge to `main`. -- Version format is zero-padded semver: `XX.YY.ZZ` (e.g. `04.00.04`). -- Never hardcode a specific version in document body text — use the badge or FILE INFORMATION header only. - ---- - -## GitHub Actions — Token Usage - -Every workflow must use **`secrets.GH_TOKEN`** (the org-level Personal Access Token). This applies to all `actions/checkout`, `gh` CLI calls, and any step that talks to the GitHub API. - -```yaml -# ✅ Correct -- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.GH_TOKEN }} - -env: - GH_TOKEN: ${{ secrets.GH_TOKEN }} -``` - -```yaml -# ❌ Wrong — never use these in workflows -token: ${{ github.token }} -token: ${{ secrets.GITHUB_TOKEN }} -``` - -PHP scripts read the token with: `getenv('GH_TOKEN') ?: getenv('GITHUB_TOKEN')` — `GH_TOKEN` is always preferred; `GITHUB_TOKEN` is accepted only as a local-dev fallback. - ---- - -## Composer Package (PHP repositories) - -This repository requires the MokoStandards enterprise library. The `composer.json` must include: - -```json -{ - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/mokoconsulting-tech/MokoStandards" - } - ], - "require": { - "mokoconsulting/mokostandards": "^4.0" - } -} -``` - -Run `composer install` after adding the dependency. See [package-installation.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/package-installation.md) for full instructions. - ---- - -## PHP Script Pattern - -All PHP scripts **must** extend `MokoStandards\Enterprise\CliFramework`. Never write standalone classes or extend the legacy `CliBase`. - -```php -#!/usr/bin/env php -setDescription('One-line description'); - $this->addArgument('--path', 'Repository root', '.'); - $this->addArgument('--dry-run', 'Preview without writing', false); - } - - protected function run(): int - { - $path = $this->getArgument('--path'); - $dryRun = (bool) $this->getArgument('--dry-run'); - - $this->log('INFO', "Processing: {$path}"); - return 0; - } -} - -$script = new MyScript('my_script', 'One-line description'); -exit($script->execute()); -``` - -**Key rules:** -- Abstract methods to implement: `configure()` and `run()` — **not** `execute()` -- `execute()` is the **public entry point** that orchestrates setup (arg parsing, `initialize()`) and then calls your `run()` implementation; call it at the bottom with `exit($script->execute())` -- Entry point at the bottom: `$script->execute()` — **not** `$script->run()` -- Constructor always takes `(string $name, string $description = '')`; pass the description here — `setDescription()` inside `configure()` is only needed to override it -- `log(string $level, string $message)` — level is the **first** argument (INFO / SUCCESS / WARNING / ERROR) -- `$this->dryRun` and `$this->verbose` are set automatically from `--dry-run` / `--verbose` - ---- - -## Naming Conventions - -| Context | Convention | Example | -|---------|-----------|---------| -| PHP class | `PascalCase` | `MyService` | -| PHP method / function | `camelCase` | `getUserData()` | -| PHP variable | `$snake_case` | `$repo_path` | -| PHP constant | `UPPER_SNAKE_CASE` | `DEFAULT_THRESHOLD` | -| PHP class file | `PascalCase.php` | `ApiClient.php` | -| PHP script file | `snake_case.php` | `check_health.php` | -| YAML workflow | `kebab-case.yml` | `bulk-repo-sync.yml` | -| Markdown doc | `kebab-case.md` | `coding-style-guide.md` | - ---- - -## Commit Messages - -Format: `(): ` — imperative, lower-case subject, no trailing period. - -Valid types: `feat` · `fix` · `docs` · `chore` · `ci` · `refactor` · `style` · `test` · `perf` · `revert` · `build` - -Examples: -- `feat(module): add user preference caching` -- `fix(api): handle null response from external service` -- `docs(readme): update installation instructions` -- `chore(deps): bump phpunit to 11.x` - ---- - -## Branch Naming - -Approved prefixes: `dev/` · `rc/` · `version/` · `copilot/` · `dependabot/` - -- `dev/XX.YY` or `dev/feature-name` — development (version optional) -- `rc/XX.YY.ZZ` — release candidate (three-part required) -- `version/XX.YY` — archive branch (auto-created, two-part) -- Release tags: `vXX` (major only — one release per major version) -- Patch `00` = development (no release), first release = `01` - -Examples: -- ✅ `dev/04.06` · `dev/new-dashboard` · `rc/04.06.01` -- ❌ `feature/my-thing` — rejected by branch protection - ---- - -## Keeping Documentation Current - -Whenever you make code changes, update the corresponding documentation in the same commit or PR. Do not leave docs stale. - -| Change type | Documentation to update | -|-------------|------------------------| -| New or renamed public PHP method | PHPDoc block on the method; `docs/api/` index for that class | -| New or changed CLI script argument | Script's own `--help` text; `docs/api/` or equivalent | -| New or changed GitHub Actions workflow | `docs/workflows/.md` | -| New or changed policy | Corresponding file under `docs/policy/` | -| New library class or major feature | `CHANGELOG.md` entry under `Added` | -| Bug fix | `CHANGELOG.md` entry under `Fixed` | -| Breaking change | `CHANGELOG.md` entry under `Changed`; update `CONTRIBUTING.md` if contributor steps change | -| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block | -| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it to all headers and badges on merge | - -If your code change makes any existing doc sentence false or incomplete, fix the doc before closing the PR. - ---- - -## Key Constraints - -- Never commit directly to `main` — all changes go via PR, squash-merged -- Never skip the FILE INFORMATION block on a new file -- Never use bare `catch (\Throwable $e) {}` without logging or re-throwing -- Never hardcode version numbers in body text — update `README.md` and let automation propagate -- Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows — always use `secrets.GH_TOKEN` -- Never extend `CliBase` in PHP scripts — extend `MokoStandards\Enterprise\CliFramework` -- Never call `$script->run()` as the entry point — call `$script->execute()` -- Policy documents and guides must not be mixed - ---- - -## MokoStandards Reference - -This repository is governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). Authoritative policies: - -| Document | Purpose | -|----------|---------| -| [file-header-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/file-header-standards.md) | Copyright-header rules for every file type | -| [coding-style-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md) | Naming and formatting conventions | -| [branching-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/branching-strategy.md) | Branch naming, hierarchy, and release workflow | -| [merge-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR title/body conventions | -| [changelog-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md | -| [scripting-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/scripting-standards.md) | PHP script requirements and CliFramework usage | -| [package-installation.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/package-installation.md) | Installing `mokoconsulting/mokostandards` via Composer | -- 2.52.0 From 34f0ff74633add347d5940a3dd2137ea75dacdb9 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:45 +0000 Subject: [PATCH 080/136] chore: move .github/copilot.yml to .mokogitea/copilot.yml [skip ci] --- .mokogitea/copilot.yml | 137 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 .mokogitea/copilot.yml diff --git a/.mokogitea/copilot.yml b/.mokogitea/copilot.yml new file mode 100644 index 0000000..f00b751 --- /dev/null +++ b/.mokogitea/copilot.yml @@ -0,0 +1,137 @@ +# GitHub Copilot Configuration +# This file configures GitHub Copilot settings for the repository + +# Allowed domains for Copilot to access +# These domains are trusted sources that Copilot can fetch information from +allowed_domains: + # Standard license providers + - "www.gnu.org" # GNU licenses (GPL, LGPL, AGPL) + - "opensource.org" # Open Source Initiative + - "choosealicense.com" # GitHub's license chooser + - "spdx.org" # Software Package Data Exchange + - "creativecommons.org" # Creative Commons licenses + - "apache.org" # Apache Software Foundation + - "fsf.org" # Free Software Foundation + + # Documentation and standards + - "semver.org" # Semantic Versioning + - "keepachangelog.com" # Changelog standards + - "conventionalcommits.org" # Commit message standards + + # GitHub and related + - "github.com" # GitHub main site + - "docs.github.com" # GitHub documentation + - "raw.githubusercontent.com" # GitHub raw content + + # Package managers and registries + - "npmjs.com" # npm registry + - "pypi.org" # Python Package Index + - "packagist.org" # PHP Composer packages + - "rubygems.org" # Ruby gems + + # Standards and specifications + - "json-schema.org" # JSON Schema + - "w3.org" # W3C standards + - "ietf.org" # IETF RFCs and standards + + # PHP and Joomla specific + - "joomla.org" # Joomla CMS + - "docs.joomla.org" # Joomla documentation + - "downloads.joomla.org" # Joomla core downloads + - "php.net" # PHP documentation + - "getcomposer.org" # Composer dependency manager + - "packagist.org" # Composer package registry (also listed under packages) + + # Dolibarr specific + - "dolibarr.org" # Dolibarr ERP/CRM + - "wiki.dolibarr.org" # Dolibarr wiki + - "docs.dolibarr.org" # Dolibarr developer documentation + + # Moko Consulting + - "mokoconsulting.tech" # Moko Consulting main site + - "*.mokoconsulting.tech" # All Moko Consulting subdomains (API, docs, CDN, etc.) + + # Google services + - "drive.google.com" # Google Drive (file sharing and assets) + - "docs.google.com" # Google Docs + - "sheets.google.com" # Google Sheets + - "accounts.google.com" # Google authentication + - "storage.googleapis.com" # Google Cloud Storage + - "*.googleapis.com" # Google APIs (Maps, Fonts, etc.) + - "*.googleusercontent.com" # Google user-uploaded content and CDN + - "fonts.googleapis.com" # Google Fonts CSS + - "fonts.gstatic.com" # Google Fonts static assets + + # GitHub extended + - "api.github.com" # GitHub REST API + - "upload.github.com" # GitHub file uploads + - "objects.githubusercontent.com" # GitHub release assets and LFS + - "user-images.githubusercontent.com" # GitHub issue/PR image attachments + - "codeload.github.com" # GitHub archive downloads + - "ghcr.io" # GitHub Container Registry + - "pkg.github.com" # GitHub Packages + + # Developer reference + - "developer.mozilla.org" # MDN Web Docs + - "stackoverflow.com" # Stack Overflow + - "git-scm.com" # Git documentation + + # CDN and infrastructure + - "cdn.jsdelivr.net" # jsDelivr CDN + - "unpkg.com" # unpkg CDN + - "cdnjs.cloudflare.com" # Cloudflare CDN + - "img.shields.io" # Shields.io badge images + - "shields.io" # Shields.io badge service + + # Container registries + - "hub.docker.com" # Docker Hub + - "registry-1.docker.io" # Docker registry pulls + - "index.docker.io" # Docker index + + # CI / code quality + - "codecov.io" # Code coverage reporting + - "coveralls.io" # Coveralls coverage service + - "sonarcloud.io" # SonarCloud static analysis + + # Terraform / infrastructure + - "registry.terraform.io" # Terraform provider registry + - "releases.hashicorp.com" # HashiCorp release downloads + - "checkpoint-api.hashicorp.com" # HashiCorp update checks + +# Settings for code generation and suggestions +copilot: + # Enable Copilot for this repository + enabled: true + + # File patterns to include for Copilot suggestions + include: + - "**/*.py" + - "**/*.js" + - "**/*.php" + - "**/*.md" + - "**/*.yml" + - "**/*.yaml" + - "**/*.json" + - "**/*.xml" + - "**/*.sh" + + # File patterns to exclude from Copilot suggestions + exclude: + - "**/node_modules/**" + - "**/vendor/**" + - "**/build/**" + - "**/dist/**" + - "**/.git/**" + - "**/LICENSE" + - "**/CHANGELOG.md" + +# Notes: +# ------ +# - This configuration allows GitHub Copilot to fetch information from trusted sources +# - License providers are included to help with license text and compliance information +# - Package registries help with dependency management and version checking +# - Standards organizations provide authoritative specifications +# - Platform-specific sites (Joomla, Dolibarr, PHP) support our technology stack +# - All domains listed are well-known, reputable sources in their respective domains +# - This list focuses on read-only access to public information +# - No authentication credentials should be used with these domains -- 2.52.0 From 10cbb9bdb72d46d43d936e875c0b429e02c30948 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 05:09:45 +0000 Subject: [PATCH 081/136] chore: remove .github/copilot.yml (moved to .mokogitea/) [skip ci] --- .github/copilot.yml | 137 -------------------------------------------- 1 file changed, 137 deletions(-) delete mode 100644 .github/copilot.yml diff --git a/.github/copilot.yml b/.github/copilot.yml deleted file mode 100644 index f00b751..0000000 --- a/.github/copilot.yml +++ /dev/null @@ -1,137 +0,0 @@ -# GitHub Copilot Configuration -# This file configures GitHub Copilot settings for the repository - -# Allowed domains for Copilot to access -# These domains are trusted sources that Copilot can fetch information from -allowed_domains: - # Standard license providers - - "www.gnu.org" # GNU licenses (GPL, LGPL, AGPL) - - "opensource.org" # Open Source Initiative - - "choosealicense.com" # GitHub's license chooser - - "spdx.org" # Software Package Data Exchange - - "creativecommons.org" # Creative Commons licenses - - "apache.org" # Apache Software Foundation - - "fsf.org" # Free Software Foundation - - # Documentation and standards - - "semver.org" # Semantic Versioning - - "keepachangelog.com" # Changelog standards - - "conventionalcommits.org" # Commit message standards - - # GitHub and related - - "github.com" # GitHub main site - - "docs.github.com" # GitHub documentation - - "raw.githubusercontent.com" # GitHub raw content - - # Package managers and registries - - "npmjs.com" # npm registry - - "pypi.org" # Python Package Index - - "packagist.org" # PHP Composer packages - - "rubygems.org" # Ruby gems - - # Standards and specifications - - "json-schema.org" # JSON Schema - - "w3.org" # W3C standards - - "ietf.org" # IETF RFCs and standards - - # PHP and Joomla specific - - "joomla.org" # Joomla CMS - - "docs.joomla.org" # Joomla documentation - - "downloads.joomla.org" # Joomla core downloads - - "php.net" # PHP documentation - - "getcomposer.org" # Composer dependency manager - - "packagist.org" # Composer package registry (also listed under packages) - - # Dolibarr specific - - "dolibarr.org" # Dolibarr ERP/CRM - - "wiki.dolibarr.org" # Dolibarr wiki - - "docs.dolibarr.org" # Dolibarr developer documentation - - # Moko Consulting - - "mokoconsulting.tech" # Moko Consulting main site - - "*.mokoconsulting.tech" # All Moko Consulting subdomains (API, docs, CDN, etc.) - - # Google services - - "drive.google.com" # Google Drive (file sharing and assets) - - "docs.google.com" # Google Docs - - "sheets.google.com" # Google Sheets - - "accounts.google.com" # Google authentication - - "storage.googleapis.com" # Google Cloud Storage - - "*.googleapis.com" # Google APIs (Maps, Fonts, etc.) - - "*.googleusercontent.com" # Google user-uploaded content and CDN - - "fonts.googleapis.com" # Google Fonts CSS - - "fonts.gstatic.com" # Google Fonts static assets - - # GitHub extended - - "api.github.com" # GitHub REST API - - "upload.github.com" # GitHub file uploads - - "objects.githubusercontent.com" # GitHub release assets and LFS - - "user-images.githubusercontent.com" # GitHub issue/PR image attachments - - "codeload.github.com" # GitHub archive downloads - - "ghcr.io" # GitHub Container Registry - - "pkg.github.com" # GitHub Packages - - # Developer reference - - "developer.mozilla.org" # MDN Web Docs - - "stackoverflow.com" # Stack Overflow - - "git-scm.com" # Git documentation - - # CDN and infrastructure - - "cdn.jsdelivr.net" # jsDelivr CDN - - "unpkg.com" # unpkg CDN - - "cdnjs.cloudflare.com" # Cloudflare CDN - - "img.shields.io" # Shields.io badge images - - "shields.io" # Shields.io badge service - - # Container registries - - "hub.docker.com" # Docker Hub - - "registry-1.docker.io" # Docker registry pulls - - "index.docker.io" # Docker index - - # CI / code quality - - "codecov.io" # Code coverage reporting - - "coveralls.io" # Coveralls coverage service - - "sonarcloud.io" # SonarCloud static analysis - - # Terraform / infrastructure - - "registry.terraform.io" # Terraform provider registry - - "releases.hashicorp.com" # HashiCorp release downloads - - "checkpoint-api.hashicorp.com" # HashiCorp update checks - -# Settings for code generation and suggestions -copilot: - # Enable Copilot for this repository - enabled: true - - # File patterns to include for Copilot suggestions - include: - - "**/*.py" - - "**/*.js" - - "**/*.php" - - "**/*.md" - - "**/*.yml" - - "**/*.yaml" - - "**/*.json" - - "**/*.xml" - - "**/*.sh" - - # File patterns to exclude from Copilot suggestions - exclude: - - "**/node_modules/**" - - "**/vendor/**" - - "**/build/**" - - "**/dist/**" - - "**/.git/**" - - "**/LICENSE" - - "**/CHANGELOG.md" - -# Notes: -# ------ -# - This configuration allows GitHub Copilot to fetch information from trusted sources -# - License providers are included to help with license text and compliance information -# - Package registries help with dependency management and version checking -# - Standards organizations provide authoritative specifications -# - Platform-specific sites (Joomla, Dolibarr, PHP) support our technology stack -# - All domains listed are well-known, reputable sources in their respective domains -# - This list focuses on read-only access to public information -# - No authentication credentials should be used with these domains -- 2.52.0 From b4e656301df9864ae111c3e6f95b0e19c66618f5 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 18:55:39 +0000 Subject: [PATCH 082/136] chore: sync .mokogitea/workflows/auto-release.yml from template [skip ci] --- .mokogitea/workflows/auto-release.yml | 1007 +++++++++++++++++++++++++ 1 file changed, 1007 insertions(+) create mode 100644 .mokogitea/workflows/auto-release.yml diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml new file mode 100644 index 0000000..1fe7aa6 --- /dev/null +++ b/.mokogitea/workflows/auto-release.yml @@ -0,0 +1,1007 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Release +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# PATH: /templates/workflows/universal/auto-release.yml.template +# VERSION: 05.00.00 +# BRIEF: Universal build & release � detects platform from .moko-platform +# +# +========================================================================+ +# | UNIVERSAL BUILD & RELEASE PIPELINE | +# +========================================================================+ +# | | +# | Reads .moko-platform (joomla|dolibarr|generic) to branch logic. | +# | | +# | Platform-specific: | +# | joomla: XML manifest, updates.xml, type-prefixed packages | +# | dolibarr: mod*.class.php, update.txt, dev version reset | +# | generic: README-only, no update stream | +# | | +# +========================================================================+ + +name: "Universal: Build & Release" + +on: + pull_request: + types: [closed] + branches: + - main + paths: + - 'src/**' + - 'htdocs/**' + workflow_dispatch: + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +permissions: + contents: write + +jobs: + release: + name: Build & Release Pipeline + runs-on: release + if: >- + github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.GA_TOKEN }} + fetch-depth: 0 + + - name: Setup MokoStandards tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN }}"}}' + run: | + # Ensure PHP + Composer are available + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \ + /tmp/mokostandards-api + cd /tmp/mokostandards-api + composer install --no-dev --no-interaction --quiet + + + # -- PLATFORM DETECTION --------------------------------------------------- + - name: Detect platform + id: platform + run: | + # Read platform from XML manifest ( tag) or plain text fallback + PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/.moko-platform 2>/dev/null | head -1) + [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/.moko-platform 2>/dev/null | tr -d '[:space:]') + [ -z "$PLATFORM" ] && PLATFORM="generic" + echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" + echo "Platform detected: ${PLATFORM}" + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) + MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1) + echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT" + echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT" + + # -- STEP 1: Read version ----------------------------------------------- + - name: "Step 1: Read version from README.md" + id: version + run: | + VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null) + if [ -z "$VERSION" ]; then + echo "No VERSION in README.md — skipping release" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + # Derive major.minor for branch naming (patches update existing branch) + MINOR=$(echo "$VERSION" | awk -F. '{printf "%s.%s", $1, $2}') + PATCH=$(echo "$VERSION" | awk -F. '{print $3}') + + MAJOR=$(echo "$VERSION" | awk -F. '{print $1}') + MINOR_NUM=$(echo "$VERSION" | awk -F. '{print $2}') + + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT" + echo "minor=$MINOR" >> "$GITHUB_OUTPUT" + echo "major=$MAJOR" >> "$GITHUB_OUTPUT" + echo "release_tag=stable" >> "$GITHUB_OUTPUT" + echo "stability=stable" >> "$GITHUB_OUTPUT" + echo "skip=false" >> "$GITHUB_OUTPUT" + if [ "$PATCH" = "00" ] || [ "$PATCH" = "01" ]; then + echo "is_minor=true" >> "$GITHUB_OUTPUT" + echo "Version: $VERSION (first release for this minor — full pipeline)" + else + echo "is_minor=false" >> "$GITHUB_OUTPUT" + echo "Version: $VERSION (patch — platform version + badges only)" + fi + + # -- STEP 1b: Bump minor version (stable = minor bump, reset patch) ------ + - name: "Step 1b: Bump minor version for stable release" + if: steps.version.outputs.skip != 'true' + id: bump + run: | + CURRENT=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1) + [ -z "$CURRENT" ] && { echo "skip=true" >> "$GITHUB_OUTPUT"; exit 0; } + + MAJOR=$((10#$(echo "$CURRENT" | cut -d. -f1))) + MINOR=$((10#$(echo "$CURRENT" | cut -d. -f2))) + + # Minor bump, reset patch. Rollover if minor > 99 + MINOR=$((MINOR + 1)) + if [ $MINOR -gt 99 ]; then + MINOR=0 + MAJOR=$((MAJOR + 1)) + fi + + VERSION=$(printf "%02d.%02d.00" $MAJOR $MINOR) + TODAY=$(date +%Y-%m-%d) + + echo "Stable bump: ${CURRENT} → ${VERSION} (minor)" + + # Update README.md + sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${VERSION}/" README.md + + # Update platform-specific manifest + PLATFORM="${{ steps.platform.outputs.platform }}" + MANIFEST="${{ steps.platform.outputs.manifest }}" + MOD_FILE="${{ steps.platform.outputs.mod_file }}" + case "$PLATFORM" in + joomla) + if [ -n "$MANIFEST" ]; then + MANIFEST_VER=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1) + [ -n "$MANIFEST_VER" ] && sed -i "s|${MANIFEST_VER}|${VERSION}|" "$MANIFEST" + sed -i "s|[^<]*|${TODAY}|" "$MANIFEST" + fi + ;; + dolibarr) + if [ -n "$MOD_FILE" ]; then + sed -i "s/\$this->version = '[^']*'/\$this->version = '${VERSION}'/" "$MOD_FILE" + fi + echo "${VERSION}" > update.txt + ;; + *) ;; + esac + + # Promote [Unreleased] section in CHANGELOG.md to new version + if [ -f "CHANGELOG.md" ] && grep -qi "Unreleased" CHANGELOG.md; then + sed -i "s|## \[Unreleased\]|## [${VERSION}] --- ${TODAY}|" CHANGELOG.md + sed -i "s|## Unreleased|## [${VERSION}] --- ${TODAY}|" CHANGELOG.md + sed -i "2i ## [Unreleased]" CHANGELOG.md + sed -i "3i \\ " CHANGELOG.md + echo "CHANGELOG promoted to [${VERSION}]" + fi + + # Commit and push + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git add -A + git diff --cached --quiet || { + git commit -m "chore(version): bump ${CURRENT} → ${VERSION} [skip ci]" + git push origin HEAD:main 2>&1 + } + + # Override version output for rest of pipeline + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "major=$(printf "%02d" $MAJOR)" >> "$GITHUB_OUTPUT" + + - name: Check if already released + if: steps.version.outputs.skip != 'true' + id: check + run: | + TAG="${{ steps.version.outputs.release_tag }}" + BRANCH="${{ steps.version.outputs.branch }}" + + TAG_EXISTS=false + BRANCH_EXISTS=false + + git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true + git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH" && BRANCH_EXISTS=true + + echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT" + echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT" + + # Tag and branch may persist across patch releases — never skip + echo "already_released=false" >> "$GITHUB_OUTPUT" + + # -- SANITY CHECKS ------------------------------------------------------- + - name: "Sanity: Pre-release validation" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + ERRORS=0 + + PLATFORM="${{ steps.platform.outputs.platform }}" + MANIFEST="${{ steps.platform.outputs.manifest }}" + MOD_FILE="${{ steps.platform.outputs.mod_file }}" + echo "## Pre-Release Sanity Checks (${PLATFORM})" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # -- Version drift check (must pass before release) -------- + README_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1) + if [ "$README_VER" != "$VERSION" ]; then + echo "- Version drift: README says \`${README_VER}\` but releasing \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + else + echo "- Version consistent: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + + # Check CHANGELOG version matches + CL_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' CHANGELOG.md 2>/dev/null | head -1) + if [ -n "$CL_VER" ] && [ "$CL_VER" != "$VERSION" ]; then + echo "- CHANGELOG drift: \`${CL_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + fi + + # Check composer.json version if present + if [ -f "composer.json" ]; then + COMP_VER=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' composer.json 2>/dev/null | head -1) + if [ -n "$COMP_VER" ] && [ "$COMP_VER" != "$VERSION" ]; then + echo "- composer.json drift: \`${COMP_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + fi + fi + + # Common checks + if [ ! -f "LICENSE" ]; then + echo "- Missing LICENSE file" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + else + echo "- LICENSE present" >> $GITHUB_STEP_SUMMARY + fi + + if [ ! -d "src" ] && [ ! -d "htdocs" ]; then + echo "- Warning: No src/ or htdocs/ directory" >> $GITHUB_STEP_SUMMARY + else + echo "- Source directory present" >> $GITHUB_STEP_SUMMARY + fi + + # -- Platform-specific checks -------- + case "$PLATFORM" in + joomla) + if [ -n "$MANIFEST" ]; then + XML_VER=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1) + if [ -n "$XML_VER" ] && [ "$XML_VER" != "$VERSION" ]; then + echo "- Manifest drift: \`${XML_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + else + echo "- Manifest version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null) + echo "- Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY + else + echo "- No Joomla XML manifest (WaaS site)" >> $GITHUB_STEP_SUMMARY + fi ;; + dolibarr) + if [ -n "$MOD_FILE" ]; then + MOD_VER=$(sed -n "s/.*\\\$this->version = '\([^']*\)'.*/\1/p" "$MOD_FILE" 2>/dev/null | head -1) + if [ -n "$MOD_VER" ] && [ "$MOD_VER" != "$VERSION" ]; then + echo "- Module drift: \`${MOD_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + else + echo "- Module version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + else + echo "- No mod*.class.php found" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + fi + if [ ! -f "update.txt" ]; then + echo "- Missing update.txt" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + fi ;; + *) echo "- Generic platform � no manifest checks" >> $GITHUB_STEP_SUMMARY ;; + esac + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "$ERRORS" -gt 0 ]; then + echo "**${ERRORS} error(s) — release may be incomplete**" >> $GITHUB_STEP_SUMMARY + else + echo "**All sanity checks passed**" >> $GITHUB_STEP_SUMMARY + fi + + # -- STEP 2: Create or update version/XX.YY archive branch --------------- + # Always runs — every version change on main archives to version/XX.YY + - name: "Step 2: Version archive branch" + if: steps.check.outputs.already_released != 'true' + run: | + BRANCH="${{ steps.version.outputs.branch }}" + IS_MINOR="${{ steps.version.outputs.is_minor }}" + PATCH="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}') + + # Check if branch exists + if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then + git push origin HEAD:"$BRANCH" --force + echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY + else + git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH" + git push origin "$BRANCH" --force + echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY + fi + + # -- STEP 3: Set platform version ---------------------------------------- + - name: "Step 3: Set platform version" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + php /tmp/mokostandards-api/cli/version_set_platform.php \ + --path . --version "$VERSION" --branch main + + # -- STEP 4: Update version badges ---------------------------------------- + - name: "Step 4: Update version badges" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + find . -name "*.md" ! -path "./.git/*" ! -path "./vendor/*" | while read -r f; do + if grep -q '\[VERSION:' "$f" 2>/dev/null; then + sed -i "s/\[VERSION:[[:space:]]*[0-9]\{2\}\.[0-9]\{2\}\.[0-9]\{2\}\]/[VERSION: ${VERSION}]/" "$f" + fi + done + + # -- STEP 5: Write updates.xml (Joomla update server) --------------------- + - name: "Step 5: Write update stream" + id: updates + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + REPO="${{ github.repository }}" + + # -- Parse extension metadata from XML manifest ---------------- + MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1) + if [ -z "$MANIFEST" ]; then + echo "Warning: No Joomla XML manifest found — skipping updates.xml" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + # Extract fields using sed (portable — no grep -P) + EXT_NAME=$(sed -n 's/.*\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1) + EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1) + EXT_CLIENT=$(sed -n 's/.*]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_FOLDER=$(sed -n 's/.*]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + TARGET_PLATFORM=$(sed -n 's/.*\(\).*/\1/p' "$MANIFEST" | head -1) + PHP_MINIMUM=$(sed -n 's/.*\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1) + + # If EXT_NAME is a language key (e.g. PLG_SYSTEM_MOKOJGDPC), resolve from .ini + if echo "$EXT_NAME" | grep -qE '^[A-Z_]+$'; then + INI_NAME=$(find . -name "*.sys.ini" -path "*/en-GB/*" -exec grep -h "^${EXT_NAME}=" {} \; 2>/dev/null | head -1 | cut -d'"' -f2) + [ -z "$INI_NAME" ] && INI_NAME=$(find . -name "*.sys.ini" -exec grep -h "^${EXT_NAME}=" {} \; 2>/dev/null | head -1 | cut -d'"' -f2) + [ -n "$INI_NAME" ] && EXT_NAME="$INI_NAME" + fi + + # Fallbacks + [ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}" + [ -z "$EXT_TYPE" ] && EXT_TYPE="component" + + # Derive element if not in manifest: + # 1. plugin="xxx" attribute (plugins) + # 2. module="xxx" attribute (modules) + # 3. XML filename (components, packages) + # 4. Repo name fallback (templates, anything else) + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(sed -n 's/.*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + fi + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(sed -n 's/.*module="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + fi + if [ -z "$EXT_ELEMENT" ]; then + FNAME=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') + # If filename is generic (templateDetails, manifest), use repo name + case "$FNAME" in + templatedetails|manifest) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;; + *) EXT_ELEMENT="$FNAME" ;; + esac + fi + # Final fallback + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + + # Save for Steps 7, 8, 8b + echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" + echo "ext_name=${EXT_NAME}" >> "$GITHUB_OUTPUT" + echo "ext_type=${EXT_TYPE}" >> "$GITHUB_OUTPUT" + echo "ext_folder=${EXT_FOLDER}" >> "$GITHUB_OUTPUT" + + # Build client tag: plugins and frontend modules need site + CLIENT_TAG="" + if [ -n "$EXT_CLIENT" ]; then + CLIENT_TAG="${EXT_CLIENT}" + elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then + CLIENT_TAG="site" + fi + + # Build folder tag for plugins (required for Joomla to match the update) + FOLDER_TAG="" + if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then + FOLDER_TAG="${EXT_FOLDER}" + fi + + # Build targetplatform (fallback to Joomla 5 if not in manifest) + if [ -z "$TARGET_PLATFORM" ]; then + TARGET_PLATFORM=$(printf '' "/") + fi + + # Build php_minimum tag + PHP_TAG="" + if [ -n "$PHP_MINIMUM" ]; then + PHP_TAG="${PHP_MINIMUM}" + fi + + # Build TYPE_PREFIX for download URL + TYPE_PREFIX="" + case "${EXT_TYPE}" in + plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; + module) TYPE_PREFIX="mod_" ;; + component) TYPE_PREFIX="com_" ;; + template) TYPE_PREFIX="tpl_" ;; + library) TYPE_PREFIX="lib_" ;; + package) TYPE_PREFIX="pkg_" ;; + esac + + DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/stable/${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip" + INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/stable" + + # -- Build update entry for a given stability tag + build_entry() { + local TAG_NAME="$1" + printf '%s\n' ' ' + printf '%s\n' " ${EXT_NAME}" + printf '%s\n' " ${EXT_NAME} update" + printf '%s\n' " ${EXT_ELEMENT}" + printf '%s\n' " ${EXT_TYPE}" + printf '%s\n' " ${VERSION}" + [ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}" + [ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}" + printf '%s\n' " ${TAG_NAME}" + printf '%s\n' " ${INFO_URL}" + printf '%s\n' ' ' + printf '%s\n' " ${DOWNLOAD_URL}" + printf '%s\n' ' ' + printf '%s\n' " ${TARGET_PLATFORM}" + [ -n "$PHP_TAG" ] && printf '%s\n' " ${PHP_TAG}" + printf '%s\n' ' Moko Consulting' + printf '%s\n' ' https://mokoconsulting.tech' + printf '%s\n' ' ' + } + + # -- Write updates.xml with cascading channels + # Stable release updates ALL channels (development, alpha, beta, rc, stable) + { + printf '%s\n' "" + printf '%s\n' "" + printf '%s\n' "" + printf '%s\n' '' + build_entry "development" + build_entry "alpha" + build_entry "beta" + build_entry "rc" + build_entry "stable" + printf '%s\n' '' + } > updates.xml + + echo "updates.xml: ${VERSION} (all channels updated to stable)" >> $GITHUB_STEP_SUMMARY + + # -- Commit all changes --------------------------------------------------- + - name: Commit release changes + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + if git diff --quiet && git diff --cached --quiet; then + echo "No changes to commit" + exit 0 + fi + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + # Set push URL with token for branch-protected repos + git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git add -A + git commit -m "chore(release): build ${VERSION} [skip ci]" \ + --author="gitea-actions[bot] " + git push -u origin HEAD + + # -- STEP 6: Create tag --------------------------------------------------- + - name: "Step 6: Create git tag" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.tag_exists != 'true' && + steps.version.outputs.is_minor == 'true' + run: | + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + # Only create the major release tag if it doesn't exist yet + if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then + git tag "$RELEASE_TAG" + git push origin "$RELEASE_TAG" + echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY + else + echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY + fi + echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY + + # -- STEP 7: Create or update Gitea Release -------------------------------- + - name: "Step 7: Gitea Release" + if: >- + steps.version.outputs.skip != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + BRANCH="${{ steps.version.outputs.branch }}" + MAJOR="${{ steps.version.outputs.major }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + # Reuse metadata from Step 5 (single source of truth) + EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}" + EXT_NAME="${{ steps.updates.outputs.ext_name }}" + EXT_TYPE="${{ steps.updates.outputs.ext_type }}" + EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}" + + # Fallbacks if Step 5 was skipped + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + fi + [ -z "$EXT_NAME" ] && EXT_NAME="${GITEA_REPO}" + + NOTES=$(php /tmp/mokostandards-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null) + [ -z "$NOTES" ] && NOTES="Release ${VERSION}" + + # Build release name: "Pretty Name VERSION (type_element-VERSION)" + TYPE_PREFIX="" + case "${EXT_TYPE}" in + plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; + module) TYPE_PREFIX="mod_" ;; + component) TYPE_PREFIX="com_" ;; + template) TYPE_PREFIX="tpl_" ;; + library) TYPE_PREFIX="lib_" ;; + package) TYPE_PREFIX="pkg_" ;; + esac + RELEASE_NAME="${EXT_NAME} ${VERSION} (${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION})" + + # Delete existing release if present (overwrite, not append) + EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true) + EXISTING_ID=$(echo "$EXISTING" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true) + + if [ -n "$EXISTING_ID" ]; then + curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/${EXISTING_ID}" 2>/dev/null || true + curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/tags/${RELEASE_TAG}" 2>/dev/null || true + echo "Deleted previous stable release (id: ${EXISTING_ID})" + fi + + # Create fresh release + curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/releases" \ + -d "$(python3 -c "import json; print(json.dumps({ + 'tag_name': '${RELEASE_TAG}', + 'name': '${RELEASE_NAME}', + 'body': '''## ${VERSION} ($(date +%Y-%m-%d))\n${NOTES}''', + 'target_commitish': '${BRANCH}' + }))")" + echo "Release created: ${RELEASE_NAME}" >> $GITHUB_STEP_SUMMARY + + # -- STEP 8: Build Joomla install ZIP + SHA-256 checksum ------------------ + - name: "Step 8: Build package and update checksum" + if: >- + steps.version.outputs.skip != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + REPO="${{ github.repository }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + # All ZIPs upload to the major release tag (vXX) + RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true) + RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + if [ -z "$RELEASE_ID" ]; then + echo "No release ${RELEASE_TAG} found — skipping ZIP upload" + exit 0 + fi + + # Find extension element name from manifest + MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1 || true) + [ -z "$MANIFEST" ] && exit 0 + + # Reuse element from Step 5, with same fallback chain + EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}" + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1) + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(sed -n 's/.*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null | head -1) + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + fi + # ZIP name: type_folder_element-VERSION (e.g. plg_system_mokojgdpc-01.01.00.zip) + EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_FOLDER=$(sed -n 's/.*]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + TYPE_PREFIX="" + case "${EXT_TYPE}" in + plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; + module) TYPE_PREFIX="mod_" ;; + component) TYPE_PREFIX="com_" ;; + template) TYPE_PREFIX="tpl_" ;; + library) TYPE_PREFIX="lib_" ;; + package) TYPE_PREFIX="pkg_" ;; + esac + ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip" + TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz" + + # -- Build install packages from src/ ---------------------------- + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + [ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ — skipping package"; exit 0; } + + EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*" + + # ZIP package + cd "$SOURCE_DIR" + zip -r "/tmp/${ZIP_NAME}" . -x $EXCLUDES + cd .. + + # tar.gz package + tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \ + --exclude='.ftpignore' --exclude='sftp-config*' \ + --exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' . + + ZIP_SIZE=$(stat -c%s "/tmp/${ZIP_NAME}" 2>/dev/null || stat -f%z "/tmp/${ZIP_NAME}" 2>/dev/null || echo "unknown") + TAR_SIZE=$(stat -c%s "/tmp/${TAR_NAME}" 2>/dev/null || stat -f%z "/tmp/${TAR_NAME}" 2>/dev/null || echo "unknown") + + # -- Calculate SHA-256 for both ---------------------------------- + SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1) + SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1) + + # -- Delete existing assets with same name before uploading ------ + ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]") + for ASSET_NAME in "$ZIP_NAME" "$TAR_NAME"; do + ASSET_ID=$(echo "$ASSETS" | python3 -c " + import sys,json + assets = json.load(sys.stdin) + for a in assets: + if a['name'] == '${ASSET_NAME}': + print(a['id']); break + " 2>/dev/null || true) + if [ -n "$ASSET_ID" ]; then + curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true + fi + done + + # -- Upload both to release tag ---------------------------------- + curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @"/tmp/${ZIP_NAME}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" > /dev/null 2>&1 || true + + curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @"/tmp/${TAR_NAME}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets?name=${TAR_NAME}" > /dev/null 2>&1 || true + + # -- Update updates.xml with both download formats --------------- + if [ -f "updates.xml" ]; then + ZIP_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}" + TAR_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${TAR_NAME}" + + # Use Python to update only the stable entry's downloads + sha256 + export PY_ZIP_URL="$ZIP_URL" PY_TAR_URL="$TAR_URL" PY_SHA="$SHA256_ZIP" + python3 << 'PYEOF' + import re, os + + with open("updates.xml") as f: + content = f.read() + + zip_url = os.environ["PY_ZIP_URL"] + tar_url = os.environ["PY_TAR_URL"] + sha = os.environ["PY_SHA"] + + # Find the stable update block and replace its downloads + sha256 + def replace_stable(m): + block = m.group(0) + # Replace downloads block + new_downloads = ( + " \n" + f" {zip_url}\n" + " " + ) + block = re.sub(r' .*?', new_downloads, block, flags=re.DOTALL) + # Add or replace sha256 + if '' in block: + block = re.sub(r' .*?', f' {sha}', block) + else: + block = block.replace('', f'\n {sha}') + return block + + content = re.sub( + r' .*?stable.*?', + replace_stable, + content, + flags=re.DOTALL + ) + + with open("updates.xml", "w") as f: + f.write(content) + PYEOF + + CURRENT_BRANCH="${{ github.ref_name }}" + git add updates.xml + git commit -m "chore(release): ZIP + tar.gz for ${VERSION} [skip ci]" \ + --author="gitea-actions[bot] " || true + git push || true + + # Sync updates.xml to main via direct API (always runs — may be on version/XX branch) + GA_TOKEN="${{ secrets.GA_TOKEN }}" + API="${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}" + + FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \ + "${API}/contents/updates.xml?ref=main" | jq -r '.sha // empty') + + if [ -n "$FILE_SHA" ]; then + CONTENT=$(base64 -w0 updates.xml) + curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/contents/updates.xml" \ + -d "$(jq -n \ + --arg content "$CONTENT" \ + --arg sha "$FILE_SHA" \ + --arg msg "chore: sync updates.xml ${VERSION} [skip ci]" \ + --arg branch "main" \ + '{content: $content, sha: $sha, message: $msg, branch: $branch}' + )" > /dev/null 2>&1 \ + && echo "updates.xml synced to main via API" \ + || echo "WARNING: failed to sync updates.xml to main" + else + echo "WARNING: could not get updates.xml SHA from main" + fi + fi + + echo "### Packages" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Package | Size | SHA-256 |" >> $GITHUB_STEP_SUMMARY + echo "|---------|------|---------|" >> $GITHUB_STEP_SUMMARY + echo "| \`${ZIP_NAME}\` | ${ZIP_SIZE} | \`${SHA256_ZIP}\` |" >> $GITHUB_STEP_SUMMARY + echo "| \`${TAR_NAME}\` | ${TAR_SIZE} | \`${SHA256_TAR}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Release | \`${RELEASE_TAG}\` | |" >> $GITHUB_STEP_SUMMARY + echo "| Download | [${ZIP_NAME}](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}) |" >> $GITHUB_STEP_SUMMARY + + # -- STEP 8b: Update release description with changelog + SHA ---------------- + - name: "Step 8b: Update release body with changelog and SHA" + if: steps.version.outputs.skip != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}" + EXT_TYPE="${{ steps.updates.outputs.ext_type }}" + EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}" + + # Build TYPE_PREFIX to match Step 8's ZIP naming + TYPE_PREFIX="" + case "${EXT_TYPE}" in + plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; + module) TYPE_PREFIX="mod_" ;; + component) TYPE_PREFIX="com_" ;; + template) TYPE_PREFIX="tpl_" ;; + library) TYPE_PREFIX="lib_" ;; + package) TYPE_PREFIX="pkg_" ;; + esac + ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip" + TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz" + + # Get SHA from the built files + SHA256_ZIP="" + [ -f "/tmp/${ZIP_NAME}" ] && SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1) + SHA256_TAR="" + [ -f "/tmp/${TAR_NAME}" ] && SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1) + + # Extract latest changelog entry (strip the ## header to avoid duplicate) + CHANGELOG="" + if [ -f "CHANGELOG.md" ]; then + CHANGELOG=$(sed -n "/^## \[*${VERSION}/,/^## \[*[0-9]/p" CHANGELOG.md | sed '$d' | sed '1d') + [ -z "$CHANGELOG" ] && CHANGELOG=$(sed -n '/^## /,/^## /p' CHANGELOG.md | sed '$d' | sed '1d' | head -30) + fi + + # Build release body (single header, no duplicate from changelog) + BODY="## ${VERSION} ($(date +%Y-%m-%d))\n\n" + if [ -n "$CHANGELOG" ]; then + BODY="${BODY}${CHANGELOG}\n\n" + fi + BODY="${BODY}---\n\n### Checksums\n\n" + BODY="${BODY}| File | SHA-256 |\n|------|--------|\n" + [ -n "$SHA256_ZIP" ] && BODY="${BODY}| \`${ZIP_NAME}\` | \`${SHA256_ZIP}\` |\n" + [ -n "$SHA256_TAR" ] && BODY="${BODY}| \`${TAR_NAME}\` | \`${SHA256_TAR}\` |\n" + + # Get release ID and update body + RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null | \ + python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + + if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then + python3 -c " + import json, urllib.request + body = '''$(printf '%b' "$BODY")''' + data = json.dumps({'body': body}).encode() + req = urllib.request.Request( + '${API_BASE}/releases/${RELEASE_ID}', + data=data, + headers={'Authorization': 'token ${{ secrets.GA_TOKEN }}', 'Content-Type': 'application/json'}, + method='PATCH' + ) + urllib.request.urlopen(req) + " 2>/dev/null && echo "Release body updated with changelog + SHA" >> $GITHUB_STEP_SUMMARY + fi + + # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- + - name: "Step 9: Mirror release to GitHub" + if: >- + steps.version.outputs.skip != 'true' && + steps.version.outputs.stability == 'stable' && + secrets.GH_TOKEN != '' + continue-on-error: true + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + MAJOR="${{ steps.version.outputs.major }}" + BRANCH="${{ steps.version.outputs.branch }}" + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + + NOTES=$(php /tmp/mokostandards-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null || true) + [ -z "$NOTES" ] && NOTES="Release ${VERSION}" + echo "$NOTES" > /tmp/release_notes.md + + EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".tag_name // empty" || true) + + if [ -z "$EXISTING" ]; then + gh release create "$RELEASE_TAG" \ + --repo "$GH_REPO" \ + --title "v${MAJOR} (latest: ${VERSION})" \ + --notes-file /tmp/release_notes.md \ + --target "$BRANCH" || true + else + gh release edit "$RELEASE_TAG" \ + --repo "$GH_REPO" \ + --title "v${MAJOR} (latest: ${VERSION})" || true + fi + + # Upload assets to GitHub mirror + for PKG in /tmp/${EXT_ELEMENT:-pkg}-${VERSION}.*; do + if [ -f "$PKG" ]; then + _RELID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".id // empty") + [ -n "$_RELID" ] && curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" -H "Content-Type: application/octet-stream" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/${_RELID}/assets?name=$(basename $PKG)" --data-binary "@$PKG" > /dev/null 2>&1 || true + fi + done + echo "GitHub mirror updated: ${GH_REPO} ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY + + # -- STEP 10: Sync main branch to GitHub mirror ---------------------------- + - name: "Step 10: Push main to GitHub mirror" + if: >- + steps.version.outputs.skip != 'true' && + secrets.GH_TOKEN != '' + continue-on-error: true + run: | + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1) + GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2) + git remote add github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ + git remote set-url github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" + git fetch origin main --depth=1 + git push github origin/main:refs/heads/main --force 2>/dev/null \ + && echo "main branch pushed to GitHub mirror" \ + || echo "WARNING: GitHub mirror push failed" + + # -- Clean up lesser pre-releases (cascade) --------------------------------- + # stable → deletes all | rc → beta,alpha,dev | beta → alpha,dev | alpha → dev + - name: "Delete lesser pre-release channels" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.GA_TOKEN }}" + + # Stable deletes all pre-release channels + TAGS_TO_DELETE="development alpha beta release-candidate" + + DELETED=0 + for TAG in $TAGS_TO_DELETE; do + RELEASE_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/releases/tags/${TAG}" 2>/dev/null | \ + python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + + if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then + curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/releases/${RELEASE_ID}" 2>/dev/null || true + curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/tags/${TAG}" 2>/dev/null || true + echo "Deleted: ${TAG} (id: ${RELEASE_ID})" + DELETED=$((DELETED + 1)) + fi + done + echo "Cleaned up ${DELETED} pre-release channel(s)" >> $GITHUB_STEP_SUMMARY + + # -- STEP 11: Reset dev branch from main ------------------------------------ + - name: "Step 11: Delete and recreate dev branch from main" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.GA_TOKEN }}" + + # Delete dev branch + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch" + + # Recreate dev from main (now includes version bump + changelog promotion) + curl -sf -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/branches" \ + -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main" + + echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY + + + # -- Dolibarr post-release: Reset dev version ----------------------------- + - name: "Dolibarr: Reset dev version" + if: >- + steps.version.outputs.skip != 'true' && + steps.platform.outputs.platform == 'dolibarr' && + steps.platform.outputs.mod_file != '' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.GA_TOKEN }}" + MOD_FILE="${{ steps.platform.outputs.mod_file }}" + ENCODED_PATH=$(echo "$MOD_FILE" | sed 's|^\./||' | python3 -c "import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip()))") + FILE_RESP=$(curl -sf -H "Authorization: token ${TOKEN}" "${API_BASE}/contents/${ENCODED_PATH}?ref=dev" 2>/dev/null || true) + FILE_SHA=$(echo "$FILE_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true) + FILE_CONTENT=$(echo "$FILE_RESP" | python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin).get('content','')).decode())" 2>/dev/null || true) + if [ -n "$FILE_SHA" ] && [ -n "$FILE_CONTENT" ]; then + UPDATED=$(echo "$FILE_CONTENT" | sed "s/\$this->version = '[^']*'/\$this->version = 'development'/") + ENCODED=$(echo "$UPDATED" | base64 -w0) + curl -sf -X PUT -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/contents/${ENCODED_PATH}" \ + -d "$(jq -n --arg content \"$ENCODED\" --arg sha \"$FILE_SHA\" --arg msg \"chore(version): reset dev version [skip ci]\" --arg branch \"dev\" '{content:$content,sha:$sha,message:$msg,branch:$branch}')" > /dev/null 2>&1 || true + fi + + # -- Summary -------------------------------------------------------------- + - name: Pipeline Summary + if: always() + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + PLATFORM="${{ steps.platform.outputs.platform }}" + if [ "${{ steps.version.outputs.skip }}" = "true" ]; then + echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY + echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY + elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then + echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY + echo "|------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY + fi -- 2.52.0 From 5507d53289503d3992a293dda8595b12784be326 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 18:55:40 +0000 Subject: [PATCH 083/136] chore: sync .mokogitea/workflows/cascade-dev.yml from template [skip ci] --- .mokogitea/workflows/cascade-dev.yml | 213 +++++++++++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 .mokogitea/workflows/cascade-dev.yml diff --git a/.mokogitea/workflows/cascade-dev.yml b/.mokogitea/workflows/cascade-dev.yml new file mode 100644 index 0000000..4dbb135 --- /dev/null +++ b/.mokogitea/workflows/cascade-dev.yml @@ -0,0 +1,213 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Maintenance +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# PATH: /templates/workflows/cascade-dev.yml.template +# VERSION: 02.00.00 +# BRIEF: Forward-merge main → all open branches after every push to main +# +# +========================================================================+ +# | CASCADE MAIN → ALL BRANCHES | +# +========================================================================+ +# | | +# | Triggers on every push to main (PR merges, bot commits, etc.) | +# | | +# | 1. List all branches matching: dev, rc/*, beta/*, alpha/* | +# | 2. For each: create PR (main → branch), auto-merge if clean | +# | 3. On conflict: leave PR open for manual resolution | +# | | +# +========================================================================+ + +name: "Universal: Cascade Main → Dev" + +on: + push: + branches: + - main + workflow_dispatch: + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +permissions: + contents: write + pull-requests: write + +jobs: + cascade: + name: Cascade main → branches + runs-on: ubuntu-latest + if: >- + !contains(github.event.head_commit.message, '[skip ci]') && + !contains(github.event.head_commit.message, '[skip cascade]') + + steps: + - name: Discover target branches + id: branches + env: + GA_TOKEN: ${{ secrets.GA_TOKEN }} + run: | + API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + # Fetch all branches (paginated) + PAGE=1 + ALL_BRANCHES="" + while true; do + BATCH=$(curl -sS \ + -H "Authorization: token ${GA_TOKEN}" \ + "${API}/branches?page=${PAGE}&limit=50" \ + | jq -r '.[].name // empty') + [ -z "$BATCH" ] && break + ALL_BRANCHES="$ALL_BRANCHES $BATCH" + PAGE=$((PAGE + 1)) + done + + # Filter to cascade targets: dev, dev/*, rc/*, beta/*, alpha/* + TARGETS="" + for BRANCH in $ALL_BRANCHES; do + case "$BRANCH" in + dev|dev/*|rc/*|beta/*|alpha/*) + TARGETS="$TARGETS $BRANCH" + ;; + esac + done + + TARGETS=$(echo "$TARGETS" | xargs) # trim whitespace + + if [ -z "$TARGETS" ]; then + echo "targets=" >> "$GITHUB_OUTPUT" + echo "ℹ️ No cascade target branches found" + else + echo "targets=$TARGETS" >> "$GITHUB_OUTPUT" + COUNT=$(echo "$TARGETS" | wc -w) + echo "📋 Found ${COUNT} target branch(es): ${TARGETS}" + fi + + - name: Cascade to all target branches + if: steps.branches.outputs.targets != '' + env: + GA_TOKEN: ${{ secrets.GA_TOKEN }} + run: | + API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + SHORT_SHA="${GITHUB_SHA:0:7}" + TARGETS="${{ steps.branches.outputs.targets }}" + + SUCCESS=0 + CONFLICTS=0 + SKIPPED=0 + FAILED=0 + + for BRANCH in $TARGETS; do + echo "" + echo "═══ main → ${BRANCH} ═══" + + # Check if branch is already up to date + ENCODED_BRANCH=$(echo "$BRANCH" | sed 's|/|%2F|g') + RESPONSE=$(curl -sS \ + -H "Authorization: token ${GA_TOKEN}" \ + "${API}/compare/${ENCODED_BRANCH}...main") + + AHEAD=$(echo "$RESPONSE" | jq '.total_commits // 0') + + if [ "$AHEAD" -eq 0 ]; then + echo " ✅ Already up to date" + SKIPPED=$((SKIPPED + 1)) + continue + fi + + echo " ℹ️ main is ${AHEAD} commit(s) ahead" + + # Check for existing cascade PR + EXISTING=$(curl -sS \ + -H "Authorization: token ${GA_TOKEN}" \ + "${API}/pulls?state=open&head=${GITEA_ORG}:main&base=${ENCODED_BRANCH}&limit=1") + + EXISTING_COUNT=$(echo "$EXISTING" | jq 'length') + PR_NUMBER="" + + if [ "$EXISTING_COUNT" -gt 0 ]; then + PR_NUMBER=$(echo "$EXISTING" | jq -r '.[0].number') + echo " ℹ️ Reusing existing PR #${PR_NUMBER}" + else + # Create cascade PR + PR_RESPONSE=$(curl -sS -w "\n%{http_code}" \ + -X POST \ + -H "Authorization: token ${GA_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ + \"title\": \"chore: cascade main → ${BRANCH} (${SHORT_SHA}) [skip ci]\", + \"body\": \"## Automatic cascade\\n\\nForward-merging \`main\` (${SHORT_SHA}) into \`${BRANCH}\`.\\n\\nIf conflicts exist, resolve manually and merge.\\n\\n> Auto-created by **Cascade Main → Dev**.\", + \"head\": \"main\", + \"base\": \"${BRANCH}\" + }" \ + "${API}/pulls") + + HTTP_CODE=$(echo "$PR_RESPONSE" | tail -1) + BODY=$(echo "$PR_RESPONSE" | sed '$d') + PR_NUMBER=$(echo "$BODY" | jq -r '.number // empty') + + if [ "$HTTP_CODE" != "201" ] || [ -z "$PR_NUMBER" ]; then + MSG=$(echo "$BODY" | jq -r '.message // .' 2>/dev/null | head -1) + echo " ❌ Failed to create PR (HTTP ${HTTP_CODE}): ${MSG}" + FAILED=$((FAILED + 1)) + continue + fi + + echo " ✅ Created PR #${PR_NUMBER}" + fi + + # Try auto-merge + PR_DATA=$(curl -sS \ + -H "Authorization: token ${GA_TOKEN}" \ + "${API}/pulls/${PR_NUMBER}") + + MERGEABLE=$(echo "$PR_DATA" | jq -r '.mergeable // false') + + if [ "$MERGEABLE" != "true" ]; then + echo " ⚠️ Conflicts — PR #${PR_NUMBER} left open" + CONFLICTS=$((CONFLICTS + 1)) + continue + fi + + MERGE_RESPONSE=$(curl -sS -w "\n%{http_code}" \ + -X POST \ + -H "Authorization: token ${GA_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ + \"Do\": \"merge\", + \"merge_message_field\": \"chore: cascade main → ${BRANCH} [skip ci]\", + \"delete_branch_after_merge\": false + }" \ + "${API}/pulls/${PR_NUMBER}/merge") + + MERGE_HTTP=$(echo "$MERGE_RESPONSE" | tail -1) + + if [ "$MERGE_HTTP" = "200" ] || [ "$MERGE_HTTP" = "204" ]; then + echo " ✅ Merged — ${BRANCH} is in sync" + SUCCESS=$((SUCCESS + 1)) + else + MERGE_BODY=$(echo "$MERGE_RESPONSE" | sed '$d') + echo " ⚠️ Merge failed (HTTP ${MERGE_HTTP}) — PR #${PR_NUMBER} left open" + CONFLICTS=$((CONFLICTS + 1)) + fi + done + + # Summary + echo "" + echo "════════════════════════════════════════" + echo " ✅ Merged: ${SUCCESS}" + echo " ⚠️ Conflicts: ${CONFLICTS}" + echo " ⏭️ Up to date: ${SKIPPED}" + echo " ❌ Failed: ${FAILED}" + echo "════════════════════════════════════════" + + if [ "$FAILED" -gt 0 ]; then + exit 1 + fi -- 2.52.0 From 29fdd5cb95dcee58917c61ff665a37bedfefe0a3 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 18:55:40 +0000 Subject: [PATCH 084/136] chore: sync .mokogitea/workflows/ci-joomla.yml from template [skip ci] --- .mokogitea/workflows/ci-joomla.yml | 450 +++++++++++++++++++++++++++++ 1 file changed, 450 insertions(+) create mode 100644 .mokogitea/workflows/ci-joomla.yml diff --git a/.mokogitea/workflows/ci-joomla.yml b/.mokogitea/workflows/ci-joomla.yml new file mode 100644 index 0000000..5c66f14 --- /dev/null +++ b/.mokogitea/workflows/ci-joomla.yml @@ -0,0 +1,450 @@ +# Copyright (C) 2026 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow.Template +# INGROUP: MokoStandards.CI +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# PATH: /templates/workflows/joomla/ci-joomla.yml.template +# VERSION: 04.06.00 +# BRIEF: CI workflow for Joomla extensions — lint, validate, test + +name: "Joomla: Extension CI" + +on: + pull_request: + branches: + - main + - 'dev/**' + workflow_dispatch: + +permissions: + contents: read + pull-requests: write + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + lint-and-validate: + name: Lint & Validate + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP + run: | + php -v && composer --version + + - name: Clone MokoStandards + env: + GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} + MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} + MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }} + run: | + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \ + /tmp/mokostandards-api + + - name: Install dependencies + env: + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}' + run: | + if [ -f "composer.json" ]; then + composer install \ + --no-interaction \ + --prefer-dist \ + --optimize-autoloader + else + echo "No composer.json found — skipping dependency install" + fi + + - name: PHP syntax check + run: | + ERRORS=0 + for DIR in src/ htdocs/; do + if [ -d "$DIR" ]; then + FOUND=1 + while IFS= read -r -d '' FILE; do + OUTPUT=$(php -l "$FILE" 2>&1) + if echo "$OUTPUT" | grep -q "Parse error"; then + echo "::error file=${FILE}::${OUTPUT}" + ERRORS=$((ERRORS + 1)) + fi + done < <(find "$DIR" -name "*.php" -print0) + fi + done + echo "### PHP Syntax Check" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} syntax error(s) found.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY + fi + + - name: XML manifest validation + run: | + echo "### XML Manifest Validation" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + # Find the extension manifest (XML with /dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No Joomla extension manifest found (XML file with \`> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Manifest found: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY + + # Validate well-formed XML + php -r " + \$xml = @simplexml_load_file('$MANIFEST'); + if (\$xml === false) { + echo 'INVALID'; + exit(1); + } + echo 'VALID'; + " > /tmp/xml_result 2>&1 + XML_RESULT=$(cat /tmp/xml_result) + if [ "$XML_RESULT" != "VALID" ]; then + echo "Manifest is not well-formed XML." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Manifest is well-formed XML." >> $GITHUB_STEP_SUMMARY + fi + + # Check required tags: name, version, author, namespace (Joomla 5+) + for TAG in name version author namespace; do + if ! grep -q "<${TAG}>" "$MANIFEST" 2>/dev/null; then + echo "Missing required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Found required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY + fi + done + fi + + if [ "${ERRORS}" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**${ERRORS} manifest issue(s) found.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Manifest validation passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Check language files referenced in manifest + run: | + echo "### Language File Check" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -n "$MANIFEST" ]; then + # Extract language file references from manifest + LANG_FILES=$(grep -oP 'language\s+tag="[^"]*"[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true) + if [ -z "$LANG_FILES" ]; then + echo "No language file references found in manifest — skipping." >> $GITHUB_STEP_SUMMARY + else + while IFS= read -r LANG_FILE; do + LANG_FILE=$(echo "$LANG_FILE" | xargs) + if [ -z "$LANG_FILE" ]; then + continue + fi + # Check in common locations + FOUND=0 + for BASE in "." "src" "htdocs"; do + if [ -f "${BASE}/${LANG_FILE}" ]; then + FOUND=1 + break + fi + done + if [ "$FOUND" -eq 0 ]; then + echo "Missing language file: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Language file present: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY + fi + done <<< "$LANG_FILES" + fi + else + echo "No manifest found — skipping language check." >> $GITHUB_STEP_SUMMARY + fi + + if [ "${ERRORS}" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**${ERRORS} missing language file(s).**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Language file check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Check index.html files in directories + run: | + echo "### Index.html Check" >> $GITHUB_STEP_SUMMARY + MISSING=0 + CHECKED=0 + + for DIR in src/ htdocs/; do + if [ -d "$DIR" ]; then + while IFS= read -r -d '' SUBDIR; do + CHECKED=$((CHECKED + 1)) + if [ ! -f "${SUBDIR}/index.html" ]; then + echo "Missing index.html in: \`${SUBDIR}\`" >> $GITHUB_STEP_SUMMARY + MISSING=$((MISSING + 1)) + fi + done < <(find "$DIR" -type d -print0) + fi + done + + if [ "${CHECKED}" -eq 0 ]; then + echo "No src/ or htdocs/ directories found — skipping." >> $GITHUB_STEP_SUMMARY + elif [ "${MISSING}" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**${MISSING} director(ies) missing index.html out of ${CHECKED} checked.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "All ${CHECKED} directories contain index.html." >> $GITHUB_STEP_SUMMARY + fi + + release-readiness: + name: Release Readiness Check + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' && github.base_ref == 'main' + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Validate release readiness + run: | + echo "## Release Readiness" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + # Extract version from README.md + README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md | head -1) + if [ -z "$README_VERSION" ]; then + echo "No VERSION found in README.md FILE INFORMATION block." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "README version: \`${README_VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + + # Find the extension manifest + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No Joomla extension manifest found." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY + + # Check matches README VERSION + MANIFEST_VERSION=$(grep -oP '\K[^<]+' "$MANIFEST" | head -1) + if [ -z "$MANIFEST_VERSION" ]; then + echo "No \`\` tag in manifest." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + elif [ -n "$README_VERSION" ] && [ "$MANIFEST_VERSION" != "$README_VERSION" ]; then + echo "Manifest version \`${MANIFEST_VERSION}\` does not match README \`${README_VERSION}\`." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Manifest version: \`${MANIFEST_VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + + # Check extension type, element, client attributes + EXT_TYPE=$(grep -oP ']*\btype="\K[^"]+' "$MANIFEST" | head -1) + if [ -z "$EXT_TYPE" ]; then + echo "Missing \`type\` attribute on \`\` tag." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Extension type: \`${EXT_TYPE}\`" >> $GITHUB_STEP_SUMMARY + fi + + # Element check (component/module/plugin name) + HAS_ELEMENT=$(grep -cP '<(element|name)>' "$MANIFEST" 2>/dev/null || echo "0") + if [ "$HAS_ELEMENT" -eq 0 ]; then + echo "Missing \`\` or \`\` in manifest." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + + # Client attribute for site/admin modules and plugins + if echo "$EXT_TYPE" | grep -qP "^(module|plugin)$"; then + HAS_CLIENT=$(grep -cP ']*\bclient=' "$MANIFEST" 2>/dev/null || echo "0") + if [ "$HAS_CLIENT" -eq 0 ]; then + echo "Missing \`client\` attribute for ${EXT_TYPE} extension." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + fi + fi + + # Check updates.xml exists + if [ -f "updates.xml" ] || [ -f "updates.xml" ]; then + echo "Update XML present." >> $GITHUB_STEP_SUMMARY + else + echo "No updates.xml found." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + + # Check CHANGELOG.md exists + if [ -f "CHANGELOG.md" ]; then + echo "CHANGELOG.md present." >> $GITHUB_STEP_SUMMARY + else + echo "No CHANGELOG.md found." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ $ERRORS -gt 0 ]; then + echo "**${ERRORS} issue(s) must be resolved before release.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Extension is ready for release.**" >> $GITHUB_STEP_SUMMARY + fi + + test: + name: Tests (PHP ${{ matrix.php }}) + runs-on: ubuntu-latest + needs: lint-and-validate + + strategy: + fail-fast: false + matrix: + php: ['8.2', '8.3'] + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP ${{ matrix.php }} + run: | + php -v && composer --version + + - name: Install dependencies + env: + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}' + run: | + if [ -f "composer.json" ]; then + composer install \ + --no-interaction \ + --prefer-dist \ + --optimize-autoloader + else + echo "No composer.json found — skipping dependency install" + fi + + - name: Run tests + run: | + echo "### Test Results (PHP ${{ matrix.php }})" >> $GITHUB_STEP_SUMMARY + if [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then + vendor/bin/phpunit --testdox 2>&1 | tee /tmp/test-output.log + EXIT=${PIPESTATUS[0]} + if [ $EXIT -eq 0 ]; then + echo "All tests passed." >> $GITHUB_STEP_SUMMARY + else + echo "Test failures detected — see log." >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + cat /tmp/test-output.log >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + fi + exit $EXIT + else + echo "No phpunit.xml found — skipping tests." >> $GITHUB_STEP_SUMMARY + fi + + static-analysis: + name: PHPStan Analysis + runs-on: ubuntu-latest + needs: lint-and-validate + continue-on-error: true + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP + run: php -v && composer --version + + - name: Install dependencies + env: + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}' + run: | + if [ -f "composer.json" ]; then + composer install --no-interaction --prefer-dist --optimize-autoloader + fi + + - name: Install PHPStan + run: | + if ! command -v vendor/bin/phpstan &> /dev/null; then + composer require --dev phpstan/phpstan --no-interaction 2>/dev/null || \ + composer global require phpstan/phpstan --no-interaction + fi + + - name: Run PHPStan + run: | + echo "### PHPStan Static Analysis" >> $GITHUB_STEP_SUMMARY + PHPSTAN="vendor/bin/phpstan" + if [ ! -f "$PHPSTAN" ]; then + PHPSTAN=$(composer global config bin-dir --absolute 2>/dev/null)/phpstan + fi + + # Determine source directory + SRC_DIR="" + for DIR in src/ htdocs/ lib/; do + if [ -d "$DIR" ]; then + SRC_DIR="$DIR" + break + fi + done + + if [ -z "$SRC_DIR" ]; then + echo "No source directory found (src/, htdocs/, lib/) — skipping." >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + # Use repo phpstan.neon if present, otherwise use baseline config + ARGS="analyse ${SRC_DIR} --memory-limit=512M --no-progress --error-format=table" + if [ -f "phpstan.neon" ] || [ -f "phpstan.neon.dist" ]; then + echo "Using project PHPStan config." >> $GITHUB_STEP_SUMMARY + else + ARGS="$ARGS --level=3" + echo "No phpstan.neon found — using level 3 (type inference)." >> $GITHUB_STEP_SUMMARY + fi + + $PHPSTAN $ARGS 2>&1 | tee /tmp/phpstan-output.txt + EXIT=${PIPESTATUS[0]} + + if [ $EXIT -eq 0 ]; then + echo "**No errors found.**" >> $GITHUB_STEP_SUMMARY + else + ERRORS=$(grep -c "ERROR" /tmp/phpstan-output.txt 2>/dev/null || echo "some") + echo "**${ERRORS} error(s) found.** Review output above." >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + tail -30 /tmp/phpstan-output.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + fi + exit $EXIT -- 2.52.0 From 87fedac5310b03b96d88fcb98fdb5afe2530c85c Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 18:55:41 +0000 Subject: [PATCH 085/136] chore: sync .mokogitea/workflows/cleanup.yml from template [skip ci] --- .mokogitea/workflows/cleanup.yml | 87 ++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 .mokogitea/workflows/cleanup.yml diff --git a/.mokogitea/workflows/cleanup.yml b/.mokogitea/workflows/cleanup.yml new file mode 100644 index 0000000..3a81856 --- /dev/null +++ b/.mokogitea/workflows/cleanup.yml @@ -0,0 +1,87 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Maintenance +# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards +# PATH: /.gitea/workflows/cleanup.yml +# VERSION: 01.00.00 +# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs + +name: "Universal: Repository Cleanup" + +on: + schedule: + - cron: '0 3 * * 0' # Weekly on Sunday at 03:00 UTC + workflow_dispatch: + +permissions: + contents: write + +env: + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + +jobs: + cleanup: + name: Clean Merged Branches + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GA_TOKEN }} + + - name: Delete merged branches + env: + GA_TOKEN: ${{ secrets.GA_TOKEN }} + run: | + echo "=== Merged Branch Cleanup ===" + API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" + + # List branches via API + BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \ + "${API}/branches?limit=50" | jq -r '.[].name') + + DELETED=0 + for BRANCH in $BRANCHES; do + # Skip protected branches + case "$BRANCH" in + main|master|develop|release/*|hotfix/*) continue ;; + esac + + # 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}" \ + "${API}/branches/${BRANCH}" 2>/dev/null || true + DELETED=$((DELETED + 1)) + fi + done + + echo "Deleted ${DELETED} merged branch(es)" + + - name: Clean old workflow runs + env: + GA_TOKEN: ${{ secrets.GA_TOKEN }} + run: | + echo "=== Workflow Run Cleanup ===" + API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" + CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ) + + # Get old completed runs + RUNS=$(curl -sS -H "Authorization: token ${GA_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}" \ + "${API}/actions/runs/${RUN_ID}" 2>/dev/null || true + DELETED=$((DELETED + 1)) + done + + echo "Deleted ${DELETED} old workflow run(s)" -- 2.52.0 From 587b61a520744090785063c54d81fb40fe4ea6d8 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 18:55:41 +0000 Subject: [PATCH 086/136] chore: sync .mokogitea/workflows/deploy-manual.yml from template [skip ci] --- .mokogitea/workflows/deploy-manual.yml | 126 +++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 .mokogitea/workflows/deploy-manual.yml diff --git a/.mokogitea/workflows/deploy-manual.yml b/.mokogitea/workflows/deploy-manual.yml new file mode 100644 index 0000000..bb133ed --- /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: + GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} + MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} + MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }} + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_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 -- 2.52.0 From 8f636448dac3517dbc583f42fde94c92d8515613 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 18:55:41 +0000 Subject: [PATCH 087/136] chore: sync .mokogitea/workflows/gitleaks.yml from template [skip ci] --- .mokogitea/workflows/gitleaks.yml | 96 +++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 .mokogitea/workflows/gitleaks.yml diff --git a/.mokogitea/workflows/gitleaks.yml b/.mokogitea/workflows/gitleaks.yml new file mode 100644 index 0000000..0c07612 --- /dev/null +++ b/.mokogitea/workflows/gitleaks.yml @@ -0,0 +1,96 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Security +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# PATH: /templates/workflows/gitleaks.yml.template +# VERSION: 01.00.00 +# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens +# +# +========================================================================+ +# | SECRET SCANNING | +# +========================================================================+ +# | | +# | Scans commits for leaked secrets using Gitleaks. | +# | | +# | - PR scan: only new commits in the PR | +# | - Scheduled: full repo scan weekly | +# | - Alerts via ntfy on findings | +# | | +# +========================================================================+ + +name: "Universal: Secret Scanning" + +on: + pull_request: + branches: + - main + - 'dev/**' + schedule: + - cron: '0 5 * * 1' # Weekly Monday 05:00 UTC + workflow_dispatch: + +permissions: + contents: read + +env: + NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }} + NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }} + +jobs: + gitleaks: + name: Gitleaks 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 + gitleaks version + + - name: Scan for secrets + id: scan + run: | + echo "### Secret Scanning" >> $GITHUB_STEP_SUMMARY + ARGS="--source . --verbose --report-format json --report-path /tmp/gitleaks-report.json" + + if [ "${{ github.event_name }}" = "pull_request" ]; then + # Scan only PR commits + ARGS="$ARGS --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}" + echo "Scanning PR commits only" >> $GITHUB_STEP_SUMMARY + else + echo "Full repository scan" >> $GITHUB_STEP_SUMMARY + fi + + if gitleaks detect $ARGS 2>&1; then + echo "result=clean" >> "$GITHUB_OUTPUT" + echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY + else + echo "result=found" >> "$GITHUB_OUTPUT" + FINDINGS=$(jq length /tmp/gitleaks-report.json 2>/dev/null || echo "unknown") + echo "**${FINDINGS} potential secret(s) detected.**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Review the findings and rotate any exposed credentials immediately." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + - name: Notify on findings + if: failure() && steps.scan.outputs.result == 'found' + run: | + REPO="${{ github.event.repository.name }}" + curl -sS \ + -H "Title: ${REPO} — secrets detected in code" \ + -H "Tags: rotating_light,key" \ + -H "Priority: urgent" \ + -d "Gitleaks found potential secrets. Review and rotate credentials immediately." \ + "${NTFY_URL}/${NTFY_TOPIC}" || true -- 2.52.0 From f546e7fa2fadd1688cc1bbccb82e963650a39480 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 18:55:42 +0000 Subject: [PATCH 088/136] chore: sync .mokogitea/workflows/notify.yml from template [skip ci] --- .mokogitea/workflows/notify.yml | 71 +++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 .mokogitea/workflows/notify.yml diff --git a/.mokogitea/workflows/notify.yml b/.mokogitea/workflows/notify.yml new file mode 100644 index 0000000..463a900 --- /dev/null +++ b/.mokogitea/workflows/notify.yml @@ -0,0 +1,71 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Notifications +# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards +# PATH: /.gitea/workflows/notify.yml +# VERSION: 01.00.00 +# BRIEF: Push notifications via ntfy on release success or workflow failure + +name: "Universal: Notifications" + +on: + workflow_run: + workflows: + - "Joomla Build & Release" + - "Joomla Extension CI" + - "Deploy" + - "Cascade Main → Dev" + types: + - completed + +permissions: + contents: read + +env: + NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }} + NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-releases' }} + +jobs: + notify: + name: Send Notification + runs-on: ubuntu-latest + if: >- + github.event.workflow_run.conclusion == 'success' || + github.event.workflow_run.conclusion == 'failure' + + steps: + - name: Notify on success (releases only) + if: >- + github.event.workflow_run.conclusion == 'success' && + contains(github.event.workflow_run.name, 'Release') + run: | + REPO="${{ github.event.repository.name }}" + WORKFLOW="${{ github.event.workflow_run.name }}" + URL="${{ github.event.workflow_run.html_url }}" + + curl -sS \ + -H "Title: ${REPO} released" \ + -H "Tags: white_check_mark,package" \ + -H "Priority: default" \ + -H "Click: ${URL}" \ + -d "${WORKFLOW} completed successfully." \ + "${NTFY_URL}/${NTFY_TOPIC}" + + - name: Notify on failure + if: github.event.workflow_run.conclusion == 'failure' + run: | + REPO="${{ github.event.repository.name }}" + WORKFLOW="${{ github.event.workflow_run.name }}" + URL="${{ github.event.workflow_run.html_url }}" + + curl -sS \ + -H "Title: ${REPO} workflow failed" \ + -H "Tags: x,warning" \ + -H "Priority: high" \ + -H "Click: ${URL}" \ + -d "${WORKFLOW} failed. Check the run for details." \ + "${NTFY_URL}/${NTFY_TOPIC}" -- 2.52.0 From 76b8119f28866d53745131e8437e45b5f8d5ffb3 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 18:55:42 +0000 Subject: [PATCH 089/136] chore: sync .mokogitea/workflows/pr-check.yml from template [skip ci] --- .mokogitea/workflows/pr-check.yml | 196 ++++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 .mokogitea/workflows/pr-check.yml diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml new file mode 100644 index 0000000..bd06c90 --- /dev/null +++ b/.mokogitea/workflows/pr-check.yml @@ -0,0 +1,196 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.CI +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# PATH: /templates/workflows/universal/pr-check.yml.template +# VERSION: 05.00.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 + ;; + hotfix/*) + if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'" + fi + ;; + alpha/*|beta/*) + if [ "$BASE" != "dev" ]; then + ALLOWED=false + REASON="Pre-release branches must target 'dev', not '${BASE}'" + fi + ;; + rc/*) + if [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Release candidate branches must target '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 + + # ── Code Validation ──────────────────────────────────────────────────── + validate: + name: Validate PR + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Detect platform + id: platform + run: | + # Read platform from XML manifest ( tag) or plain text fallback + PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/.moko-platform 2>/dev/null | head -1) + [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/.moko-platform 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: 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 + 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: 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; } -- 2.52.0 From 108ff0b8abd1864bd22eef2a8f6c325221ab109c Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 18:55:42 +0000 Subject: [PATCH 090/136] chore: sync .mokogitea/workflows/pre-release.yml from template [skip ci] --- .mokogitea/workflows/pre-release.yml | 384 +++++++++++++++++++++++++++ 1 file changed, 384 insertions(+) create mode 100644 .mokogitea/workflows/pre-release.yml diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml new file mode 100644 index 0000000..6e05d96 --- /dev/null +++ b/.mokogitea/workflows/pre-release.yml @@ -0,0 +1,384 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Release +# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards +# PATH: /templates/workflows/universal/pre-release.yml.template +# VERSION: 05.00.00 +# BRIEF: Manual pre-release — builds dev/alpha/beta/rc packages from any branch + +name: "Universal: Pre-Release" + +on: + workflow_dispatch: + inputs: + stability: + description: 'Pre-release channel' + required: true + type: choice + options: + - development + - alpha + - beta + - release-candidate + +permissions: + contents: write + +env: + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +jobs: + build: + name: "Build Pre-Release (${{ inputs.stability }})" + runs-on: release + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GA_TOKEN }} + + - name: Setup PHP + run: | + if ! command -v php &> /dev/null; then + sudo apt-get update -qq + sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip >/dev/null 2>&1 + fi + + - name: Detect platform + id: platform + run: | + # Read platform from XML manifest ( tag) or plain text fallback + PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/.moko-platform 2>/dev/null | head -1) + [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/.moko-platform 2>/dev/null | tr -d '[:space:]') + [ -z "$PLATFORM" ] && PLATFORM="generic" + echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) + MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1) + echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT" + echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT" + + - name: Resolve metadata + id: meta + run: | + STABILITY="${{ inputs.stability }}" + + case "$STABILITY" in + development) SUFFIX="-dev"; TAG="development" ;; + alpha) SUFFIX="-alpha"; TAG="alpha" ;; + beta) SUFFIX="-beta"; TAG="beta" ;; + release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; + esac + + # Read and bump patch version (with rollover) + CURRENT=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1) + [ -z "$CURRENT" ] && CURRENT="00.00.00" + + MAJOR=$(echo "$CURRENT" | cut -d. -f1) + MINOR=$(echo "$CURRENT" | cut -d. -f2) + PATCH=$(echo "$CURRENT" | cut -d. -f3) + + # Patch bump with rollover: ZZ=99 → bump minor, YY=99 → bump major + NEW_PATCH=$((10#$PATCH + 1)) + NEW_MINOR=$((10#$MINOR)) + NEW_MAJOR=$((10#$MAJOR)) + + if [ $NEW_PATCH -gt 99 ]; then + NEW_PATCH=0 + NEW_MINOR=$((NEW_MINOR + 1)) + fi + if [ $NEW_MINOR -gt 99 ]; then + NEW_MINOR=0 + NEW_MAJOR=$((NEW_MAJOR + 1)) + fi + + VERSION=$(printf "%02d.%02d.%02d" $NEW_MAJOR $NEW_MINOR $NEW_PATCH) + TODAY=$(date +%Y-%m-%d) + + echo "Bumping: ${CURRENT} → ${VERSION} (patch)" + + # Update README.md + sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${VERSION}/" README.md + + # Update platform-specific manifest + PLATFORM="${{ steps.platform.outputs.platform }}" + MANIFEST="${{ steps.platform.outputs.manifest }}" + MOD_FILE="${{ steps.platform.outputs.mod_file }}" + case "$PLATFORM" in + joomla) + if [ -n "$MANIFEST" ]; then + MANIFEST_VER=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1) + sed -i "s|${MANIFEST_VER}|${VERSION}|" "$MANIFEST" + sed -i "s|[^<]*|${TODAY}|" "$MANIFEST" + fi + ;; + dolibarr) + if [ -n "$MOD_FILE" ]; then + sed -i "s/\$this->version = '[^']*'/\$this->version = '${VERSION}'/" "$MOD_FILE" + fi + ;; + *) ;; + esac + + # Commit version bump + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git add -A + git diff --cached --quiet || { + git commit -m "chore(version): bump ${CURRENT} → ${VERSION} [skip ci]" + git push origin HEAD 2>&1 + } + + # Auto-detect element (platform-aware) + case "$PLATFORM" in + joomla) + MANIFEST="${{ steps.platform.outputs.manifest }}" + EXT_ELEMENT="" + if [ -n "$MANIFEST" ]; then + EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1) + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') + case "$EXT_ELEMENT" in + templatedetails|manifest) EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;; + esac + fi + else + EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + fi + ;; + dolibarr) + MOD_FILE="${{ steps.platform.outputs.mod_file }}" + if [ -n "$MOD_FILE" ]; then + MOD_BASENAME=$(basename "$MOD_FILE" .class.php) + EXT_ELEMENT=$(echo "$MOD_BASENAME" | sed 's/^mod//' | tr '[:upper:]' '[:lower:]') + else + EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + fi + ;; + *) + EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + ;; + esac + + ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip" + + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" + echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" + echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" + echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT" + + echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ===" + + - name: Build package + run: | + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + if [ ! -d "$SOURCE_DIR" ]; then + echo "::error::No src/ or htdocs/ directory" + exit 1 + fi + + mkdir -p build/package + rsync -a \ + --exclude='sftp-config*' \ + --exclude='.ftpignore' \ + --exclude='*.ppk' \ + --exclude='*.pem' \ + --exclude='*.key' \ + --exclude='.env*' \ + --exclude='*.local' \ + --exclude='.build-trigger' \ + "${SOURCE_DIR}/" build/package/ + + - name: Create ZIP + id: zip + run: | + ZIP_NAME="${{ steps.meta.outputs.zip_name }}" + cd build/package + zip -r "../${ZIP_NAME}" . + cd .. + + SHA256=$(sha256sum "${ZIP_NAME}" | cut -d' ' -f1) + echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT" + echo "ZIP: ${ZIP_NAME} (SHA: ${SHA256:0:16}...)" + + - name: Create or replace Gitea release + id: release + run: | + TAG="${{ steps.meta.outputs.tag }}" + VERSION="${{ steps.meta.outputs.version }}" + STABILITY="${{ steps.meta.outputs.stability }}" + SHA256="${{ steps.zip.outputs.sha256 }}" + ZIP_NAME="${{ steps.meta.outputs.zip_name }}" + EXT_ELEMENT="${{ steps.meta.outputs.ext_element }}" + TOKEN="${{ secrets.GA_TOKEN }}" + API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + BRANCH=$(git branch --show-current) + + BODY="## ${VERSION} ($(date +%Y-%m-%d)) + **Channel:** ${STABILITY} + **SHA-256:** \`${SHA256}\`" + + # Delete existing release + EXISTING_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \ + "${API}/releases/tags/${TAG}" | jq -r '.id // empty' 2>/dev/null) + if [ -n "$EXISTING_ID" ]; then + curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API}/releases/${EXISTING_ID}" 2>/dev/null || true + curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API}/tags/${TAG}" 2>/dev/null || true + fi + + # Create release + RELEASE_ID=$(curl -sS -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/releases" \ + -d "$(jq -n \ + --arg tag "$TAG" \ + --arg target "$BRANCH" \ + --arg name "${EXT_ELEMENT} ${VERSION} (${STABILITY})" \ + --arg body "$BODY" \ + '{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: true}' + )" | jq -r '.id') + + echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT" + + # Upload ZIP + curl -sS -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/octet-stream" \ + "${API}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \ + --data-binary "@build/${ZIP_NAME}" + + echo "Released: ${EXT_ELEMENT} ${VERSION} (${STABILITY})" + + - name: Update updates.xml + if: steps.platform.outputs.platform == 'joomla' + run: | + STABILITY="${{ steps.meta.outputs.stability }}" + VERSION="${{ steps.meta.outputs.version }}" + SHA256="${{ steps.zip.outputs.sha256 }}" + ZIP_NAME="${{ steps.meta.outputs.zip_name }}" + TAG="${{ steps.meta.outputs.tag }}" + DATE=$(date +%Y-%m-%d) + + if [ ! -f "updates.xml" ]; then + echo "No updates.xml — skipping" + exit 0 + fi + + export PY_STABILITY="$STABILITY" PY_VERSION="$VERSION" PY_SHA256="$SHA256" \ + PY_ZIP_NAME="$ZIP_NAME" PY_TAG="$TAG" PY_DATE="$DATE" \ + PY_GITEA_ORG="$GITEA_ORG" PY_GITEA_REPO="$GITEA_REPO" + python3 << 'PYEOF' + import re, os + + stability = os.environ["PY_STABILITY"] + version = os.environ["PY_VERSION"] + sha256 = os.environ["PY_SHA256"] + zip_name = os.environ["PY_ZIP_NAME"] + tag = os.environ["PY_TAG"] + date = os.environ["PY_DATE"] + gitea_org = os.environ["PY_GITEA_ORG"] + gitea_repo = os.environ["PY_GITEA_REPO"] + download_url = f"https://git.mokoconsulting.tech/{gitea_org}/{gitea_repo}/releases/download/{tag}/{zip_name}" + + with open("updates.xml", "r") as f: + content = f.read() + + # Map stability to XML tag name + tag_map = {"development": "development", "alpha": "alpha", "beta": "beta", "release-candidate": "rc"} + xml_tag = tag_map.get(stability, stability) + + pattern = r"((?:(?!).)*?" + re.escape(xml_tag) + r".*?)" + match = re.search(pattern, content, re.DOTALL) + if match: + block = match.group(1) + updated = re.sub(r"[^<]*", f"{version}", block) + updated = re.sub(r"[^<]*", f"{date}", updated) + if "" in updated: + updated = re.sub(r"[^<]*", f"{sha256}", updated) + else: + updated = updated.replace("", f"\n {sha256}") + updated = re.sub(r"(]*>)[^<]*()", rf"\g<1>{download_url}\g<2>", updated) + content = content.replace(block, updated) + print(f"Updated {xml_tag} channel: version={version}") + else: + print(f"WARNING: No {xml_tag} block in updates.xml") + + with open("updates.xml", "w") as f: + f.write(content) + PYEOF + + # Commit and push to current branch + if ! git diff --quiet updates.xml 2>/dev/null; then + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git add updates.xml + git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]" + git push origin HEAD 2>&1 || echo "WARNING: push failed" + fi + + - name: "Sync updates.xml to all branches" + if: steps.platform.outputs.platform == 'joomla' + run: | + CURRENT_BRANCH="${{ github.ref_name }}" + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + + # Sync updates.xml to main and dev (whichever isn't current) + for BRANCH in main dev; do + [ "$BRANCH" = "$CURRENT_BRANCH" ] && continue + + echo "Syncing updates.xml → ${BRANCH}" + git fetch origin "${BRANCH}" 2>/dev/null || continue + git checkout "origin/${BRANCH}" -- . 2>/dev/null || continue + git checkout "${CURRENT_BRANCH}" -- updates.xml + if ! git diff --quiet updates.xml 2>/dev/null; then + git add updates.xml + git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]" + git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed" + fi + git checkout "${CURRENT_BRANCH}" 2>/dev/null + done + + - name: "Delete lesser pre-release channels (cascade)" + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.GA_TOKEN }}" + STABILITY="${{ steps.meta.outputs.stability }}" + + # Cascade: rc → beta,alpha,dev | beta → alpha,dev | alpha → dev | dev → nothing + case "$STABILITY" in + release-candidate) TAGS_TO_DELETE="beta alpha development" ;; + beta) TAGS_TO_DELETE="alpha development" ;; + alpha) TAGS_TO_DELETE="development" ;; + *) TAGS_TO_DELETE="" ;; + esac + + [ -z "$TAGS_TO_DELETE" ] && exit 0 + + for TAG in $TAGS_TO_DELETE; do + RELEASE_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/releases/tags/${TAG}" 2>/dev/null | \ + python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + + if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then + curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/releases/${RELEASE_ID}" 2>/dev/null || true + curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/tags/${TAG}" 2>/dev/null || true + echo "Deleted: ${TAG} (id: ${RELEASE_ID})" + fi + done -- 2.52.0 From 876276366a10d7f0e912da9349a359e1e58ea65f Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 18:55:43 +0000 Subject: [PATCH 091/136] chore: sync .mokogitea/workflows/repo-health.yml from template [skip ci] --- .mokogitea/workflows/repo-health.yml | 766 +++++++++++++++++++++++++++ 1 file changed, 766 insertions(+) create mode 100644 .mokogitea/workflows/repo-health.yml diff --git a/.mokogitea/workflows/repo-health.yml b/.mokogitea/workflows/repo-health.yml new file mode 100644 index 0000000..e5e1c73 --- /dev/null +++ b/.mokogitea/workflows/repo-health.yml @@ -0,0 +1,766 @@ +# ============================================================================ +# 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: MokoStandards.Validation +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# PATH: /templates/workflows/joomla/repo_health.yml.template +# VERSION: 04.06.00 +# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts. +# ============================================================================ + +name: "Joomla: Repo Health" + +concurrency: + group: repo-health-${{ github.repository }}-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +on: + workflow_dispatch: + inputs: + profile: + description: 'Validation profile: all, release, scripts, or repo' + required: true + default: all + type: choice + options: + - all + - release + - scripts + - repo + pull_request: + push: + +permissions: + contents: read + +env: + # Release policy - Repository Variables Only + RELEASE_REQUIRED_REPO_VARS: RS_FTP_PATH_SUFFIX + RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX + + # 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,.gitea/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: .gitea/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.GA_TOKEN || secrets.GA_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 + + release_config: + name: Release configuration + 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: Guardrails release vars + env: + PROFILE_RAW: ${{ github.event.inputs.profile }} + RS_FTP_PATH_SUFFIX: ${{ vars.RS_FTP_PATH_SUFFIX }} + DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }} + run: | + set -euo pipefail + + profile="${PROFILE_RAW:-all}" + case "${profile}" in + all|release|scripts|repo) ;; + *) + printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" + exit 1 + ;; + esac + + if [ "${profile}" = 'scripts' ] || [ "${profile}" = 'repo' ]; then + { + printf '%s\n' '### Release configuration (Repository Variables)' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' 'Status: SKIPPED' + printf '%s\n' 'Reason: profile excludes release validation' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 0 + fi + + IFS=',' read -r -a required <<< "${RELEASE_REQUIRED_REPO_VARS}" + IFS=',' read -r -a optional <<< "${RELEASE_OPTIONAL_REPO_VARS}" + + missing=() + missing_optional=() + + for k in "${required[@]}"; do + v="${!k:-}" + [ -z "${v}" ] && missing+=("${k}") + done + + for k in "${optional[@]}"; do + v="${!k:-}" + [ -z "${v}" ] && missing_optional+=("${k}") + done + + { + printf '%s\n' '### Release configuration (Repository Variables)' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' '| Variable | Status |' + printf '%s\n' '|---|---|' + printf '%s\n' "| RS_FTP_PATH_SUFFIX | ${RS_FTP_PATH_SUFFIX:-NOT SET} |" + printf '%s\n' "| DEV_FTP_SUFFIX | ${DEV_FTP_SUFFIX:-NOT SET} |" + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + if [ "${#missing_optional[@]}" -gt 0 ]; then + { + printf '%s\n' '### Missing optional repository variables' + for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + if [ "${#missing[@]}" -gt 0 ]; then + { + printf '%s\n' '### Missing required repository variables' + for m in "${missing[@]}"; do printf '%s\n' "- ${m}"; done + printf '%s\n' 'ERROR: Guardrails failed. Missing required repository variables.' + } >> "${GITHUB_STEP_SUMMARY}" + exit 1 + fi + + { + printf '%s\n' '### Repository variables validation result' + printf '%s\n' 'Status: OK' + printf '%s\n' 'All required repository variables present.' + printf '%s\n' '' + printf '%s\n' '**Note**: Organization secrets (RS_FTP_HOST, RS_FTP_USER, etc.) are validated at deployment time, not in repository health checks.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + 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|release|scripts|repo) ;; + *) + printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" + exit 1 + ;; + esac + + if [ "${profile}" = 'release' ] || [ "${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 + + IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}" + 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|release|scripts|repo) ;; + *) + printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" + exit 1 + ;; + esac + + if [ "${profile}" = 'release' ] || [ "${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 + + # Source directory: src/ or htdocs/ (either is valid) + if [ -d "src" ]; then + SOURCE_DIR="src" + elif [ -d "htdocs" ]; then + SOURCE_DIR="htdocs" + else + missing_required+=("src/ or htdocs/ (source directory required)") + fi + + IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}" + IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}" + IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}" + IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES}" + + missing_required=() + missing_optional=() + + 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 ]; then + missing_required+=("dev/* branch (e.g. dev/01.00.00)") + fi + + if [ "${#dev_branches[@]}" -gt 0 ]; then + missing_required+=("invalid branch dev (must be dev/)") + 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="$(python3 - <<'PY' + import json + import os + + profile = os.environ.get('PROFILE_RAW') or 'all' + + missing_required = os.environ.get('MISSING_REQUIRED', '').splitlines() if os.environ.get('MISSING_REQUIRED') else [] + missing_optional = os.environ.get('MISSING_OPTIONAL', '').splitlines() if os.environ.get('MISSING_OPTIONAL') else [] + content_warnings = os.environ.get('CONTENT_WARNINGS', '').splitlines() if os.environ.get('CONTENT_WARNINGS') else [] + + out = { + 'profile': profile, + 'missing_required': [x for x in missing_required if x], + 'missing_optional': [x for x in missing_optional if x], + 'content_warnings': [x for x in content_warnings if x], + } + + print(json.dumps(out, indent=2)) + PY + )" + + { + 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 + + 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 + + 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="$(python3 - <<'PY' + import os + import re + + idx = os.environ.get('DOCS_INDEX', 'docs/docs-index.md') + base = os.getcwd() + + bad = [] + pat = re.compile(r'\[[^\]]+\]\(([^)]+)\)') + + with open(idx, 'r', encoding='utf-8') as f: + for line in f: + for m in pat.findall(line): + link = m.strip() + if link.startswith('http://') or link.startswith('https://') or link.startswith('#') or link.startswith('mailto:'): + continue + if link.startswith('/'): + rel = link.lstrip('/') + else: + rel = os.path.normpath(os.path.join(os.path.dirname(idx), link)) + rel = rel.split('#', 1)[0] + rel = rel.split('?', 1)[0] + if not rel: + continue + p = os.path.join(base, rel) + if not os.path.exists(p): + bad.append(rel) + + print('\n'.join(sorted(set(bad)))) + PY + )" + 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:' + while IFS= read -r l; do [ -n "${l}" ] && printf '%s\n' "- ${l}"; done <<< "${missing_links}" + 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 variables | OK | Repository variables validation |' + 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}" -- 2.52.0 From 56c9492c0ec10fbc2a4ce463be21e0acdcba0a11 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 18:55:43 +0000 Subject: [PATCH 092/136] chore: sync .mokogitea/workflows/security-audit.yml from template [skip ci] --- .mokogitea/workflows/security-audit.yml | 82 +++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 .mokogitea/workflows/security-audit.yml diff --git a/.mokogitea/workflows/security-audit.yml b/.mokogitea/workflows/security-audit.yml new file mode 100644 index 0000000..789325a --- /dev/null +++ b/.mokogitea/workflows/security-audit.yml @@ -0,0 +1,82 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Security +# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards +# PATH: /.gitea/workflows/security-audit.yml +# VERSION: 01.00.00 +# BRIEF: Dependency vulnerability scanning for composer and npm packages + +name: "Universal: Security Audit" + +on: + schedule: + - cron: '0 6 * * 1' # Weekly on Monday at 06:00 UTC + pull_request: + branches: + - main + paths: + - 'composer.json' + - 'composer.lock' + - 'package.json' + - 'package-lock.json' + workflow_dispatch: + +permissions: + contents: read + +env: + NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }} + NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }} + +jobs: + audit: + name: Dependency Audit + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Composer audit + if: hashFiles('composer.lock') != '' + run: | + echo "=== Composer Security Audit ===" + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq + sudo apt-get install -y -qq php-cli composer >/dev/null 2>&1 + fi + composer audit --format=plain 2>&1 | tee /tmp/composer-audit.txt + RESULT=$? + if [ $RESULT -ne 0 ]; then + echo "::warning::Composer vulnerabilities found" + echo "composer_vulnerable=true" >> "$GITHUB_ENV" + else + echo "No known vulnerabilities in composer dependencies" + fi + + - name: NPM audit + if: hashFiles('package-lock.json') != '' + run: | + echo "=== NPM Security Audit ===" + npm audit --production 2>&1 | tee /tmp/npm-audit.txt || true + if npm audit --production 2>&1 | grep -q "found 0 vulnerabilities"; then + echo "No known vulnerabilities in npm dependencies" + else + echo "::warning::NPM vulnerabilities found" + echo "npm_vulnerable=true" >> "$GITHUB_ENV" + fi + + - name: Notify on vulnerabilities + if: env.composer_vulnerable == 'true' || env.npm_vulnerable == 'true' + run: | + REPO="${{ github.event.repository.name }}" + curl -sS \ + -H "Title: ${REPO} has vulnerable dependencies" \ + -H "Tags: lock,warning" \ + -H "Priority: high" \ + -d "Security audit found vulnerabilities. Review dependency updates." \ + "${NTFY_URL}/${NTFY_TOPIC}" || true -- 2.52.0 From 535cd10d2fab6443b174434669a429330d523804 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 18:55:43 +0000 Subject: [PATCH 093/136] chore: sync .mokogitea/workflows/update-server.yml from template [skip ci] --- .mokogitea/workflows/update-server.yml | 464 +++++++++++++++++++++++++ 1 file changed, 464 insertions(+) create mode 100644 .mokogitea/workflows/update-server.yml diff --git a/.mokogitea/workflows/update-server.yml b/.mokogitea/workflows/update-server.yml new file mode 100644 index 0000000..6e617f6 --- /dev/null +++ b/.mokogitea/workflows/update-server.yml @@ -0,0 +1,464 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Joomla +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# PATH: /templates/workflows/joomla/update-server.yml.template +# VERSION: 04.06.00 +# BRIEF: Update Joomla update server XML feed with stable/rc/dev entries +# +# Writes updates.xml with multiple entries: +# - stable on push to main (from auto-release) +# - rc on push to rc/** +# - development on push to dev or dev/** +# +# Joomla filters by user's "Minimum Stability" setting. + +name: "Joomla: Update Server" + +on: + push: + branches: + - 'dev' + - 'dev/**' + - 'alpha/**' + - 'beta/**' + - 'rc/**' + paths: + - 'src/**' + - 'htdocs/**' + pull_request: + types: [closed] + branches: + - 'dev' + - 'dev/**' + - 'alpha/**' + - 'beta/**' + - 'rc/**' + paths: + - 'src/**' + - 'htdocs/**' + workflow_dispatch: + inputs: + stability: + description: 'Stability tag' + required: true + default: 'development' + type: choice + options: + - development + - alpha + - beta + - rc + - stable + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +permissions: + contents: write + +jobs: + update-xml: + name: Update updates.xml + runs-on: release + if: >- + github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push' + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.GA_TOKEN }} + fetch-depth: 0 + + - name: Setup MokoStandards tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_TOKEN }}"}}}' + run: | + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + 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: Generate updates.xml entry + id: update + run: | + BRANCH="${{ github.ref_name }}" + REPO="${{ github.repository }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "0.0.0") + + # Auto-bump patch on all branches (dev, alpha, beta, rc) + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + BUMPED=$(php /tmp/mokostandards-api/cli/version_bump.php --path . 2>/dev/null || true) + if [ -n "$BUMPED" ]; then + VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "$VERSION") + git add -A + git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \ + --author="gitea-actions[bot] " 2>/dev/null || true + git push 2>/dev/null || true + fi + + # Determine stability from branch or input + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + STABILITY="${{ inputs.stability }}" + elif [[ "$BRANCH" == rc/* ]]; then + STABILITY="rc" + elif [[ "$BRANCH" == beta/* ]]; then + STABILITY="beta" + elif [[ "$BRANCH" == alpha/* ]]; then + STABILITY="alpha" + elif [[ "$BRANCH" == dev/* ]] || [[ "$BRANCH" == "dev" ]]; then + STABILITY="development" + else + STABILITY="stable" + fi + + echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" + + # Parse manifest (portable — no grep -P) + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '/dev/null | head -1) + if [ -z "$MANIFEST" ]; then + echo "No Joomla manifest found — skipping" + exit 0 + fi + + # Extract fields using sed (works on all runners) + EXT_NAME=$(sed -n 's/.*\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1) + EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1) + EXT_CLIENT=$(sed -n 's/.*]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_FOLDER=$(sed -n 's/.*]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_VERSION=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1) + TARGET_PLATFORM=$(sed -n 's/.*\(\).*/\1/p' "$MANIFEST" | head -1) + PHP_MINIMUM=$(sed -n 's/.*\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1) + + # Fallbacks + [ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}" + [ -z "$EXT_TYPE" ] && EXT_TYPE="component" + + # Derive element if not in manifest: try XML filename, then repo name + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') + case "$EXT_ELEMENT" in + templatedetails|manifest|*.xml) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;; + esac + fi + + # Use manifest version if README version is empty + [ "$VERSION" = "0.0.0" ] && [ -n "$EXT_VERSION" ] && VERSION="$EXT_VERSION" + + [ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '' "/") + + CLIENT_TAG="" + [ -n "$EXT_CLIENT" ] && CLIENT_TAG="${EXT_CLIENT}" + [ -z "$CLIENT_TAG" ] && ([ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]) && CLIENT_TAG="site" + + FOLDER_TAG="" + [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ] && FOLDER_TAG="${EXT_FOLDER}" + + PHP_TAG="" + [ -n "$PHP_MINIMUM" ] && PHP_TAG="${PHP_MINIMUM}" + + # Version suffix for non-stable + DISPLAY_VERSION="$VERSION" + case "$STABILITY" in + development) DISPLAY_VERSION="${VERSION}-dev" ;; + alpha) DISPLAY_VERSION="${VERSION}-alpha" ;; + beta) DISPLAY_VERSION="${VERSION}-beta" ;; + rc) DISPLAY_VERSION="${VERSION}-rc" ;; + esac + + MAJOR=$(echo "$VERSION" | awk -F. '{print $1}') + + # Each stability level has its own release tag + case "$STABILITY" in + development) RELEASE_TAG="development" ;; + alpha) RELEASE_TAG="alpha" ;; + beta) RELEASE_TAG="beta" ;; + rc) RELEASE_TAG="release-candidate" ;; + *) RELEASE_TAG="v${MAJOR}" ;; + esac + + PACKAGE_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.zip" + DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}" + INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}" + + # -- Build install packages (ZIP + tar.gz) -------------------- + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + if [ -d "$SOURCE_DIR" ]; then + EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*" + TAR_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.tar.gz" + + cd "$SOURCE_DIR" + zip -r "/tmp/${PACKAGE_NAME}" . -x $EXCLUDES + cd .. + tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \ + --exclude='.ftpignore' --exclude='sftp-config*' \ + --exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' . + + SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1) + + # Ensure release exists on Gitea + RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true) + RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + + if [ -z "$RELEASE_ID" ]; then + # Create release + RELEASE_JSON=$(curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/releases" \ + -d "$(python3 -c "import json; print(json.dumps({ + 'tag_name': '${RELEASE_TAG}', + 'name': '${RELEASE_TAG} (${DISPLAY_VERSION})', + 'body': '${STABILITY} release', + 'prerelease': True, + 'target_commitish': 'main' + }))")" 2>/dev/null || true) + RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + fi + + if [ -n "$RELEASE_ID" ]; then + # Delete existing assets with same name before uploading + ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]") + for ASSET_FILE in "$PACKAGE_NAME" "$TAR_NAME"; do + ASSET_ID=$(echo "$ASSETS" | python3 -c " + import sys,json + assets = json.load(sys.stdin) + for a in assets: + if a['name'] == '${ASSET_FILE}': + print(a['id']); break + " 2>/dev/null || true) + if [ -n "$ASSET_ID" ]; then + curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true + fi + done + + # Upload both formats + curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @"/tmp/${PACKAGE_NAME}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets?name=${PACKAGE_NAME}" > /dev/null 2>&1 || true + + curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @"/tmp/${TAR_NAME}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets?name=${TAR_NAME}" > /dev/null 2>&1 || true + fi + + echo "Packages: ${PACKAGE_NAME} + ${TAR_NAME} (SHA: ${SHA256})" >> $GITHUB_STEP_SUMMARY + else + SHA256="" + fi + + # -- Build the new entry (canonical format matching release.yml) -- + NEW_ENTRY="" + NEW_ENTRY="${NEW_ENTRY} \n" + NEW_ENTRY="${NEW_ENTRY} ${EXT_NAME}\n" + NEW_ENTRY="${NEW_ENTRY} ${EXT_NAME} ${STABILITY} build.\n" + NEW_ENTRY="${NEW_ENTRY} ${EXT_ELEMENT}\n" + NEW_ENTRY="${NEW_ENTRY} ${EXT_TYPE}\n" + [ -n "$CLIENT_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${CLIENT_TAG}\n" + [ -n "$FOLDER_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${FOLDER_TAG}\n" + NEW_ENTRY="${NEW_ENTRY} ${VERSION}\n" + NEW_ENTRY="${NEW_ENTRY} $(date +%Y-%m-%d)\n" + NEW_ENTRY="${NEW_ENTRY} https://git.mokoconsulting.tech/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${RELEASE_TAG}\n" + NEW_ENTRY="${NEW_ENTRY} \n" + NEW_ENTRY="${NEW_ENTRY} ${DOWNLOAD_URL}\n" + NEW_ENTRY="${NEW_ENTRY} \n" + [ -n "$SHA256" ] && NEW_ENTRY="${NEW_ENTRY} ${SHA256}\n" + NEW_ENTRY="${NEW_ENTRY} ${STABILITY}\n" + NEW_ENTRY="${NEW_ENTRY} Moko Consulting\n" + NEW_ENTRY="${NEW_ENTRY} https://mokoconsulting.tech\n" + NEW_ENTRY="${NEW_ENTRY} \n" + [ -n "$PHP_MINIMUM" ] && NEW_ENTRY="${NEW_ENTRY} ${PHP_MINIMUM}\n" + NEW_ENTRY="${NEW_ENTRY} " + + # -- Write new entry to temp file -------------------------------- + printf '%b' "$NEW_ENTRY" > /tmp/new_entry.xml + + # -- Merge into updates.xml ---------------------------------------- + # Cascade: stable→all | rc→rc+lower | beta→beta+lower | alpha→alpha+dev | dev→dev + CASCADE_MAP="stable:development,alpha,beta,rc,stable rc:development,alpha,beta,rc beta:development,alpha,beta alpha:development,alpha development:development" + TARGETS="" + for entry in $CASCADE_MAP; do + key="${entry%%:*}" + vals="${entry#*:}" + if [ "$key" = "${STABILITY}" ]; then + TARGETS="$vals" + break + fi + done + [ -z "$TARGETS" ] && TARGETS="${STABILITY}" + + echo "Cascade: ${STABILITY} → ${TARGETS}" + + # Create updates.xml if missing + if [ ! -f "updates.xml" ]; then + printf '%s\n' "" > updates.xml + printf '%s\n' "" >> updates.xml + printf '%s\n' "" >> updates.xml + printf '%s\n' "" >> updates.xml + fi + + # Update existing blocks or create missing ones + export PY_TARGETS="$TARGETS" PY_VERSION="$VERSION" PY_DATE="$(date +%Y-%m-%d)" + python3 << 'PYEOF' + import re, os + + targets = os.environ["PY_TARGETS"].split(",") + version = os.environ["PY_VERSION"] + date = os.environ["PY_DATE"] + + with open("updates.xml") as f: + content = f.read() + with open("/tmp/new_entry.xml") as f: + new_entry_template = f.read() + + for tag in targets: + tag = tag.strip() + # Build entry with this tag's name + new_entry = re.sub(r"[^<]*", f"{tag}", new_entry_template) + + # Try to find existing block (handles both single-line and multi-line ) + block_pattern = r"((?:(?!).)*?" + re.escape(tag) + r".*?)" + match = re.search(block_pattern, content, re.DOTALL) + + if match: + # Update in place — replace entire block + content = content.replace(match.group(1), new_entry.strip()) + print(f" UPDATED: {tag} → {version}") + else: + # Create — insert before + content = content.replace("", "\n" + new_entry.strip() + "\n\n") + print(f" CREATED: {tag} → {version}") + + # Clean up excessive blank lines + content = re.sub(r"\n{3,}", "\n\n", content) + + with open("updates.xml", "w") as f: + f.write(content) + PYEOF + + # Commit + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git add updates.xml + git diff --cached --quiet || { + git commit -m "chore: update updates.xml (${STABILITY}: ${DISPLAY_VERSION}) [skip ci]" \ + --author="gitea-actions[bot] " + git push + } + + # -- Sync updates.xml to main (for non-main branches) ---------------------- + - name: Sync updates.xml to main + if: github.ref_name != 'main' + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + GA_TOKEN="${{ secrets.GA_TOKEN }}" + + FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \ + "${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true) + + if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then + CONTENT=$(base64 -w0 updates.xml) + curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/contents/updates.xml" \ + -d "$(python3 -c "import json; print(json.dumps({ + 'content': '${CONTENT}', + 'sha': '${FILE_SHA}', + 'message': 'chore: sync updates.xml from ${STABILITY} [skip ci]', + 'branch': 'main' + }))")" > /dev/null 2>&1 \ + && echo "updates.xml synced to main (${STABILITY})" >> $GITHUB_STEP_SUMMARY \ + || echo "WARNING: failed to sync updates.xml to main" >> $GITHUB_STEP_SUMMARY + else + echo "WARNING: could not get updates.xml SHA from main" >> $GITHUB_STEP_SUMMARY + fi + + - name: SFTP deploy to dev server + if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev' + env: + DEV_HOST: ${{ vars.DEV_FTP_HOST }} + DEV_PATH: ${{ vars.DEV_FTP_PATH }} + DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }} + DEV_USER: ${{ vars.DEV_FTP_USERNAME }} + DEV_PORT: ${{ vars.DEV_FTP_PORT }} + DEV_KEY: ${{ secrets.DEV_FTP_KEY }} + DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }} + run: | + # -- Permission check: admin or maintain role required -------- + ACTOR="${{ github.actor }}" + REPO="${{ github.repository }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \ + python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read") + case "$PERMISSION" in + admin|maintain|write) ;; + *) + echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write" + exit 0 + ;; + esac + + [ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; } + + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + [ ! -d "$SOURCE_DIR" ] && exit 0 + + PORT="${DEV_PORT:-22}" + REMOTE="${DEV_PATH%/}" + [ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}" + + printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \ + "$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json + if [ -n "$DEV_KEY" ]; then + echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key + printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json + else + printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json + fi + + PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true) + if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then + php /tmp/mokostandards-api/deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json + elif [ -f "/tmp/mokostandards-api/deploy/deploy-sftp.php" ]; then + php /tmp/mokostandards-api/deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json + fi + rm -f /tmp/deploy_key /tmp/sftp-config.json + echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY + + - name: Summary + if: always() + run: | + echo "## Joomla Update Server" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${DISPLAY_VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Element | \`${EXT_ELEMENT}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Download | [ZIP](${DOWNLOAD_URL}) |" >> $GITHUB_STEP_SUMMARY -- 2.52.0 From 4a2c96177f58a89814da77b98d972e92ef0aba76 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 18:55:44 +0000 Subject: [PATCH 094/136] chore: sync .mokogitea/ISSUE_TEMPLATE/adr.md from template [skip ci] --- .mokogitea/ISSUE_TEMPLATE/adr.md | 110 +++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 .mokogitea/ISSUE_TEMPLATE/adr.md diff --git a/.mokogitea/ISSUE_TEMPLATE/adr.md b/.mokogitea/ISSUE_TEMPLATE/adr.md new file mode 100644 index 0000000..eb40760 --- /dev/null +++ b/.mokogitea/ISSUE_TEMPLATE/adr.md @@ -0,0 +1,110 @@ +--- +name: Architecture Decision Record (ADR) +about: Propose or document an architectural decision +title: '[ADR] ' +labels: 'architecture, decision' +assignees: '' + +--- + + +## ADR Number +ADR-XXXX + +## Status +- [ ] Proposed +- [ ] Accepted +- [ ] Deprecated +- [ ] Superseded by ADR-XXXX + +## Context +Describe the issue or problem that motivates this decision. + +## Decision +State the architecture decision and provide rationale. + +## Consequences +### Positive +- List positive consequences + +### Negative +- List negative consequences or trade-offs + +### Neutral +- List neutral aspects + +## Alternatives Considered +### Alternative 1 +- Description +- Pros +- Cons +- Why not chosen + +### Alternative 2 +- Description +- Pros +- Cons +- Why not chosen + +## Implementation Plan +1. Step 1 +2. Step 2 +3. Step 3 + +## Stakeholders +- **Decision Makers**: @user1, @user2 +- **Consulted**: @user3, @user4 +- **Informed**: team-name + +## Technical Details +### Architecture Diagram +``` +[Add diagram or link] +``` + +### Dependencies +- Dependency 1 +- Dependency 2 + +### Impact Analysis +- **Performance**: [Impact description] +- **Security**: [Impact description] +- **Scalability**: [Impact description] +- **Maintainability**: [Impact description] + +## Testing Strategy +- [ ] Unit tests +- [ ] Integration tests +- [ ] Performance tests +- [ ] Security tests + +## Documentation +- [ ] Architecture documentation updated +- [ ] API documentation updated +- [ ] Developer guide updated +- [ ] Runbook created + +## Migration Path +Describe how to migrate from current state to new architecture. + +## Rollback Plan +Describe how to rollback if issues occur. + +## Timeline +- **Proposal Date**: +- **Decision Date**: +- **Implementation Start**: +- **Expected Completion**: + +## References +- Related ADRs: +- External resources: +- RFCs: + +## Review Checklist +- [ ] Aligns with enterprise architecture principles +- [ ] Security implications reviewed +- [ ] Performance implications reviewed +- [ ] Cost implications reviewed +- [ ] Compliance requirements met +- [ ] Team consensus achieved -- 2.52.0 From 5644f2c6c55272e8fb4f53d80c78f7000bf63cb4 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 18:55:44 +0000 Subject: [PATCH 095/136] chore: sync .mokogitea/ISSUE_TEMPLATE/bug_report.md from template [skip ci] --- .mokogitea/ISSUE_TEMPLATE/bug_report.md | 48 +++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .mokogitea/ISSUE_TEMPLATE/bug_report.md diff --git a/.mokogitea/ISSUE_TEMPLATE/bug_report.md b/.mokogitea/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..38a16a7 --- /dev/null +++ b/.mokogitea/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,48 @@ +--- +name: Bug Report +about: Report a bug or issue with the project +title: '[BUG] ' +labels: 'bug' +assignees: '' + +--- + + +## Bug Description +A clear and concise description of what the bug is. + +## Steps to Reproduce +1. Go to '...' +2. Click on '...' +3. Scroll down to '...' +4. See error + +## Expected Behavior +A clear and concise description of what you expected to happen. + +## Actual Behavior +A clear and concise description of what actually happened. + +## Screenshots +If applicable, add screenshots to help explain your problem. + +## Environment +- **Project**: [e.g., MokoDoliTools, moko-cassiopeia] +- **Version**: [e.g., 1.2.3] +- **Platform**: [e.g., Dolibarr 18.0, Joomla 5.0] +- **PHP Version**: [e.g., 8.1] +- **Database**: [e.g., MySQL 8.0, PostgreSQL 14] +- **Browser** (if applicable): [e.g., Chrome 120, Firefox 121] +- **OS**: [e.g., Ubuntu 22.04, Windows 11] + +## Additional Context +Add any other context about the problem here. + +## Possible Solution +If you have suggestions on how to fix the issue, please describe them here. + +## Checklist +- [ ] I have searched for similar issues before creating this one +- [ ] I have provided all the requested information +- [ ] I have tested this on the latest stable version +- [ ] I have checked the documentation and couldn't find a solution -- 2.52.0 From b52b4e3e119d10da2b400300c9da08beb77fc265 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 18:55:45 +0000 Subject: [PATCH 096/136] chore: sync .mokogitea/ISSUE_TEMPLATE/config.yml from template [skip ci] --- .mokogitea/ISSUE_TEMPLATE/config.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .mokogitea/ISSUE_TEMPLATE/config.yml diff --git a/.mokogitea/ISSUE_TEMPLATE/config.yml b/.mokogitea/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..d4d49ec --- /dev/null +++ b/.mokogitea/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,18 @@ +--- +blank_issues_enabled: true +contact_links: + - name: 💼 Enterprise Support + url: https://mokoconsulting.tech/enterprise + about: Enterprise-level support and consultation services + - name: 💬 Ask a Question + url: https://mokoconsulting.tech/ + about: Get help or ask questions through our website + - name: 📚 MokoStandards Documentation + url: https://git.mokoconsulting.tech/MokoConsulting/moko-platform + about: View our coding standards and best practices + - name: 🔒 Report a Security Vulnerability + url: https://git.mokoconsulting.tech/mokoconsulting-tech/.github-private/security/advisories/new + about: Report security vulnerabilities privately (for critical issues) + - name: 💡 Community Discussions + url: https://github.com/orgs/mokoconsulting-tech/discussions + about: Join community discussions and Q&A -- 2.52.0 From 967afeba770418229d81f40c676a903af480c728 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 18:55:45 +0000 Subject: [PATCH 097/136] chore: sync .mokogitea/ISSUE_TEMPLATE/documentation.md from template [skip ci] --- .mokogitea/ISSUE_TEMPLATE/documentation.md | 52 ++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .mokogitea/ISSUE_TEMPLATE/documentation.md diff --git a/.mokogitea/ISSUE_TEMPLATE/documentation.md b/.mokogitea/ISSUE_TEMPLATE/documentation.md new file mode 100644 index 0000000..ed4dabc --- /dev/null +++ b/.mokogitea/ISSUE_TEMPLATE/documentation.md @@ -0,0 +1,52 @@ +--- +name: Documentation Issue +about: Report an issue with documentation +title: '[DOCS] ' +labels: 'documentation' +assignees: '' + +--- + + +## Documentation Issue + +**Location**: + + +## Issue Type + +- [ ] Typo or grammar error +- [ ] Outdated information +- [ ] Missing documentation +- [ ] Unclear explanation +- [ ] Broken links +- [ ] Missing examples +- [ ] Other (specify below) + +## Description + + +## Current Content + +``` +Current text here +``` + +## Suggested Improvement + +``` +Suggested text here +``` + +## Additional Context + + +## Standards Alignment +- [ ] Follows MokoStandards documentation guidelines +- [ ] Uses en_US/en_GB localization +- [ ] Includes proper SPDX headers where applicable + +## Checklist +- [ ] I have searched for similar documentation issues +- [ ] I have provided a clear description +- [ ] I have suggested an improvement (if applicable) -- 2.52.0 From f8b7bd8a4b456bf3a2b9314382ebcc1dae0d9b7c Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 18:55:45 +0000 Subject: [PATCH 098/136] chore: sync .mokogitea/ISSUE_TEMPLATE/feature_request.md from template [skip ci] --- .mokogitea/ISSUE_TEMPLATE/feature_request.md | 51 ++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .mokogitea/ISSUE_TEMPLATE/feature_request.md diff --git a/.mokogitea/ISSUE_TEMPLATE/feature_request.md b/.mokogitea/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..7b76dc9 --- /dev/null +++ b/.mokogitea/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,51 @@ +--- +name: Feature Request +about: Suggest a new feature or enhancement +title: '[FEATURE] ' +labels: 'enhancement' +assignees: '' + +--- + + +## Feature Description +A clear and concise description of the feature you'd like to see. + +## Problem or Use Case +Describe the problem this feature would solve or the use case it addresses. +Ex. I'm always frustrated when [...] + +## Proposed Solution +A clear and concise description of what you want to happen. + +## Alternative Solutions +A clear and concise description of any alternative solutions or features you've considered. + +## Benefits +Describe how this feature would benefit users: +- Who would use this feature? +- What problems does it solve? +- What value does it add? + +## Implementation Details (Optional) +If you have ideas about how this could be implemented, share them here: +- Technical approach +- Files/components that might need changes +- Any concerns or challenges you foresee + +## Additional Context +Add any other context, mockups, or screenshots about the feature request here. + +## Relevant Standards +Does this relate to any standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards)? +- [ ] Accessibility (WCAG 2.1 AA) +- [ ] Localization (en_US/en_GB) +- [ ] Security best practices +- [ ] Code quality standards +- [ ] Other: [specify] + +## Checklist +- [ ] I have searched for similar feature requests before creating this one +- [ ] I have clearly described the use case and benefits +- [ ] I have considered alternative solutions +- [ ] This feature aligns with the project's goals and scope -- 2.52.0 From da4470d6a5360d44ff5d9fbe8b0e83b45eccc24c Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 18:55:46 +0000 Subject: [PATCH 099/136] chore: sync .mokogitea/ISSUE_TEMPLATE/joomla_issue.md from template [skip ci] --- .mokogitea/ISSUE_TEMPLATE/joomla_issue.md | 87 +++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 .mokogitea/ISSUE_TEMPLATE/joomla_issue.md diff --git a/.mokogitea/ISSUE_TEMPLATE/joomla_issue.md b/.mokogitea/ISSUE_TEMPLATE/joomla_issue.md new file mode 100644 index 0000000..d808f79 --- /dev/null +++ b/.mokogitea/ISSUE_TEMPLATE/joomla_issue.md @@ -0,0 +1,87 @@ +--- +name: Joomla Extension Issue +about: Report an issue with a Joomla extension +title: '[JOOMLA] ' +labels: 'joomla' +assignees: '' + +--- + + +## Issue Type +- [ ] Component Issue +- [ ] Module Issue +- [ ] Plugin Issue +- [ ] Template Issue + +## Extension Details +- **Extension Name**: [e.g., moko-cassiopeia] +- **Extension Version**: [e.g., 1.2.3] +- **Extension Type**: [Component / Module / Plugin / Template] + +## Joomla Environment +- **Joomla Version**: [e.g., 4.4.0, 5.0.0] +- **PHP Version**: [e.g., 8.1.0] +- **Database**: [MySQL / PostgreSQL / MariaDB] +- **Database Version**: [e.g., 8.0] +- **Server**: [Apache / Nginx / IIS] +- **Hosting**: [Shared / VPS / Dedicated / Cloud] + +## Issue Description +Provide a clear and detailed description of the issue. + +## Steps to Reproduce +1. Go to '...' +2. Click on '...' +3. Configure '...' +4. See error + +## Expected Behavior +What you expected to happen. + +## Actual Behavior +What actually happened. + +## Error Messages +``` +# Paste any error messages from Joomla error logs +# Location: administrator/logs/error.php +``` + +## Browser Console Errors +```javascript +// Paste any JavaScript console errors (F12 in browser) +``` + +## Screenshots +Add screenshots to help explain the issue. + +## Configuration +```ini +# Paste extension configuration (sanitize sensitive data) +``` + +## Installed Extensions +List other installed extensions that might conflict: +- Extension 1 (version) +- Extension 2 (version) + +## Template Overrides +- [ ] Using template overrides +- [ ] Custom CSS +- [ ] Custom JavaScript + +## Additional Context +- **Multilingual Site**: [Yes / No] +- **Cache Enabled**: [Yes / No] +- **Debug Mode**: [Yes / No] +- **SEF URLs**: [Yes / No] + +## Checklist +- [ ] I have cleared Joomla cache +- [ ] I have disabled other extensions to test for conflicts +- [ ] I have checked Joomla error logs +- [ ] I have tested with a default Joomla template +- [ ] I have checked browser console for JavaScript errors +- [ ] I have searched for similar issues +- [ ] I am using a supported Joomla version -- 2.52.0 From 725b874cd3cd08a055a637d2a1c437f5692afe40 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 18:55:46 +0000 Subject: [PATCH 100/136] chore: sync .mokogitea/ISSUE_TEMPLATE/question.md from template [skip ci] --- .mokogitea/ISSUE_TEMPLATE/question.md | 82 +++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 .mokogitea/ISSUE_TEMPLATE/question.md diff --git a/.mokogitea/ISSUE_TEMPLATE/question.md b/.mokogitea/ISSUE_TEMPLATE/question.md new file mode 100644 index 0000000..3175013 --- /dev/null +++ b/.mokogitea/ISSUE_TEMPLATE/question.md @@ -0,0 +1,82 @@ +--- +name: Question +about: Ask a question about usage, features, or best practices +title: '[QUESTION] ' +labels: ['question'] +assignees: ['jmiller'] +--- + + +## Question + +**Your question:** + + +## Context + +**What are you trying to accomplish?** + + +**What have you already tried?** + + +**Category**: +- [ ] Script usage +- [ ] Configuration +- [ ] Workflow setup +- [ ] Documentation interpretation +- [ ] Best practices +- [ ] Integration +- [ ] Other: __________ + +## Environment (if relevant) + +**Your setup**: +- Operating System: +- Version: + +## What You've Researched + +**Documentation reviewed**: +- [ ] README.md +- [ ] Project documentation +- [ ] Other (specify): __________ + +**Similar issues/questions found**: +- # +- # + +## Expected Outcome + +**What result are you hoping for?** + + +## Code/Configuration Samples + +**Relevant code or configuration** (if applicable): + +```bash +# Your code here +``` + +## Additional Context + +**Any other relevant information:** + + +**Screenshots** (if helpful): + + +## Urgency + +- [ ] Urgent (blocking work) +- [ ] Normal (can work on other things meanwhile) +- [ ] Low priority (just curious) + +## Checklist + +- [ ] I have searched existing issues and discussions +- [ ] I have reviewed relevant documentation +- [ ] I have provided sufficient context +- [ ] I have included code/configuration samples if relevant +- [ ] This is a genuine question (not a bug report or feature request) -- 2.52.0 From 43ca491bb00bbac2774684763b8515f8a5a805fd Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 18:55:46 +0000 Subject: [PATCH 101/136] chore: sync .mokogitea/ISSUE_TEMPLATE/rfc.md from template [skip ci] --- .mokogitea/ISSUE_TEMPLATE/rfc.md | 126 +++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 .mokogitea/ISSUE_TEMPLATE/rfc.md diff --git a/.mokogitea/ISSUE_TEMPLATE/rfc.md b/.mokogitea/ISSUE_TEMPLATE/rfc.md new file mode 100644 index 0000000..6f09af7 --- /dev/null +++ b/.mokogitea/ISSUE_TEMPLATE/rfc.md @@ -0,0 +1,126 @@ +--- +name: Request for Comments (RFC) +about: Propose a significant change for community discussion +title: '[RFC] ' +labels: 'rfc, discussion' +assignees: '' + +--- + + +## RFC Summary +One-paragraph summary of the proposal. + +## Motivation +Why are we doing this? What use cases does it support? What is the expected outcome? + +## Detailed Design +### Overview +Provide a detailed explanation of the proposed change. + +### API Changes (if applicable) +```php +// Before +function oldApi($param1) { } + +// After +function newApi($param1, $param2) { } +``` + +### User Experience Changes +Describe how users will interact with this change. + +### Implementation Approach +High-level implementation strategy. + +## Drawbacks +Why should we *not* do this? + +## Alternatives +What other designs have been considered? What is the impact of not doing this? + +### Alternative 1 +- Description +- Trade-offs + +### Alternative 2 +- Description +- Trade-offs + +## Adoption Strategy +How will existing users adopt this? Is this a breaking change? + +### Migration Guide +```bash +# Steps to migrate +``` + +### Deprecation Timeline +- **Announcement**: +- **Deprecation**: +- **Removal**: + +## Unresolved Questions +- Question 1 +- Question 2 + +## Future Possibilities +What future work does this enable? + +## Impact Assessment +### Performance +Expected performance impact. + +### Security +Security considerations and implications. + +### Compatibility +- **Backward Compatible**: [Yes / No] +- **Breaking Changes**: [List] + +### Maintenance +Long-term maintenance considerations. + +## Community Input +### Stakeholders +- [ ] Core team +- [ ] Module developers +- [ ] End users +- [ ] Enterprise customers + +### Feedback Period +**Duration**: [e.g., 2 weeks] +**Deadline**: [date] + +## Implementation Timeline +### Phase 1: Design +- [ ] RFC discussion +- [ ] Design finalization +- [ ] Approval + +### Phase 2: Implementation +- [ ] Core implementation +- [ ] Tests +- [ ] Documentation + +### Phase 3: Release +- [ ] Beta release +- [ ] Feedback collection +- [ ] Stable release + +## Success Metrics +How will we measure success? +- Metric 1 +- Metric 2 + +## References +- Related RFCs: +- External documentation: +- Prior art: + +## Open Questions for Community +1. Question 1? +2. Question 2? + +--- +**Note**: This RFC is open for community discussion. Please provide feedback in the comments below. -- 2.52.0 From 7030c36ce68dfb84318ce1c9470e6c9a693e7f7f Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 18:55:47 +0000 Subject: [PATCH 102/136] chore: sync .mokogitea/ISSUE_TEMPLATE/security.md from template [skip ci] --- .mokogitea/ISSUE_TEMPLATE/security.md | 51 +++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .mokogitea/ISSUE_TEMPLATE/security.md diff --git a/.mokogitea/ISSUE_TEMPLATE/security.md b/.mokogitea/ISSUE_TEMPLATE/security.md new file mode 100644 index 0000000..f57b284 --- /dev/null +++ b/.mokogitea/ISSUE_TEMPLATE/security.md @@ -0,0 +1,51 @@ +--- +name: Security Vulnerability Report +about: Report a security vulnerability (use only for non-critical issues) +title: '[SECURITY] ' +labels: 'security' +assignees: '' + +--- + + +## ⚠️ IMPORTANT: Private Disclosure Required + +**For critical security vulnerabilities, DO NOT use this template.** +Follow the process in [SECURITY.md](../SECURITY.md) for responsible disclosure. + +Use this template only for: +- Security improvements +- Non-critical security suggestions +- Security documentation updates + +--- + +## Security Issue + +**Severity**: + + +## Description + + +## Affected Components + + +## Suggested Mitigation + + +## Standards Reference +Does this relate to security standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards)? +- [ ] SPDX license identifiers +- [ ] Secret management +- [ ] Dependency security +- [ ] Access control +- [ ] Other: [specify] + +## Additional Context + + +## Checklist +- [ ] This is NOT a critical vulnerability requiring private disclosure +- [ ] I have reviewed the SECURITY.md policy +- [ ] I have provided sufficient detail for evaluation -- 2.52.0 From 4c1d155f2077a26e595cfa2fa6e8ccb6783fe3a4 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 18:55:47 +0000 Subject: [PATCH 103/136] chore: sync .mokogitea/ISSUE_TEMPLATE/version.md from template [skip ci] --- .mokogitea/ISSUE_TEMPLATE/version.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .mokogitea/ISSUE_TEMPLATE/version.md diff --git a/.mokogitea/ISSUE_TEMPLATE/version.md b/.mokogitea/ISSUE_TEMPLATE/version.md new file mode 100644 index 0000000..6328421 --- /dev/null +++ b/.mokogitea/ISSUE_TEMPLATE/version.md @@ -0,0 +1,24 @@ +--- +name: Version Bump +about: Request or track a version change +title: '[VERSION] ' +labels: 'version, type: version' +assignees: 'jmiller' +--- + +## Version Change + +**Current version**: +**Requested version**: +**Change type**: + +## Reason + + + +## Checklist + +- [ ] README.md `VERSION:` field updated +- [ ] CHANGELOG.md entry added +- [ ] Module descriptor version updated (Dolibarr: `$this->version`, Joomla: ``) +- [ ] All file headers will be auto-propagated by `sync-version-on-merge` workflow -- 2.52.0 From 23a8487b65b4e7110da04c70c032a1ae16d00818 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 19:25:57 +0000 Subject: [PATCH 104/136] chore: force-sync .mokogitea/workflows/auto-release.yml [skip ci] --- .mokogitea/workflows/auto-release.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index 1fe7aa6..84fc701 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -8,13 +8,13 @@ # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API # PATH: /templates/workflows/universal/auto-release.yml.template # VERSION: 05.00.00 -# BRIEF: Universal build & release � detects platform from .moko-platform +# BRIEF: Universal build & release � detects platform from manifest.xml # # +========================================================================+ # | UNIVERSAL BUILD & RELEASE PIPELINE | # +========================================================================+ # | | -# | Reads .moko-platform (joomla|dolibarr|generic) to branch logic. | +# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. | # | | # | Platform-specific: | # | joomla: XML manifest, updates.xml, type-prefixed packages | @@ -80,8 +80,8 @@ jobs: id: platform run: | # Read platform from XML manifest ( tag) or plain text fallback - PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/.moko-platform 2>/dev/null | head -1) - [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/.moko-platform 2>/dev/null | tr -d '[:space:]') + 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" echo "Platform detected: ${PLATFORM}" -- 2.52.0 From 6e96a4e4be55b3a2c76136811adab0fd1363c67e Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 19:25:57 +0000 Subject: [PATCH 105/136] chore: force-sync .mokogitea/workflows/cascade-dev.yml [skip ci] -- 2.52.0 From d942f0afe32b74a97cc1756c209e06ecc8991678 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 19:25:58 +0000 Subject: [PATCH 106/136] chore: force-sync .mokogitea/workflows/ci-joomla.yml [skip ci] -- 2.52.0 From c1c4e8fbe1d33b1cdd5d166a35e2a7d4f74357be Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 19:25:58 +0000 Subject: [PATCH 107/136] chore: force-sync .mokogitea/workflows/cleanup.yml [skip ci] -- 2.52.0 From b94bc929e48be469b91b50ed7334bb2d10f11808 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 19:25:58 +0000 Subject: [PATCH 108/136] chore: force-sync .mokogitea/workflows/deploy-manual.yml [skip ci] -- 2.52.0 From 9de82f84e957f440f228642c7fabdecea2c8324a Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 19:25:59 +0000 Subject: [PATCH 109/136] chore: force-sync .mokogitea/workflows/gitleaks.yml [skip ci] -- 2.52.0 From 0020bef6c01d6d0a9015de55092a2f5bb50cd9ef Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 19:25:59 +0000 Subject: [PATCH 110/136] chore: force-sync .mokogitea/workflows/notify.yml [skip ci] -- 2.52.0 From e936ef24d924b985ebe1bb4f707903c3e87e7705 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 19:26:00 +0000 Subject: [PATCH 111/136] chore: force-sync .mokogitea/workflows/pr-check.yml [skip ci] --- .mokogitea/workflows/pr-check.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index bd06c90..99e063f 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -109,8 +109,8 @@ jobs: id: platform run: | # Read platform from XML manifest ( tag) or plain text fallback - PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/.moko-platform 2>/dev/null | head -1) - [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/.moko-platform 2>/dev/null | tr -d '[:space:]') + 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" -- 2.52.0 From d074b06225335efd58bd5b246977899090449205 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 19:26:00 +0000 Subject: [PATCH 112/136] chore: force-sync .mokogitea/workflows/pre-release.yml [skip ci] --- .mokogitea/workflows/pre-release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index 6e05d96..c70ea7d 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -56,8 +56,8 @@ jobs: id: platform run: | # Read platform from XML manifest ( tag) or plain text fallback - PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/.moko-platform 2>/dev/null | head -1) - [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/.moko-platform 2>/dev/null | tr -d '[:space:]') + 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" MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) -- 2.52.0 From 6d2717430da4a187dfd9137b6d55ee668bf9e0a5 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 19:26:00 +0000 Subject: [PATCH 113/136] chore: force-sync .mokogitea/workflows/repo-health.yml [skip ci] -- 2.52.0 From 8e082a4e79dc99a5b66595dc1fc1d18009ea9f79 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 19:26:01 +0000 Subject: [PATCH 114/136] chore: force-sync .mokogitea/workflows/security-audit.yml [skip ci] -- 2.52.0 From fe6394c6077eb2b1e5362d4f43723cc7a2008267 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 19:26:01 +0000 Subject: [PATCH 115/136] chore: force-sync .mokogitea/workflows/update-server.yml [skip ci] -- 2.52.0 From 378e758f40cc873d71b6be359a850d3cb0f12fcc Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 19:26:01 +0000 Subject: [PATCH 116/136] chore: force-sync .mokogitea/ISSUE_TEMPLATE/adr.md [skip ci] -- 2.52.0 From e6eed3b6fe3905b671ff7f41da6ebfa588133e5f Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 19:26:02 +0000 Subject: [PATCH 117/136] chore: force-sync .mokogitea/ISSUE_TEMPLATE/bug_report.md [skip ci] -- 2.52.0 From 10a2405be34f500c43e161b1a5bef9bf8b873e58 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 19:26:02 +0000 Subject: [PATCH 118/136] chore: force-sync .mokogitea/ISSUE_TEMPLATE/config.yml [skip ci] -- 2.52.0 From b4267a07ac04343c3c4bc73b145f3253427168ac Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 19:26:03 +0000 Subject: [PATCH 119/136] chore: force-sync .mokogitea/ISSUE_TEMPLATE/documentation.md [skip ci] -- 2.52.0 From 96fb10fd19a9bc940291612f15eea83c96c669a0 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 19:26:03 +0000 Subject: [PATCH 120/136] chore: force-sync .mokogitea/ISSUE_TEMPLATE/feature_request.md [skip ci] -- 2.52.0 From 3dc249155a92e04c84ead6b8488a49b44b6ad570 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 19:26:03 +0000 Subject: [PATCH 121/136] chore: force-sync .mokogitea/ISSUE_TEMPLATE/joomla_issue.md [skip ci] -- 2.52.0 From 70b5621fc5562d210dea746bd12034fc74d88282 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 19:26:04 +0000 Subject: [PATCH 122/136] chore: force-sync .mokogitea/ISSUE_TEMPLATE/question.md [skip ci] -- 2.52.0 From 89f39778ff450f75c4d789570f468087ab54ebd8 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 19:26:04 +0000 Subject: [PATCH 123/136] chore: force-sync .mokogitea/ISSUE_TEMPLATE/rfc.md [skip ci] -- 2.52.0 From fe3c69e6f93063b710233f7a93ca87cb5f7f6a01 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 19:26:04 +0000 Subject: [PATCH 124/136] chore: force-sync .mokogitea/ISSUE_TEMPLATE/security.md [skip ci] -- 2.52.0 From 6647977cf51bb435d8bf72a9ef9bf0db20815cd0 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 12 May 2026 19:26:05 +0000 Subject: [PATCH 125/136] chore: force-sync .mokogitea/ISSUE_TEMPLATE/version.md [skip ci] -- 2.52.0 From 85556999720fb8d1b07713c18af9b04728bfff37 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 16 May 2026 09:05:29 -0500 Subject: [PATCH 126/136] chore(ci): version bump targets dev branch instead of main [skip ci] Co-Authored-By: Claude Opus 4.6 (1M context) --- .mokogitea/workflows/auto-release.yml | 37 ++++++++++++++++----------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index 84fc701..dbaf151 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -79,14 +79,16 @@ jobs: - 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:]') + # Parse .manifest.xml via manifest_read.php — outputs all fields to GITHUB_OUTPUT + php /tmp/mokostandards-api/cli/manifest_read.php --path . --github-output 2>/dev/null || true + PLATFORM=$(php /tmp/mokostandards-api/cli/manifest_read.php --path . --field platform 2>/dev/null) [ -z "$PLATFORM" ] && PLATFORM="generic" echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" echo "Platform detected: ${PLATFORM}" + # entry-point from manifest, find as fallback + MOD_FILE=$(php /tmp/mokostandards-api/cli/manifest_read.php --path . --field entry-point 2>/dev/null) + [ -z "$MOD_FILE" ] && MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1) MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) - MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1) echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT" echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT" @@ -185,7 +187,7 @@ jobs: git add -A git diff --cached --quiet || { git commit -m "chore(version): bump ${CURRENT} → ${VERSION} [skip ci]" - git push origin HEAD:main 2>&1 + git push origin HEAD:dev 2>&1 || git push origin HEAD:main 2>&1 } # Override version output for rest of pipeline @@ -939,25 +941,30 @@ jobs: done echo "Cleaned up ${DELETED} pre-release channel(s)" >> $GITHUB_STEP_SUMMARY - # -- STEP 11: Reset dev branch from main ------------------------------------ - - name: "Step 11: Delete and recreate dev branch from main" + # -- STEP 11: Sync dev branch with main + version bump ---------------------- + - name: "Step 11: Merge main into dev (version bump lands on dev)" if: steps.version.outputs.skip != 'true' continue-on-error: true run: | API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" TOKEN="${{ secrets.GA_TOKEN }}" - # Delete dev branch - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch" - - # Recreate dev from main (now includes version bump + changelog promotion) + # Merge main into dev so dev has the release + version bump curl -sf -X POST -H "Authorization: token ${TOKEN}" \ -H "Content-Type: application/json" \ - "${API_BASE}/branches" \ - -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main" + "${API_BASE}/merges" \ + -d "{\"base\":\"dev\",\"head\":\"main\",\"message\":\"chore: sync main into dev after release [skip ci]\"}" \ + 2>/dev/null && echo "Merged main into dev" - echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY + # If dev doesn't exist, create it from main + if [ $? -ne 0 ]; then + curl -sf -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/branches" \ + -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Created dev from main" + fi + + echo "Dev branch synced with main (version bump on dev)" >> $GITHUB_STEP_SUMMARY # -- Dolibarr post-release: Reset dev version ----------------------------- -- 2.52.0 From fe5f56e4b80e5473fb2aaf67ca3f6f8bb924062d Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 16 May 2026 09:11:10 -0500 Subject: [PATCH 127/136] feat(ci): add changelog gate to PR checks [skip ci] Co-Authored-By: Claude Opus 4.6 (1M context) --- .mokogitea/workflows/pr-check.yml | 34 ++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index 99e063f..9290a89 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -108,9 +108,8 @@ jobs: - 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:]') + # Parse manifest for platform detection + PLATFORM=$(php /tmp/mokostandards-api/cli/manifest_read.php --path . --field platform 2>/dev/null) [ -z "$PLATFORM" ] && PLATFORM="generic" echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" @@ -194,3 +193,32 @@ jobs: 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; } + + # ── Changelog Gate ──────────────────────────────────────────────────── + changelog: + name: Changelog Updated + runs-on: ubuntu-latest + if: github.base_ref == 'main' + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check CHANGELOG.md was updated + run: | + BASE="${{ github.event.pull_request.base.sha }}" + HEAD="${{ github.event.pull_request.head.sha }}" + + if git diff --name-only "$BASE" "$HEAD" | grep -q "^CHANGELOG.md$"; then + echo "CHANGELOG.md updated" + else + # Allow [skip changelog] in PR title or body + PR_TITLE="${{ github.event.pull_request.title }}" + PR_BODY="${{ github.event.pull_request.body }}" + if echo "$PR_TITLE $PR_BODY" | grep -qi "\[skip changelog\]"; then + echo "::warning::Changelog skip requested via [skip changelog]" + exit 0 + fi + echo "::error::CHANGELOG.md must be updated before merging to main. Add [skip changelog] to the PR title to bypass." + exit 1 + fi -- 2.52.0 From a45800fd825abc8246bb2847615fb2b71025c073 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 16 May 2026 18:58:39 +0000 Subject: [PATCH 128/136] feat(ci): deploy auto-release to dev [skip ci] --- .gitea/workflows/auto-release.yml | 1034 +++++++++++++++++++++++++++++ 1 file changed, 1034 insertions(+) create mode 100644 .gitea/workflows/auto-release.yml diff --git a/.gitea/workflows/auto-release.yml b/.gitea/workflows/auto-release.yml new file mode 100644 index 0000000..3b254a7 --- /dev/null +++ b/.gitea/workflows/auto-release.yml @@ -0,0 +1,1034 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Release +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# PATH: /templates/workflows/universal/auto-release.yml.template +# VERSION: 05.00.00 +# BRIEF: Universal build & release � detects platform from manifest.xml +# +# +========================================================================+ +# | UNIVERSAL BUILD & RELEASE PIPELINE | +# +========================================================================+ +# | | +# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. | +# | | +# | Platform-specific: | +# | joomla: XML manifest, updates.xml, type-prefixed packages | +# | dolibarr: mod*.class.php, update.txt, dev version reset | +# | generic: README-only, no update stream | +# | | +# +========================================================================+ + +name: "Universal: Build & Release" + +on: + pull_request: + types: [closed] + branches: + - main + paths: + - 'src/**' + - 'htdocs/**' + workflow_dispatch: + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +permissions: + contents: write + +jobs: + release: + name: Build & Release Pipeline + runs-on: release + if: >- + github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.GA_TOKEN }} + fetch-depth: 0 + + - name: Setup MokoStandards tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN }}"}}' + run: | + # Ensure PHP + Composer are available + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \ + /tmp/mokostandards-api 2>/dev/null || true + if [ -d /tmp/mokostandards-api ]; then + cd /tmp/mokostandards-api + composer install --no-dev --no-interaction --quiet 2>/dev/null || true + fi + echo "MokoStandards tools: $([ -d /tmp/mokostandards-api ] && echo 'available' || echo 'unavailable (using fallback)')" + + + # -- PLATFORM DETECTION --------------------------------------------------- + - name: Detect platform + id: platform + run: | + # 1. Try explicit platform from manifest (.mokogitea takes precedence) + if [ -f ".mokogitea/manifest.xml" ]; then + PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1) + elif [ -f ".gitea/manifest.xml" ]; then + PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .gitea/manifest.xml 2>/dev/null | head -1) + fi + + # 2. Auto-sense from file structure if no manifest + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) + CB_MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) + MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1) + + if [ -z "$PLATFORM" ]; then + if [ -n "$CB_MANIFEST" ]; then + PLATFORM="joomla" + MANIFEST="$CB_MANIFEST" + elif [ -n "$MANIFEST" ]; then + PLATFORM="joomla" + elif [ -n "$MOD_FILE" ]; then + PLATFORM="dolibarr" + else + PLATFORM="generic" + fi + fi + + echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" + echo "Platform detected: ${PLATFORM}" + echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT" + echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT" + + # Detect update file methodology + if [ "$PLATFORM" = "joomla" ]; then + echo "update_method=updates.xml" >> "$GITHUB_OUTPUT" + echo "Update method: Joomla updates.xml" + elif [ "$PLATFORM" = "dolibarr" ]; then + echo "update_method=update.txt" >> "$GITHUB_OUTPUT" + echo "Update method: Dolibarr update.txt" + else + echo "update_method=none" >> "$GITHUB_OUTPUT" + echo "Update method: none (generic)" + fi + + # -- STEP 1: Read version ----------------------------------------------- + - name: "Step 1: Read version from README.md" + id: version + run: | + # Try MokoStandards PHP tool first, fall back to direct parsing + VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || true) + if [ -z "$VERSION" ]; then + # Fallback: read from README table format "| **Version** | XX.YY.ZZ |" + VERSION=$(sed -n 's/.*\*\*Version\*\*[[:space:]]*|[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1) + fi + if [ -z "$VERSION" ]; then + # Fallback: read VERSION: XX.YY.ZZ pattern from any file + VERSION=$(grep -rh "VERSION:[[:space:]]*[0-9]" README.md CHANGELOG.md 2>/dev/null | sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' | head -1) + fi + if [ -z "$VERSION" ]; then + echo "No VERSION in README.md — skipping release" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + # Derive major.minor for branch naming (patches update existing branch) + MINOR=$(echo "$VERSION" | awk -F. '{printf "%s.%s", $1, $2}') + PATCH=$(echo "$VERSION" | awk -F. '{print $3}') + + MAJOR=$(echo "$VERSION" | awk -F. '{print $1}') + MINOR_NUM=$(echo "$VERSION" | awk -F. '{print $2}') + + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT" + echo "minor=$MINOR" >> "$GITHUB_OUTPUT" + echo "major=$MAJOR" >> "$GITHUB_OUTPUT" + echo "release_tag=stable" >> "$GITHUB_OUTPUT" + echo "stability=stable" >> "$GITHUB_OUTPUT" + echo "skip=false" >> "$GITHUB_OUTPUT" + if [ "$PATCH" = "00" ] || [ "$PATCH" = "01" ]; then + echo "is_minor=true" >> "$GITHUB_OUTPUT" + echo "Version: $VERSION (first release for this minor — full pipeline)" + else + echo "is_minor=false" >> "$GITHUB_OUTPUT" + echo "Version: $VERSION (patch — platform version + badges only)" + fi + + # -- STEP 1b: Promote CHANGELOG [Unreleased] to current version ----------- + - name: "Step 1b: Promote CHANGELOG for release" + if: steps.version.outputs.skip != 'true' + id: bump + run: | + VERSION="${{ steps.version.outputs.version }}" + TODAY=$(date +%Y-%m-%d) + + # Promote [Unreleased] section in CHANGELOG.md to release version + if [ -f "CHANGELOG.md" ] && grep -qi "Unreleased" CHANGELOG.md; then + sed -i "s|## \[Unreleased\]|## [${VERSION}] --- ${TODAY}|" CHANGELOG.md + sed -i "s|## Unreleased|## [${VERSION}] --- ${TODAY}|" CHANGELOG.md + sed -i "2i ## [Unreleased]" CHANGELOG.md + sed -i "3i \\ " CHANGELOG.md + echo "CHANGELOG promoted to [${VERSION}]" + fi + + # Commit changelog promotion if changed + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git add -A + git diff --cached --quiet || { + git commit -m "chore(release): promote CHANGELOG ${VERSION} [skip ci]" + git push origin HEAD:main 2>&1 + } + + # Pass through version (no bump — release uses version as-is from dev) + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "major=${{ steps.version.outputs.major }}" >> "$GITHUB_OUTPUT" + + - name: Check if already released + if: steps.version.outputs.skip != 'true' + id: check + run: | + TAG="${{ steps.version.outputs.release_tag }}" + BRANCH="${{ steps.version.outputs.branch }}" + + TAG_EXISTS=false + BRANCH_EXISTS=false + + git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true + git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH" && BRANCH_EXISTS=true + + echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT" + echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT" + + # Tag and branch may persist across patch releases — never skip + echo "already_released=false" >> "$GITHUB_OUTPUT" + + # -- SANITY CHECKS ------------------------------------------------------- + - name: "Sanity: Pre-release validation" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + ERRORS=0 + + PLATFORM="${{ steps.platform.outputs.platform }}" + MANIFEST="${{ steps.platform.outputs.manifest }}" + MOD_FILE="${{ steps.platform.outputs.mod_file }}" + echo "## Pre-Release Sanity Checks (${PLATFORM})" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # -- Version drift check (must pass before release) -------- + README_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1) + if [ "$README_VER" != "$VERSION" ]; then + echo "- Version drift: README says \`${README_VER}\` but releasing \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + else + echo "- Version consistent: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + + # Check CHANGELOG version matches + CL_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' CHANGELOG.md 2>/dev/null | head -1) + if [ -n "$CL_VER" ] && [ "$CL_VER" != "$VERSION" ]; then + echo "- CHANGELOG drift: \`${CL_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + fi + + # Check composer.json version if present + if [ -f "composer.json" ]; then + COMP_VER=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' composer.json 2>/dev/null | head -1) + if [ -n "$COMP_VER" ] && [ "$COMP_VER" != "$VERSION" ]; then + echo "- composer.json drift: \`${COMP_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + fi + fi + + # Common checks + if [ ! -f "LICENSE" ]; then + echo "- Missing LICENSE file" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + else + echo "- LICENSE present" >> $GITHUB_STEP_SUMMARY + fi + + if [ ! -d "src" ] && [ ! -d "htdocs" ]; then + echo "- Warning: No src/ or htdocs/ directory" >> $GITHUB_STEP_SUMMARY + else + echo "- Source directory present" >> $GITHUB_STEP_SUMMARY + fi + + # -- Platform-specific checks -------- + case "$PLATFORM" in + joomla) + if [ -n "$MANIFEST" ]; then + XML_VER=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1) + if [ -n "$XML_VER" ] && [ "$XML_VER" != "$VERSION" ]; then + echo "- Manifest drift: \`${XML_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + else + echo "- Manifest version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null) + echo "- Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY + else + echo "- No Joomla XML manifest (WaaS site)" >> $GITHUB_STEP_SUMMARY + fi ;; + dolibarr) + if [ -n "$MOD_FILE" ]; then + MOD_VER=$(sed -n "s/.*\\\$this->version = '\([^']*\)'.*/\1/p" "$MOD_FILE" 2>/dev/null | head -1) + if [ -n "$MOD_VER" ] && [ "$MOD_VER" != "$VERSION" ]; then + echo "- Module drift: \`${MOD_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + else + echo "- Module version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + else + echo "- No mod*.class.php found" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + fi + if [ ! -f "update.txt" ]; then + echo "- Missing update.txt" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + fi ;; + *) echo "- Generic platform � no manifest checks" >> $GITHUB_STEP_SUMMARY ;; + esac + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "$ERRORS" -gt 0 ]; then + echo "**${ERRORS} error(s) — release may be incomplete**" >> $GITHUB_STEP_SUMMARY + else + echo "**All sanity checks passed**" >> $GITHUB_STEP_SUMMARY + fi + + # -- STEP 2: Create or update version/XX.YY archive branch --------------- + # Always runs — every version change on main archives to version/XX.YY + - name: "Step 2: Version archive branch" + if: steps.check.outputs.already_released != 'true' + run: | + BRANCH="${{ steps.version.outputs.branch }}" + IS_MINOR="${{ steps.version.outputs.is_minor }}" + PATCH="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}') + + # Check if branch exists + if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then + git push origin HEAD:"$BRANCH" --force + echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY + else + git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH" + git push origin "$BRANCH" --force + echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY + fi + + # -- STEP 3: Set platform version ---------------------------------------- + - name: "Step 3: Set platform version" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + if [ -f /tmp/mokostandards-api/cli/version_set_platform.php ]; then + php /tmp/mokostandards-api/cli/version_set_platform.php \ + --path . --version "$VERSION" --branch main + else + # Fallback: update version in templateDetails.xml directly + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) + if [ -n "$MANIFEST" ]; then + sed -i "s|[^<]*|${VERSION}|" "$MANIFEST" + sed -i "s|[^<]*|$(date +%Y-%m-%d)|" "$MANIFEST" + fi + # Update README version table + sed -i "s/|\s*\*\*Version\*\*\s*|[^|]*/| **Version** | ${VERSION} /" README.md 2>/dev/null || true + fi + + # -- STEP 4: Update version badges ---------------------------------------- + - name: "Step 4: Update version badges" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + find . -name "*.md" ! -path "./.git/*" ! -path "./vendor/*" | while read -r f; do + if grep -q '\[VERSION:' "$f" 2>/dev/null; then + sed -i "s/\[VERSION:[[:space:]]*[0-9]\{2\}\.[0-9]\{2\}\.[0-9]\{2\}\]/[VERSION: ${VERSION}]/" "$f" + fi + done + + # -- STEP 5: Write updates.xml (Joomla update server) --------------------- + - name: "Step 5: Write update stream" + id: updates + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + REPO="${{ github.repository }}" + + # -- Parse extension metadata from XML manifest ---------------- + # Check for Joomla or Community Builder + MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1) + CB_MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1) + IS_CB="false" + + if [ -z "$MANIFEST" ] && [ -z "$CB_MANIFEST" ]; then + echo "Warning: No Joomla/CB XML manifest found — skipping updates.xml" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + # CB plugin takes precedence if no standard manifest + if [ -z "$MANIFEST" ] && [ -n "$CB_MANIFEST" ]; then + MANIFEST="$CB_MANIFEST" + IS_CB="true" + fi + + # Extract fields using sed (portable — no grep -P) + EXT_NAME=$(sed -n 's/.*\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1) + if [ "$IS_CB" = "true" ]; then + EXT_TYPE="cb_plugin" + EXT_ELEMENT=$(sed -n 's/.*]*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_CLIENT="" + EXT_FOLDER="cb" + else + EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1) + EXT_CLIENT=$(sed -n 's/.*]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_FOLDER=$(sed -n 's/.*]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + fi + TARGET_PLATFORM=$(sed -n 's/.*\(\).*/\1/p' "$MANIFEST" | head -1) + PHP_MINIMUM=$(sed -n 's/.*\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1) + + # If EXT_NAME is a language key (e.g. PLG_SYSTEM_MOKOJGDPC), resolve from .ini + if echo "$EXT_NAME" | grep -qE '^[A-Z_]+$'; then + INI_NAME=$(find . -name "*.sys.ini" -path "*/en-GB/*" -exec grep -h "^${EXT_NAME}=" {} \; 2>/dev/null | head -1 | cut -d'"' -f2) + [ -z "$INI_NAME" ] && INI_NAME=$(find . -name "*.sys.ini" -exec grep -h "^${EXT_NAME}=" {} \; 2>/dev/null | head -1 | cut -d'"' -f2) + [ -n "$INI_NAME" ] && EXT_NAME="$INI_NAME" + fi + + # Fallbacks + [ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}" + [ -z "$EXT_TYPE" ] && EXT_TYPE="component" + + # Derive element if not in manifest: + # 1. plugin="xxx" attribute (plugins) + # 2. module="xxx" attribute (modules) + # 3. XML filename (components, packages) + # 4. Repo name fallback (templates, anything else) + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(sed -n 's/.*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + fi + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(sed -n 's/.*module="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + fi + if [ -z "$EXT_ELEMENT" ]; then + FNAME=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') + # If filename is generic (templateDetails, manifest), use repo name + case "$FNAME" in + templatedetails|manifest) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;; + *) EXT_ELEMENT="$FNAME" ;; + esac + fi + # Final fallback + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + + # Save for Steps 7, 8, 8b + echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" + echo "ext_name=${EXT_NAME}" >> "$GITHUB_OUTPUT" + echo "ext_type=${EXT_TYPE}" >> "$GITHUB_OUTPUT" + echo "ext_folder=${EXT_FOLDER}" >> "$GITHUB_OUTPUT" + + # Build client tag: plugins and frontend modules need site + CLIENT_TAG="" + if [ -n "$EXT_CLIENT" ]; then + CLIENT_TAG="${EXT_CLIENT}" + elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then + CLIENT_TAG="site" + fi + + # Build folder tag for plugins (required for Joomla to match the update) + FOLDER_TAG="" + if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then + FOLDER_TAG="${EXT_FOLDER}" + fi + + # Build targetplatform (fallback to Joomla 5 if not in manifest) + if [ -z "$TARGET_PLATFORM" ]; then + TARGET_PLATFORM=$(printf '' "/") + fi + + # Build php_minimum tag + PHP_TAG="" + if [ -n "$PHP_MINIMUM" ]; then + PHP_TAG="${PHP_MINIMUM}" + fi + + # Build TYPE_PREFIX for download URL + TYPE_PREFIX="" + case "${EXT_TYPE}" in + plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; + module) TYPE_PREFIX="mod_" ;; + component) TYPE_PREFIX="com_" ;; + template) TYPE_PREFIX="tpl_" ;; + library) TYPE_PREFIX="lib_" ;; + package) TYPE_PREFIX="pkg_" ;; + cb_plugin) TYPE_PREFIX="cb_" ;; + esac + + DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/stable/${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip" + INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/stable" + + # -- Build update entry for a given stability tag + build_entry() { + local TAG_NAME="$1" + printf '%s\n' ' ' + printf '%s\n' " ${EXT_NAME}" + printf '%s\n' " ${EXT_NAME} update" + printf '%s\n' " ${EXT_ELEMENT}" + printf '%s\n' " ${EXT_TYPE}" + printf '%s\n' " ${VERSION}" + [ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}" + [ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}" + printf '%s\n' " ${TAG_NAME}" + printf '%s\n' " ${INFO_URL}" + printf '%s\n' ' ' + printf '%s\n' " ${DOWNLOAD_URL}" + printf '%s\n' ' ' + printf '%s\n' " ${TARGET_PLATFORM}" + [ -n "$PHP_TAG" ] && printf '%s\n' " ${PHP_TAG}" + printf '%s\n' ' Moko Consulting' + printf '%s\n' ' https://mokoconsulting.tech' + printf '%s\n' ' ' + } + + # -- Write updates.xml with cascading channels + # Stable release updates ALL channels (development, alpha, beta, rc, stable) + { + printf '%s\n' "" + printf '%s\n' "" + printf '%s\n' "" + printf '%s\n' '' + build_entry "development" + build_entry "alpha" + build_entry "beta" + build_entry "rc" + build_entry "stable" + printf '%s\n' '' + } > updates.xml + + echo "updates.xml: ${VERSION} (all channels updated to stable)" >> $GITHUB_STEP_SUMMARY + + # -- Commit all changes --------------------------------------------------- + - name: Commit release changes + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + if git diff --quiet && git diff --cached --quiet; then + echo "No changes to commit" + exit 0 + fi + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + # Set push URL with token for branch-protected repos + git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git add -A + git commit -m "chore(release): build ${VERSION} [skip ci]" \ + --author="gitea-actions[bot] " + git push -u origin HEAD + + # -- STEP 6: Create tag --------------------------------------------------- + - name: "Step 6: Create git tag" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.tag_exists != 'true' && + steps.version.outputs.is_minor == 'true' + run: | + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + # Only create the major release tag if it doesn't exist yet + if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then + git tag "$RELEASE_TAG" + git push origin "$RELEASE_TAG" + echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY + else + echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY + fi + echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY + + # -- STEP 7: Create or update Gitea Release -------------------------------- + - name: "Step 7: Gitea Release" + if: >- + steps.version.outputs.skip != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + BRANCH="${{ steps.version.outputs.branch }}" + MAJOR="${{ steps.version.outputs.major }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + # Reuse metadata from Step 5 (single source of truth) + EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}" + EXT_NAME="${{ steps.updates.outputs.ext_name }}" + EXT_TYPE="${{ steps.updates.outputs.ext_type }}" + EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}" + + # Fallbacks if Step 5 was skipped + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + fi + [ -z "$EXT_NAME" ] && EXT_NAME="${GITEA_REPO}" + + NOTES=$(php /tmp/mokostandards-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null || true) + [ -z "$NOTES" ] && NOTES="Release ${VERSION}" + + # Build release name: "Pretty Name VERSION (type_element-VERSION)" + TYPE_PREFIX="" + case "${EXT_TYPE}" in + plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; + module) TYPE_PREFIX="mod_" ;; + component) TYPE_PREFIX="com_" ;; + template) TYPE_PREFIX="tpl_" ;; + library) TYPE_PREFIX="lib_" ;; + package) TYPE_PREFIX="pkg_" ;; + cb_plugin) TYPE_PREFIX="cb_" ;; + esac + RELEASE_NAME="${EXT_NAME} ${VERSION} (${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION})" + + # Delete existing release if present (overwrite, not append) + EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true) + EXISTING_ID=$(echo "$EXISTING" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true) + + if [ -n "$EXISTING_ID" ]; then + curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/${EXISTING_ID}" 2>/dev/null || true + curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/tags/${RELEASE_TAG}" 2>/dev/null || true + echo "Deleted previous stable release (id: ${EXISTING_ID})" + fi + + # Create fresh release + curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/releases" \ + -d "$(python3 -c "import json; print(json.dumps({ + 'tag_name': '${RELEASE_TAG}', + 'name': '${RELEASE_NAME}', + 'body': '''## ${VERSION} ($(date +%Y-%m-%d))\n${NOTES}''', + 'target_commitish': '${BRANCH}' + }))")" + echo "Release created: ${RELEASE_NAME}" >> $GITHUB_STEP_SUMMARY + + # -- STEP 8: Build Joomla install ZIP + SHA-256 checksum ------------------ + - name: "Step 8: Build package and update checksum" + if: >- + steps.version.outputs.skip != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + REPO="${{ github.repository }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + # All ZIPs upload to the major release tag (vXX) + RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true) + RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + if [ -z "$RELEASE_ID" ]; then + echo "No release ${RELEASE_TAG} found — skipping ZIP upload" + exit 0 + fi + + # Find extension element name from manifest + MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1 || true) + [ -z "$MANIFEST" ] && exit 0 + + # Reuse element from Step 5, with same fallback chain + EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}" + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1) + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(sed -n 's/.*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null | head -1) + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + fi + # ZIP name: type_folder_element-VERSION (e.g. plg_system_mokojgdpc-01.01.00.zip) + EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_FOLDER=$(sed -n 's/.*]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + TYPE_PREFIX="" + case "${EXT_TYPE}" in + plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; + module) TYPE_PREFIX="mod_" ;; + component) TYPE_PREFIX="com_" ;; + template) TYPE_PREFIX="tpl_" ;; + library) TYPE_PREFIX="lib_" ;; + package) TYPE_PREFIX="pkg_" ;; + cb_plugin) TYPE_PREFIX="cb_" ;; + esac + ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip" + TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz" + + # -- Build install packages from src/ ---------------------------- + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + [ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ — skipping package"; exit 0; } + + EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*" + + # ZIP package + cd "$SOURCE_DIR" + zip -r "/tmp/${ZIP_NAME}" . -x $EXCLUDES + cd .. + + # tar.gz package + tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \ + --exclude='.ftpignore' --exclude='sftp-config*' \ + --exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' . + + ZIP_SIZE=$(stat -c%s "/tmp/${ZIP_NAME}" 2>/dev/null || stat -f%z "/tmp/${ZIP_NAME}" 2>/dev/null || echo "unknown") + TAR_SIZE=$(stat -c%s "/tmp/${TAR_NAME}" 2>/dev/null || stat -f%z "/tmp/${TAR_NAME}" 2>/dev/null || echo "unknown") + + # -- Calculate SHA-256 for both ---------------------------------- + SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1) + SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1) + + # -- Delete existing assets with same name before uploading ------ + ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]") + for ASSET_NAME in "$ZIP_NAME" "$TAR_NAME"; do + ASSET_ID=$(echo "$ASSETS" | python3 -c " + import sys,json + assets = json.load(sys.stdin) + for a in assets: + if a['name'] == '${ASSET_NAME}': + print(a['id']); break + " 2>/dev/null || true) + if [ -n "$ASSET_ID" ]; then + curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true + fi + done + + # -- Upload both to release tag ---------------------------------- + curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @"/tmp/${ZIP_NAME}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" > /dev/null 2>&1 || true + + curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @"/tmp/${TAR_NAME}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets?name=${TAR_NAME}" > /dev/null 2>&1 || true + + # -- Update updates.xml with both download formats --------------- + if [ -f "updates.xml" ]; then + ZIP_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}" + TAR_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${TAR_NAME}" + + # Use Python to update only the stable entry's downloads + sha256 + export PY_ZIP_URL="$ZIP_URL" PY_TAR_URL="$TAR_URL" PY_SHA="$SHA256_ZIP" + python3 << 'PYEOF' + import re, os + + with open("updates.xml") as f: + content = f.read() + + zip_url = os.environ["PY_ZIP_URL"] + tar_url = os.environ["PY_TAR_URL"] + sha = os.environ["PY_SHA"] + + # Find the stable update block and replace its downloads + sha256 + def replace_stable(m): + block = m.group(0) + # Replace downloads block + new_downloads = ( + " \n" + f" {zip_url}\n" + " " + ) + block = re.sub(r' .*?', new_downloads, block, flags=re.DOTALL) + # Add or replace sha256 + if '' in block: + block = re.sub(r' .*?', f' {sha}', block) + else: + block = block.replace('', f'\n {sha}') + return block + + content = re.sub( + r' .*?stable.*?', + replace_stable, + content, + flags=re.DOTALL + ) + + with open("updates.xml", "w") as f: + f.write(content) + PYEOF + + CURRENT_BRANCH="${{ github.ref_name }}" + git add updates.xml + git commit -m "chore(release): ZIP + tar.gz for ${VERSION} [skip ci]" \ + --author="gitea-actions[bot] " || true + git push || true + + # Sync updates.xml to main via direct API (always runs — may be on version/XX branch) + GA_TOKEN="${{ secrets.GA_TOKEN }}" + API="${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}" + + FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \ + "${API}/contents/updates.xml?ref=main" | jq -r '.sha // empty') + + if [ -n "$FILE_SHA" ]; then + CONTENT=$(base64 -w0 updates.xml) + curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/contents/updates.xml" \ + -d "$(jq -n \ + --arg content "$CONTENT" \ + --arg sha "$FILE_SHA" \ + --arg msg "chore: sync updates.xml ${VERSION} [skip ci]" \ + --arg branch "main" \ + '{content: $content, sha: $sha, message: $msg, branch: $branch}' + )" > /dev/null 2>&1 \ + && echo "updates.xml synced to main via API" \ + || echo "WARNING: failed to sync updates.xml to main" + else + echo "WARNING: could not get updates.xml SHA from main" + fi + fi + + echo "### Packages" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Package | Size | SHA-256 |" >> $GITHUB_STEP_SUMMARY + echo "|---------|------|---------|" >> $GITHUB_STEP_SUMMARY + echo "| \`${ZIP_NAME}\` | ${ZIP_SIZE} | \`${SHA256_ZIP}\` |" >> $GITHUB_STEP_SUMMARY + echo "| \`${TAR_NAME}\` | ${TAR_SIZE} | \`${SHA256_TAR}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Release | \`${RELEASE_TAG}\` | |" >> $GITHUB_STEP_SUMMARY + echo "| Download | [${ZIP_NAME}](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}) |" >> $GITHUB_STEP_SUMMARY + + # -- STEP 8b: Update release description with changelog + SHA ---------------- + - name: "Step 8b: Update release body with changelog and SHA" + if: steps.version.outputs.skip != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}" + EXT_TYPE="${{ steps.updates.outputs.ext_type }}" + EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}" + + # Build TYPE_PREFIX to match Step 8's ZIP naming + TYPE_PREFIX="" + case "${EXT_TYPE}" in + plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; + module) TYPE_PREFIX="mod_" ;; + component) TYPE_PREFIX="com_" ;; + template) TYPE_PREFIX="tpl_" ;; + library) TYPE_PREFIX="lib_" ;; + package) TYPE_PREFIX="pkg_" ;; + cb_plugin) TYPE_PREFIX="cb_" ;; + esac + ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip" + TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz" + + # Get SHA from the built files + SHA256_ZIP="" + [ -f "/tmp/${ZIP_NAME}" ] && SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1) + SHA256_TAR="" + [ -f "/tmp/${TAR_NAME}" ] && SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1) + + # Extract latest changelog entry (strip the ## header to avoid duplicate) + CHANGELOG="" + if [ -f "CHANGELOG.md" ]; then + CHANGELOG=$(sed -n "/^## \[*${VERSION}/,/^## \[*[0-9]/p" CHANGELOG.md | sed '$d' | sed '1d') + [ -z "$CHANGELOG" ] && CHANGELOG=$(sed -n '/^## /,/^## /p' CHANGELOG.md | sed '$d' | sed '1d' | head -30) + fi + + # Build release body (single header, no duplicate from changelog) + BODY="## ${VERSION} ($(date +%Y-%m-%d))\n\n" + if [ -n "$CHANGELOG" ]; then + BODY="${BODY}${CHANGELOG}\n\n" + fi + BODY="${BODY}---\n\n### Checksums\n\n" + BODY="${BODY}| File | SHA-256 |\n|------|--------|\n" + [ -n "$SHA256_ZIP" ] && BODY="${BODY}| \`${ZIP_NAME}\` | \`${SHA256_ZIP}\` |\n" + [ -n "$SHA256_TAR" ] && BODY="${BODY}| \`${TAR_NAME}\` | \`${SHA256_TAR}\` |\n" + + # Get release ID and update body + RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null | \ + python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + + if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then + python3 -c " + import json, urllib.request + body = '''$(printf '%b' "$BODY")''' + data = json.dumps({'body': body}).encode() + req = urllib.request.Request( + '${API_BASE}/releases/${RELEASE_ID}', + data=data, + headers={'Authorization': 'token ${{ secrets.GA_TOKEN }}', 'Content-Type': 'application/json'}, + method='PATCH' + ) + urllib.request.urlopen(req) + " 2>/dev/null && echo "Release body updated with changelog + SHA" >> $GITHUB_STEP_SUMMARY + fi + + # -- (GitHub mirror removed — Gitea is the sole release platform) ---------- + + # -- Clean up lesser pre-releases (cascade) --------------------------------- + # stable → deletes all | rc → beta,alpha,dev | beta → alpha,dev | alpha → dev + - name: "Delete lesser pre-release channels" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.GA_TOKEN }}" + + # Stable deletes all pre-release channels + TAGS_TO_DELETE="development alpha beta release-candidate" + + DELETED=0 + for TAG in $TAGS_TO_DELETE; do + RELEASE_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/releases/tags/${TAG}" 2>/dev/null | \ + python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + + if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then + curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/releases/${RELEASE_ID}" 2>/dev/null || true + curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/tags/${TAG}" 2>/dev/null || true + echo "Deleted: ${TAG} (id: ${RELEASE_ID})" + DELETED=$((DELETED + 1)) + fi + done + echo "Cleaned up ${DELETED} pre-release channel(s)" >> $GITHUB_STEP_SUMMARY + + # -- STEP 11: Reset dev branch and bump to next minor ------------------------- + - name: "Step 11: Reset dev and bump to next minor" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.GA_TOKEN }}" + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + PLATFORM="${{ steps.platform.outputs.platform }}" + + # Delete dev branch + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch" + + # Recreate dev from main + curl -sf -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/branches" \ + -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main" + + # Calculate next minor version for dev + MAJOR=$((10#$(echo "$VERSION" | cut -d. -f1))) + MINOR=$((10#$(echo "$VERSION" | cut -d. -f2))) + MINOR=$((MINOR + 1)) + if [ $MINOR -gt 99 ]; then + MINOR=0 + MAJOR=$((MAJOR + 1)) + fi + NEXT=$(printf "%02d.%02d.00" $MAJOR $MINOR) + + # Bump version on dev via API (README + manifest) + # Update README.md on dev + README_RESP=$(curl -sf -H "Authorization: token ${TOKEN}" "${API_BASE}/contents/README.md?ref=dev" 2>/dev/null || true) + README_SHA=$(echo "$README_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true) + README_CONTENT=$(echo "$README_RESP" | python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin).get('content','')).decode())" 2>/dev/null || true) + if [ -n "$README_SHA" ] && [ -n "$README_CONTENT" ]; then + UPDATED=$(echo "$README_CONTENT" | sed "s/${VERSION}/${NEXT}/g") + ENCODED=$(echo "$UPDATED" | base64 -w0) + curl -sf -X PUT -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" \ + "${API_BASE}/contents/README.md" \ + -d "$(python3 -c "import json; print(json.dumps({'content':'${ENCODED}','sha':'${README_SHA}','message':'chore(version): bump ${VERSION} → ${NEXT} (dev) [skip ci]','branch':'dev'}))")" > /dev/null 2>&1 || true + fi + + # Update manifest on dev (Joomla or Dolibarr) + case "$PLATFORM" in + joomla) + MANIFEST_PATH="${{ steps.platform.outputs.manifest }}" + [ -n "$MANIFEST_PATH" ] && MANIFEST_PATH=$(echo "$MANIFEST_PATH" | sed 's|^\./||') + if [ -n "$MANIFEST_PATH" ]; then + ENCODED_PATH=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${MANIFEST_PATH}'))") + MF_RESP=$(curl -sf -H "Authorization: token ${TOKEN}" "${API_BASE}/contents/${ENCODED_PATH}?ref=dev" 2>/dev/null || true) + MF_SHA=$(echo "$MF_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true) + MF_CONTENT=$(echo "$MF_RESP" | python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin).get('content','')).decode())" 2>/dev/null || true) + if [ -n "$MF_SHA" ] && [ -n "$MF_CONTENT" ]; then + UPDATED=$(echo "$MF_CONTENT" | sed "s|${VERSION}|${NEXT}|") + ENCODED=$(echo "$UPDATED" | base64 -w0) + curl -sf -X PUT -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" \ + "${API_BASE}/contents/${ENCODED_PATH}" \ + -d "$(python3 -c "import json; print(json.dumps({'content':'${ENCODED}','sha':'${MF_SHA}','message':'chore(version): bump ${VERSION} → ${NEXT} (dev) [skip ci]','branch':'dev'}))")" > /dev/null 2>&1 || true + fi + fi + ;; + dolibarr) + # Dolibarr handled by separate step below + ;; + esac + + echo "Dev branch bumped to ${NEXT}" >> $GITHUB_STEP_SUMMARY + + + # -- Dolibarr post-release: Reset dev version ----------------------------- + - name: "Dolibarr: Reset dev version" + if: >- + steps.version.outputs.skip != 'true' && + steps.platform.outputs.platform == 'dolibarr' && + steps.platform.outputs.mod_file != '' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.GA_TOKEN }}" + MOD_FILE="${{ steps.platform.outputs.mod_file }}" + ENCODED_PATH=$(echo "$MOD_FILE" | sed 's|^\./||' | python3 -c "import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip()))") + FILE_RESP=$(curl -sf -H "Authorization: token ${TOKEN}" "${API_BASE}/contents/${ENCODED_PATH}?ref=dev" 2>/dev/null || true) + FILE_SHA=$(echo "$FILE_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true) + FILE_CONTENT=$(echo "$FILE_RESP" | python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin).get('content','')).decode())" 2>/dev/null || true) + if [ -n "$FILE_SHA" ] && [ -n "$FILE_CONTENT" ]; then + UPDATED=$(echo "$FILE_CONTENT" | sed "s/\$this->version = '[^']*'/\$this->version = 'development'/") + ENCODED=$(echo "$UPDATED" | base64 -w0) + curl -sf -X PUT -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/contents/${ENCODED_PATH}" \ + -d "$(jq -n --arg content \"$ENCODED\" --arg sha \"$FILE_SHA\" --arg msg \"chore(version): reset dev version [skip ci]\" --arg branch \"dev\" '{content:$content,sha:$sha,message:$msg,branch:$branch}')" > /dev/null 2>&1 || true + fi + + # -- Summary -------------------------------------------------------------- + - name: Pipeline Summary + if: always() + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + PLATFORM="${{ steps.platform.outputs.platform }}" + if [ "${{ steps.version.outputs.skip }}" = "true" ]; then + echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY + echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY + elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then + echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY + echo "|------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY + fi -- 2.52.0 From 761915d5408e978a41ee4afe5882355b4f425927 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 16 May 2026 14:32:57 -0500 Subject: [PATCH 129/136] feat(template): scaffold Joomla 5 template structure Convert repo from empty module scaffold to a complete Joomla 5 site template with manifest, index/error/offline/component pages, Web Asset Manager config, CSS custom-property stylesheet, language files, and 11 module positions including a hero section. Co-Authored-By: Claude Opus 4.6 (1M context) --- Makefile | 4 +- README.md | 80 +++-- composer.json | 33 +- src/!.gitkeep | 0 src/component.php | 39 +++ src/css/template.css | 330 ++++++++++++++++++++ src/error.php | 88 ++++++ src/html/index.html | 1 + src/images/index.html | 1 + src/index.php | 181 +++++++++++ src/joomla.asset.json | 32 ++ src/js/template.js | 35 +++ src/language/en-GB/tpl_mokojoomhero.ini | 13 + src/language/en-GB/tpl_mokojoomhero.sys.ini | 25 ++ src/offline.php | 134 ++++++++ src/templateDetails.xml | 129 ++++++++ update.xml | 10 +- updates.xml | 10 +- 18 files changed, 1113 insertions(+), 32 deletions(-) delete mode 100644 src/!.gitkeep create mode 100644 src/component.php create mode 100644 src/css/template.css create mode 100644 src/error.php create mode 100644 src/html/index.html create mode 100644 src/images/index.html create mode 100644 src/index.php create mode 100644 src/joomla.asset.json create mode 100644 src/js/template.js create mode 100644 src/language/en-GB/tpl_mokojoomhero.ini create mode 100644 src/language/en-GB/tpl_mokojoomhero.sys.ini create mode 100644 src/offline.php create mode 100644 src/templateDetails.xml diff --git a/Makefile b/Makefile index ec94cc9..ac012bb 100644 --- a/Makefile +++ b/Makefile @@ -12,8 +12,8 @@ # ============================================================================== # Extension Configuration -EXTENSION_NAME := mokoexample -EXTENSION_TYPE := module +EXTENSION_NAME := mokojoomhero +EXTENSION_TYPE := template # Options: module, plugin, component, package, template EXTENSION_VERSION := 1.0.0 diff --git a/README.md b/README.md index a2552af..cd734e3 100644 --- a/README.md +++ b/README.md @@ -13,37 +13,41 @@ # MokoJoomHero -A Joomla Module developed following MokoStandards. +A Joomla 5 site template developed following MokoStandards. [![License](https://img.shields.io/badge/license-GPL--3.0--or--later-blue.svg)](LICENSE) ## Overview -This is a standardized Joomla module that conforms to Moko Consulting's development standards and best practices. It provides a consistent starting point with pre-configured tooling, documentation structure, and development workflows. +MokoJoomHero is a modern, accessible Joomla 5 template by Moko Consulting. It features a prominent hero section, flexible module positions, sticky header, customisable brand colour, and responsive layout — all built on Joomla's Web Asset Manager. ## Features -- **Standardized Structure**: Pre-organized directories for source code, documentation, and scripts -- **Build Automation**: Makefile with common tasks (lint, build, package, install) -- **Code Quality**: Pre-configured PHP linting and CodeSniffer for Joomla standards -- **Development Tools**: EditorConfig for consistent coding styles across IDEs -- **Documentation**: Template structure following MokoStandards documentation practices -- **Git Configuration**: Pre-configured git attributes, ignore patterns, and commit message templates +- **Hero Section**: Dedicated `hero` module position with gradient styling +- **Flexible Layout**: 11 module positions including sidebars, topbar, banner, and footer +- **Sticky Header**: Optional sticky navigation with smooth scroll +- **Brand Customisation**: Configurable brand colour, logo, and tagline via Template Manager +- **Responsive**: Mobile-first layout with CSS custom properties +- **Accessibility**: Semantic HTML5, ARIA landmarks, skip-to-content support +- **Web Asset Manager**: Modern asset loading via `joomla.asset.json` +- **Error & Offline Pages**: Custom styled error and offline templates ## Prerequisites -- **PHP**: 7.4 or higher (8.0+ recommended for Joomla 4.x/5.x) -- **Joomla**: A working Joomla installation for testing (3.x, 4.x, or 5.x) -- **Make**: GNU Make for running build commands +- **PHP**: 8.1 or higher +- **Joomla**: 5.x (also compatible with 6.x) +- **Make**: GNU Make for build automation ## Installation -1. Build the module package: +1. Build the template package: ```bash make build ``` -2. Upload the generated ZIP file via Joomla's Extension Manager +2. Upload the generated `tpl_mokojoomhero-1.0.0.zip` via Joomla's Extension Manager + +3. Set as default template in **System → Site Template Styles** ## Usage @@ -54,19 +58,57 @@ make help # Validate code make validate -# Build module package +# Build template package make build ``` +## Module Positions + +| Position | Purpose | +|----------|---------| +| `topbar` | Slim bar above the header (contact info, social links) | +| `banner` | Full-width banner below the header | +| `menu` | Main navigation inside the header | +| `hero` | Hero section with gradient background | +| `breadcrumbs` | Breadcrumb trail | +| `sidebar-left` | Left sidebar | +| `sidebar-right` | Right sidebar | +| `main-top` | Above the main content area | +| `main-bottom` | Below the main content area | +| `footer` | Footer area | +| `debug` | Debug output (hidden from users) | + +## Template Parameters + +Configure via **System → Site Template Styles → MokoJoomHero**: + +- **Logo** — Upload a site logo image +- **Site Description** — Tagline displayed next to the logo +- **Brand Colour** — Primary accent colour (default: `#1a73e8`) +- **Fluid Container** — Toggle full-width vs fixed-width layout +- **Sticky Header** — Keep the header visible on scroll +- **Back to Top** — Floating scroll-to-top button + ## Project Structure ``` . -├── docs/ # Documentation files -├── scripts/ # Build and deployment scripts -├── src/ # Module source code -├── Makefile # Build automation -└── README.md # This file +├── docs/ # Documentation files +├── scripts/ # Build and deployment scripts +├── src/ # Template source code +│ ├── css/template.css # Main stylesheet +│ ├── js/template.js # Main JavaScript +│ ├── images/ # Template images +│ ├── html/ # Template overrides +│ ├── language/en-GB/ # Language files +│ ├── templateDetails.xml # Joomla manifest +│ ├── joomla.asset.json # Web Asset Manager config +│ ├── index.php # Main template +│ ├── error.php # Error page +│ ├── offline.php # Offline page +│ └── component.php # Component-only output +├── Makefile # Build automation +└── README.md # This file ``` ## Contributing diff --git a/composer.json b/composer.json index 8b13789..6f30f8b 100644 --- a/composer.json +++ b/composer.json @@ -1 +1,32 @@ - +{ + "name": "mokoconsulting/tpl-mokojoomhero", + "description": "MokoJoomHero — Modern Joomla 5 template by Moko Consulting", + "type": "joomla-template", + "license": "GPL-3.0-or-later", + "authors": [ + { + "name": "Moko Consulting", + "email": "hello@mokoconsulting.tech", + "homepage": "https://mokoconsulting.tech" + } + ], + "require": { + "php": ">=8.1" + }, + "require-dev": { + "squizlabs/php_codesniffer": "^3.7", + "joomla/coding-standards": "^3.0", + "phpstan/phpstan": "^1.10" + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/mokoconsulting-tech/MokoStandards" + } + ], + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } + } +} diff --git a/src/!.gitkeep b/src/!.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/component.php b/src/component.php new file mode 100644 index 0000000..3895ecb --- /dev/null +++ b/src/component.php @@ -0,0 +1,39 @@ + + * + * This file is part of a Moko Consulting project. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: MokoJoomHero.Template + * INGROUP: MokoJoomHero + * REPO: https://github.com/mokoconsulting-tech/MokoJoomHero + * PATH: /src/component.php + * VERSION: 01.00.01 + * BRIEF: Component-only output — used when ?tmpl=component strips all template chrome + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Document\HtmlDocument; + +/** @var HtmlDocument $this */ + +$wa = $this->getWebAssetManager(); +$wa->usePreset('template.tpl_mokojoomhero'); + +?> + + + + + + + + + + + + + diff --git a/src/css/template.css b/src/css/template.css new file mode 100644 index 0000000..3397338 --- /dev/null +++ b/src/css/template.css @@ -0,0 +1,330 @@ +/* Copyright (C) 2026 Moko Consulting + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: MokoJoomHero.Template.Assets + * INGROUP: MokoJoomHero.Template + * REPO: https://github.com/mokoconsulting-tech/MokoJoomHero + * PATH: /src/css/template.css + * VERSION: 01.00.01 + * BRIEF: Main template stylesheet — base layout, typography, and module chrome + */ + +/* ============================================================ + CSS Custom Properties + ============================================================ */ +:root { + --brand-color: #1a73e8; + --brand-color-dark: #1557b0; + --text-color: #333; + --text-muted: #666; + --bg-color: #fff; + --bg-light: #f8f9fa; + --border-color: #dee2e6; + --header-height: 64px; + --sidebar-width: 260px; + --container-max: 1200px; + --spacing-xs: 0.25rem; + --spacing-sm: 0.5rem; + --spacing-md: 1rem; + --spacing-lg: 2rem; + --spacing-xl: 3rem; + --radius: 6px; + --shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + --transition: 0.2s ease; +} + +/* ============================================================ + Reset & Base + ============================================================ */ +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + font-size: 1rem; + line-height: 1.6; + color: var(--text-color); + background-color: var(--bg-color); +} + +img { + max-width: 100%; + height: auto; +} + +a { + color: var(--brand-color); + text-decoration: none; +} + +a:hover { + color: var(--brand-color-dark); + text-decoration: underline; +} + +/* ============================================================ + Container + ============================================================ */ +.container { + width: 100%; + max-width: var(--container-max); + margin-inline: auto; + padding-inline: var(--spacing-md); +} + +.container-fluid { + width: 100%; + padding-inline: var(--spacing-md); +} + +/* ============================================================ + Header + ============================================================ */ +.site-header { + background: var(--bg-color); + border-bottom: 1px solid var(--border-color); + z-index: 100; +} + +.site-header.sticky-header { + position: sticky; + top: 0; +} + +.header-inner { + display: flex; + align-items: center; + justify-content: space-between; + min-height: var(--header-height); + gap: var(--spacing-md); +} + +.site-brand a { + display: flex; + align-items: center; + gap: var(--spacing-sm); + text-decoration: none; +} + +.site-logo { + height: 40px; + width: auto; +} + +.site-title { + font-size: 1.25rem; + font-weight: 700; + color: var(--text-color); +} + +.site-description { + font-size: 0.875rem; + color: var(--text-muted); + margin: 0; +} + +/* ============================================================ + Navigation + ============================================================ */ +.site-navigation ul { + list-style: none; + margin: 0; + padding: 0; + display: flex; + gap: var(--spacing-sm); +} + +.site-navigation a { + display: block; + padding: var(--spacing-sm) var(--spacing-md); + color: var(--text-color); + border-radius: var(--radius); + transition: background var(--transition); +} + +.site-navigation a:hover, +.site-navigation a.active { + background: var(--bg-light); + text-decoration: none; + color: var(--brand-color); +} + +/* ============================================================ + Topbar + ============================================================ */ +.topbar { + background: var(--bg-light); + border-bottom: 1px solid var(--border-color); + font-size: 0.875rem; + padding: var(--spacing-xs) 0; +} + +/* ============================================================ + Hero Section + ============================================================ */ +.hero-section { + padding: var(--spacing-xl) 0; + background: linear-gradient(135deg, var(--brand-color) 0%, var(--brand-color-dark) 100%); + color: #fff; +} + +.hero-section h1, +.hero-section h2 { + color: #fff; +} + +/* ============================================================ + Breadcrumbs + ============================================================ */ +.breadcrumbs-area { + padding: var(--spacing-sm) 0; + background: var(--bg-light); + border-bottom: 1px solid var(--border-color); + font-size: 0.875rem; +} + +/* ============================================================ + Main Content Layout + ============================================================ */ +.site-main { + padding: var(--spacing-lg) 0; + min-height: 50vh; +} + +.content-area { + display: flex; + gap: var(--spacing-lg); +} + +.content-body { + flex: 1; + min-width: 0; +} + +.sidebar { + flex: 0 0 var(--sidebar-width); +} + +/* ============================================================ + Module Chrome — card style + ============================================================ */ +.card { + background: var(--bg-color); + border: 1px solid var(--border-color); + border-radius: var(--radius); + box-shadow: var(--shadow); + margin-bottom: var(--spacing-md); + overflow: hidden; +} + +.card .card-header { + padding: var(--spacing-md); + border-bottom: 1px solid var(--border-color); + font-weight: 600; +} + +.card .card-body { + padding: var(--spacing-md); +} + +/* ============================================================ + Footer + ============================================================ */ +.site-footer { + background: var(--bg-light); + border-top: 1px solid var(--border-color); + padding: var(--spacing-lg) 0; + margin-top: var(--spacing-xl); +} + +.footer-copyright { + text-align: center; + font-size: 0.875rem; + color: var(--text-muted); +} + +.footer-copyright p { + margin: 0; +} + +/* ============================================================ + Back to Top + ============================================================ */ +.back-to-top { + position: fixed; + bottom: 2rem; + right: 2rem; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + background: var(--brand-color); + color: #fff; + border-radius: 50%; + text-decoration: none; + box-shadow: var(--shadow); + opacity: 0; + visibility: hidden; + transition: opacity var(--transition), visibility var(--transition); + z-index: 99; +} + +.back-to-top.visible { + opacity: 1; + visibility: visible; +} + +.back-to-top:hover { + background: var(--brand-color-dark); + color: #fff; + text-decoration: none; +} + +/* ============================================================ + System Messages + ============================================================ */ +.alert { + padding: var(--spacing-md); + margin-bottom: var(--spacing-md); + border: 1px solid transparent; + border-radius: var(--radius); +} + +.alert-message { background: #d1ecf1; border-color: #bee5eb; color: #0c5460; } +.alert-warning { background: #fff3cd; border-color: #ffeeba; color: #856404; } +.alert-error { background: #f8d7da; border-color: #f5c6cb; color: #721c24; } + +/* ============================================================ + Responsive + ============================================================ */ +@media (max-width: 768px) { + .header-inner { + flex-direction: column; + align-items: flex-start; + padding: var(--spacing-sm) 0; + } + + .site-navigation ul { + flex-direction: column; + width: 100%; + } + + .content-area { + flex-direction: column; + } + + .sidebar { + flex: 1 1 auto; + } +} diff --git a/src/error.php b/src/error.php new file mode 100644 index 0000000..9cbb9a5 --- /dev/null +++ b/src/error.php @@ -0,0 +1,88 @@ + + * + * This file is part of a Moko Consulting project. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: MokoJoomHero.Template + * INGROUP: MokoJoomHero + * REPO: https://github.com/mokoconsulting-tech/MokoJoomHero + * PATH: /src/error.php + * VERSION: 01.00.01 + * BRIEF: Error page template — displayed for 403, 404, 500, etc. + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Uri\Uri; + +/** @var Joomla\CMS\Document\ErrorDocument $this */ + +$app = Factory::getApplication(); + +// Error details +$error_code = $this->error->getCode(); +$error_message = $this->error->getMessage(); + +if (!in_array($error_code, [400, 401, 403, 404, 500])) { + $error_code = 500; +} + +?> + + + + + + <?php echo $error_code; ?> - <?php echo htmlspecialchars($error_message, ENT_QUOTES, 'UTF-8'); ?> + + + + +
+

+

+ + + +
+ + diff --git a/src/html/index.html b/src/html/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/src/html/index.html @@ -0,0 +1 @@ + diff --git a/src/images/index.html b/src/images/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/src/images/index.html @@ -0,0 +1 @@ + diff --git a/src/index.php b/src/index.php new file mode 100644 index 0000000..603a111 --- /dev/null +++ b/src/index.php @@ -0,0 +1,181 @@ + + * + * This file is part of a Moko Consulting project. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: MokoJoomHero.Template + * INGROUP: MokoJoomHero + * REPO: https://github.com/mokoconsulting-tech/MokoJoomHero + * PATH: /src/index.php + * VERSION: 01.00.01 + * BRIEF: Main template entry point — renders the full HTML page + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Document\HtmlDocument; +use Joomla\CMS\Factory; +use Joomla\CMS\HTML\HTMLHelper; +use Joomla\CMS\Uri\Uri; + +/** @var HtmlDocument $this */ + +$app = Factory::getApplication(); +$wa = $this->getWebAssetManager(); +$params = $app->getTemplate(true)->params; + +// Register and use template assets via Web Asset Manager +$wa->usePreset('template.tpl_mokojoomhero'); + +// Template parameters +$logo = $params->get('logoFile', ''); +$site_description = $params->get('siteDescription', ''); +$brand_color = $params->get('brandColor', '#1a73e8'); +$fluid = (bool) $params->get('fluidContainer', 0); +$sticky_header = (bool) $params->get('stickyHeader', 1); +$back_to_top = (bool) $params->get('backToTop', 1); + +$container_class = $fluid ? 'container-fluid' : 'container'; + +// Inject brand color as CSS custom property +$this->addHeadStyle(':root { --brand-color: ' . htmlspecialchars($brand_color, ENT_QUOTES, 'UTF-8') . '; }'); + +?> + + + + + + + + + + + countModules('topbar')) : ?> +
+
+ +
+
+ + + +
+
+
+ + + + + countModules('menu')) : ?> + + +
+
+
+ + + countModules('banner')) : ?> + + + + + countModules('hero')) : ?> +
+
+ +
+
+ + + + countModules('breadcrumbs')) : ?> + + + + +
+
+ countModules('main-top')) : ?> +
+ +
+ + +
+ + countModules('sidebar-left')) : ?> + + + + +
+ + +
+ + + countModules('sidebar-right')) : ?> + + +
+ + countModules('main-bottom')) : ?> +
+ +
+ +
+
+ + +
+
+ countModules('footer')) : ?> + + + +
+
+ + + + + + + + + + + + diff --git a/src/joomla.asset.json b/src/joomla.asset.json new file mode 100644 index 0000000..6bb190c --- /dev/null +++ b/src/joomla.asset.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://developer.joomla.org/schemas/json-schema/web_assets.json", + "name": "tpl_mokojoomhero", + "version": "1.0.0", + "description": "MokoJoomHero template web assets", + "license": "GPL-3.0-or-later", + "assets": [ + { + "name": "template.tpl_mokojoomhero", + "type": "preset", + "dependencies": [ + "template.tpl_mokojoomhero.css", + "template.tpl_mokojoomhero.js" + ] + }, + { + "name": "template.tpl_mokojoomhero.css", + "type": "style", + "uri": "css/template.css", + "version": "1.0.0" + }, + { + "name": "template.tpl_mokojoomhero.js", + "type": "script", + "uri": "js/template.js", + "version": "1.0.0", + "attributes": { + "defer": true + } + } + ] +} diff --git a/src/js/template.js b/src/js/template.js new file mode 100644 index 0000000..7ec6480 --- /dev/null +++ b/src/js/template.js @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2026 Moko Consulting + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: MokoJoomHero.Template.Assets + * INGROUP: MokoJoomHero.Template + * REPO: https://github.com/mokoconsulting-tech/MokoJoomHero + * PATH: /src/js/template.js + * VERSION: 01.00.01 + * BRIEF: Main template JavaScript — back-to-top toggle and lightweight UI helpers + */ + +'use strict'; + +document.addEventListener('DOMContentLoaded', function () { + // Back-to-top button visibility toggle + var backToTop = document.querySelector('.back-to-top'); + + if (backToTop) { + window.addEventListener('scroll', function () { + if (window.scrollY > 300) { + backToTop.classList.add('visible'); + } else { + backToTop.classList.remove('visible'); + } + }, { passive: true }); + + backToTop.addEventListener('click', function (e) { + e.preventDefault(); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }); + } +}); diff --git a/src/language/en-GB/tpl_mokojoomhero.ini b/src/language/en-GB/tpl_mokojoomhero.ini new file mode 100644 index 0000000..79af3f4 --- /dev/null +++ b/src/language/en-GB/tpl_mokojoomhero.ini @@ -0,0 +1,13 @@ +; Copyright (C) 2026 Moko Consulting +; SPDX-License-Identifier: GPL-3.0-or-later +; +; FILE INFORMATION +; DEFGROUP: MokoJoomHero.Template.Language +; INGROUP: MokoJoomHero.Template +; REPO: https://github.com/mokoconsulting-tech/MokoJoomHero +; PATH: /src/language/en-GB/tpl_mokojoomhero.ini +; VERSION: 01.00.01 +; BRIEF: Front-end language strings for MokoJoomHero template + +TPL_MOKOJOOMHERO_MAIN_NAV="Main Navigation" +TPL_MOKOJOOMHERO_BACK_TO_TOP="Back to top" diff --git a/src/language/en-GB/tpl_mokojoomhero.sys.ini b/src/language/en-GB/tpl_mokojoomhero.sys.ini new file mode 100644 index 0000000..eaca64e --- /dev/null +++ b/src/language/en-GB/tpl_mokojoomhero.sys.ini @@ -0,0 +1,25 @@ +; Copyright (C) 2026 Moko Consulting +; SPDX-License-Identifier: GPL-3.0-or-later +; +; FILE INFORMATION +; DEFGROUP: MokoJoomHero.Template.Language +; INGROUP: MokoJoomHero.Template +; REPO: https://github.com/mokoconsulting-tech/MokoJoomHero +; PATH: /src/language/en-GB/tpl_mokojoomhero.sys.ini +; VERSION: 01.00.01 +; BRIEF: System language strings — used in admin Extension Manager and Template Manager + +TPL_MOKOJOOMHERO="MokoJoomHero" +TPL_MOKOJOOMHERO_DESCRIPTION="A modern, accessible Joomla template by Moko Consulting. Features a hero section, flexible module positions, sticky header, and customisable brand colour." +TPL_MOKOJOOMHERO_LOGO_FILE_LABEL="Logo" +TPL_MOKOJOOMHERO_LOGO_FILE_DESC="Select an image to use as the site logo." +TPL_MOKOJOOMHERO_SITE_DESCRIPTION_LABEL="Site Description" +TPL_MOKOJOOMHERO_SITE_DESCRIPTION_DESC="A short tagline displayed next to the logo." +TPL_MOKOJOOMHERO_BRAND_COLOR_LABEL="Brand Colour" +TPL_MOKOJOOMHERO_BRAND_COLOR_DESC="Primary accent colour used throughout the template." +TPL_MOKOJOOMHERO_FLUID_LABEL="Fluid Container" +TPL_MOKOJOOMHERO_FLUID_DESC="Use a full-width container instead of a fixed-width one." +TPL_MOKOJOOMHERO_STICKY_HEADER_LABEL="Sticky Header" +TPL_MOKOJOOMHERO_STICKY_HEADER_DESC="Keep the header visible when scrolling." +TPL_MOKOJOOMHERO_BACK_TO_TOP_LABEL="Back to Top Button" +TPL_MOKOJOOMHERO_BACK_TO_TOP_DESC="Show a floating button that scrolls the page to the top." diff --git a/src/offline.php b/src/offline.php new file mode 100644 index 0000000..2e560ac --- /dev/null +++ b/src/offline.php @@ -0,0 +1,134 @@ + + * + * This file is part of a Moko Consulting project. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: MokoJoomHero.Template + * INGROUP: MokoJoomHero + * REPO: https://github.com/mokoconsulting-tech/MokoJoomHero + * PATH: /src/offline.php + * VERSION: 01.00.01 + * BRIEF: Offline page template — shown when the site is taken offline via Global Configuration + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\Helper\AuthenticationHelper; +use Joomla\CMS\HTML\HTMLHelper; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Router\Route; +use Joomla\CMS\Uri\Uri; + +/** @var Joomla\CMS\Document\HtmlDocument $this */ + +$app = Factory::getApplication(); + +// Offline message from Global Configuration +$offline_message = $app->get('offline_message', Text::_('JOFFLINE_MESSAGE')); + +// Extra login methods +$extra_buttons = AuthenticationHelper::getLoginButtons('form-login'); + +?> + + + + + + <?php echo htmlspecialchars($app->get('sitename'), ENT_QUOTES, 'UTF-8'); ?> — <?php echo Text::_('JOFFLINE'); ?> + + + + + +
+

get('sitename'), ENT_QUOTES, 'UTF-8'); ?>

+ +

+ + +
+ + diff --git a/src/templateDetails.xml b/src/templateDetails.xml new file mode 100644 index 0000000..dd6ba7e --- /dev/null +++ b/src/templateDetails.xml @@ -0,0 +1,129 @@ + + + + tpl_mokojoomhero + 2026-05 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + 1.0.0 + TPL_MOKOJOOMHERO_DESCRIPTION + 0 + + + index.php + error.php + offline.php + component.php + joomla.asset.json + templateDetails.xml + css + js + images + html + language + + + + css + js + images + + + + topbar + banner + menu + hero + breadcrumbs + sidebar-left + sidebar-right + main-top + main-bottom + footer + debug + + + + en-GB/tpl_mokojoomhero.ini + en-GB/tpl_mokojoomhero.sys.ini + + + + +
+ + + +
+
+ + + + + + + + + + + + +
+
+
+
diff --git a/update.xml b/update.xml index 551709a..a1d12c7 100644 --- a/update.xml +++ b/update.xml @@ -20,11 +20,11 @@ --> - {{EXTENSION_NAME}} - MokoJoomHero — Moko Consulting Joomla extension - {{EXTENSION_ELEMENT}} - {{EXTENSION_TYPE}} - {{VERSION}} + MokoJoomHero + MokoJoomHero — Modern Joomla template by Moko Consulting + tpl_mokojoomhero + template + 1.0.0 https://git.mokoconsulting.tech/mokoconsulting-tech/MokoJoomHero/releases/download/v{{VERSION}}/{{EXTENSION_ELEMENT}}.zip diff --git a/updates.xml b/updates.xml index 96e60df..f9f097d 100644 --- a/updates.xml +++ b/updates.xml @@ -20,11 +20,11 @@ --> - {{EXTENSION_NAME}} - MokoJoomHero — Moko Consulting Joomla extension - {{EXTENSION_ELEMENT}} - {{EXTENSION_TYPE}} - {{VERSION}} + MokoJoomHero + MokoJoomHero — Modern Joomla template by Moko Consulting + tpl_mokojoomhero + template + 1.0.0 https://git.mokoconsulting.tech/mokoconsulting-tech/MokoJoomHero/releases/download/v{{VERSION}}/{{EXTENSION_ELEMENT}}.zip -- 2.52.0 From 61ef80d633763629bdce712a43fcb5a84eeda295 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 16 May 2026 22:19:26 +0000 Subject: [PATCH 130/136] chore: rename Setup step to moko-platform [skip ci] --- .gitea/workflows/auto-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/auto-release.yml b/.gitea/workflows/auto-release.yml index 3b254a7..99679ad 100644 --- a/.gitea/workflows/auto-release.yml +++ b/.gitea/workflows/auto-release.yml @@ -58,7 +58,7 @@ jobs: token: ${{ secrets.GA_TOKEN }} fetch-depth: 0 - - name: Setup MokoStandards tools + - name: Setup moko-platform env: MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting -- 2.52.0 From 383a60d5bb52d2e50ffc6be75ba36a2872b55fb2 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 16 May 2026 22:19:55 +0000 Subject: [PATCH 131/136] =?UTF-8?q?chore:=20remove=20docs/=20=E2=80=94=20d?= =?UTF-8?q?ocumentation=20lives=20in=20wiki=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/!.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 docs/!.gitkeep diff --git a/docs/!.gitkeep b/docs/!.gitkeep deleted file mode 100644 index e69de29..0000000 -- 2.52.0 From d46a379659890d6d252e734488b755a5562c014c Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 16 May 2026 22:19:55 +0000 Subject: [PATCH 132/136] =?UTF-8?q?chore:=20remove=20docs/=20=E2=80=94=20d?= =?UTF-8?q?ocumentation=20lives=20in=20wiki=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/FIREWALL_CONFIGURATION.md | 263 --------------------------------- 1 file changed, 263 deletions(-) delete mode 100644 docs/FIREWALL_CONFIGURATION.md diff --git a/docs/FIREWALL_CONFIGURATION.md b/docs/FIREWALL_CONFIGURATION.md deleted file mode 100644 index 77d9728..0000000 --- a/docs/FIREWALL_CONFIGURATION.md +++ /dev/null @@ -1,263 +0,0 @@ -# Firewall Configuration for License Downloads - -This document provides guidance on configuring firewalls to allow the automated license download workflow to access www.gnu.org. - -## Overview - -The `download-license.yml` GitHub Actions workflow automatically downloads the GPL-3.0 license from www.gnu.org. For enterprise environments with strict firewall rules, specific domains must be allowlisted to enable this functionality. - -## Required Access - -### Primary Domain -- **Domain:** `www.gnu.org` -- **Protocol:** HTTPS -- **Port:** 443 -- **Purpose:** Primary source for GPL-3.0 license download - -### Fallback Domain -- **Domain:** `ftp.gnu.org` -- **Protocol:** HTTPS -- **Port:** 443 -- **Purpose:** Alternative source if primary is unavailable - -## Firewall Configuration Examples - -### iptables (Linux) - -```bash -# Allow outbound HTTPS to www.gnu.org -iptables -A OUTPUT -p tcp -d www.gnu.org --dport 443 -j ACCEPT - -# Allow outbound HTTPS to ftp.gnu.org (fallback) -iptables -A OUTPUT -p tcp -d ftp.gnu.org --dport 443 -j ACCEPT -``` - -### UFW (Uncomplicated Firewall) - -```bash -# Allow outbound HTTPS to www.gnu.org -ufw allow out to www.gnu.org port 443 proto tcp - -# Allow outbound HTTPS to ftp.gnu.org (fallback) -ufw allow out to ftp.gnu.org port 443 proto tcp -``` - -### firewalld (RHEL/CentOS/Fedora) - -```bash -# Add www.gnu.org to allowed domains -firewall-cmd --permanent --direct --add-rule ipv4 filter OUTPUT 0 -p tcp -d www.gnu.org --dport 443 -j ACCEPT - -# Add ftp.gnu.org to allowed domains -firewall-cmd --permanent --direct --add-rule ipv4 filter OUTPUT 0 -p tcp -d ftp.gnu.org --dport 443 -j ACCEPT - -# Reload firewall -firewall-cmd --reload -``` - -### Windows Firewall - -```powershell -# Allow outbound HTTPS to www.gnu.org -New-NetFirewallRule -DisplayName "Allow HTTPS to www.gnu.org" ` - -Direction Outbound ` - -RemoteAddress www.gnu.org ` - -Protocol TCP ` - -RemotePort 443 ` - -Action Allow - -# Allow outbound HTTPS to ftp.gnu.org -New-NetFirewallRule -DisplayName "Allow HTTPS to ftp.gnu.org" ` - -Direction Outbound ` - -RemoteAddress ftp.gnu.org ` - -Protocol TCP ` - -RemotePort 443 ` - -Action Allow -``` - -## Network Security Groups (Cloud Providers) - -### AWS Security Groups - -```yaml -# Outbound rule for www.gnu.org -Type: HTTPS -Protocol: TCP -Port Range: 443 -Destination: 0.0.0.0/0 # Or specific IP range if known -Description: Allow license download from www.gnu.org -``` - -### Azure Network Security Groups - -```bash -az network nsg rule create \ - --resource-group myResourceGroup \ - --nsg-name myNSG \ - --name Allow-GNU-HTTPS \ - --protocol tcp \ - --priority 1000 \ - --destination-port-range 443 \ - --access Allow \ - --direction Outbound \ - --description "Allow HTTPS to www.gnu.org for license downloads" -``` - -### Google Cloud Platform - -```bash -gcloud compute firewall-rules create allow-gnu-https \ - --direction=EGRESS \ - --priority=1000 \ - --network=default \ - --action=ALLOW \ - --rules=tcp:443 \ - --destination-ranges=0.0.0.0/0 \ - --description="Allow HTTPS to www.gnu.org for license downloads" -``` - -## Testing Connectivity - -### Test DNS Resolution - -```bash -nslookup www.gnu.org -``` - -Expected output should show IP addresses for www.gnu.org. - -### Test HTTPS Connectivity - -```bash -curl -I https://www.gnu.org/licenses/gpl-3.0.txt -``` - -Expected output should show HTTP 200 OK response. - -### Full Download Test - -```bash -curl -f -L -o GPL-3.0.txt https://www.gnu.org/licenses/gpl-3.0.txt -``` - -This should download the GPL-3.0 license without errors. - -## Troubleshooting - -### Connection Timeout - -**Symptom:** `curl: (28) Connection timed out after 10000 milliseconds` - -**Cause:** Firewall is blocking outbound connections to www.gnu.org - -**Solution:** -1. Verify firewall rules are properly configured -2. Check if corporate proxy is required -3. Ensure DNS resolution is working -4. Contact network administrator to allowlist www.gnu.org - -### DNS Resolution Failure - -**Symptom:** `curl: (6) Could not resolve host: www.gnu.org` - -**Cause:** DNS server cannot resolve www.gnu.org or DNS queries are blocked - -**Solution:** -1. Test with `nslookup www.gnu.org` -2. Check DNS server configuration -3. Verify DNS queries (port 53) are not blocked -4. Try using alternate DNS (e.g., 8.8.8.8, 1.1.1.1) - -### Certificate Verification Failed - -**Symptom:** `curl: (60) SSL certificate problem` - -**Cause:** SSL/TLS inspection or certificate validation issues - -**Solution:** -1. Ensure system CA certificates are up-to-date -2. If using corporate SSL inspection, ensure root certificates are installed -3. Verify system time is correct (affects certificate validation) - -## Proxy Configuration - -If your enterprise environment uses a proxy server, configure the workflow to use it: - -### GitHub Actions with Proxy - -Add these environment variables to your workflow: - -```yaml -env: - HTTP_PROXY: http://proxy.example.com:8080 - HTTPS_PROXY: http://proxy.example.com:8080 - NO_PROXY: localhost,127.0.0.1 -``` - -### Self-Hosted Runners - -For self-hosted GitHub Actions runners, configure the proxy in the runner's environment: - -```bash -# Linux/macOS -export HTTP_PROXY=http://proxy.example.com:8080 -export HTTPS_PROXY=http://proxy.example.com:8080 - -# Windows -setx HTTP_PROXY "http://proxy.example.com:8080" -setx HTTPS_PROXY "http://proxy.example.com:8080" -``` - -## Security Considerations - -### Minimal Permissions - -The workflow uses the principle of least privilege: -- Only requires `contents: write` permission for committing the license -- No access to secrets or sensitive repository data -- Downloads only from trusted gnu.org domains - -### Domain Verification - -The workflow includes built-in verification: -1. DNS resolution check -2. HTTPS connectivity test -3. License content validation (checks for GPL-3.0 markers) -4. File integrity checks (size, format) - -### Alternative: Manual License Management - -If automated downloads are not possible due to security policies, you can: - -1. **Manually download the license:** - ```bash - curl -o LICENSE https://www.gnu.org/licenses/gpl-3.0.txt - ``` - -2. **Add copyright header:** - Add your project's copyright information at the top of the LICENSE file - -3. **Commit to repository:** - ```bash - git add LICENSE - git commit -m "chore: add GPL-3.0 license" - git push - ``` - -4. **Disable the workflow:** - Remove or comment out the workflow file `.github/workflows/download-license.yml` - -## Support - -For questions or issues related to firewall configuration: - -1. Consult your organization's network security team -2. Review this documentation for common solutions -3. Check the workflow logs in GitHub Actions for specific error messages -4. Contact Moko Consulting: hello@mokoconsulting.tech - -## References - -- [GNU GPL-3.0 License](https://www.gnu.org/licenses/gpl-3.0.html) -- [GitHub Actions Documentation](https://docs.github.com/en/actions) -- [Firewall Best Practices](https://www.gnu.org/prep/maintain/html_node/Firewalls.html) -- 2.52.0 From 6b005901c0279be0b87ea9ae29dd84589d4456cb Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 16 May 2026 22:19:56 +0000 Subject: [PATCH 133/136] =?UTF-8?q?chore:=20remove=20docs/=20=E2=80=94=20d?= =?UTF-8?q?ocumentation=20lives=20in=20wiki=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/IMPLEMENTATION_SUMMARY.md | 165 --------------------------------- 1 file changed, 165 deletions(-) delete mode 100644 docs/IMPLEMENTATION_SUMMARY.md diff --git a/docs/IMPLEMENTATION_SUMMARY.md b/docs/IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index da159c1..0000000 --- a/docs/IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,165 +0,0 @@ -# Implementation Summary: License Download with Firewall Configuration - -## Overview - -This implementation adds automated GPL-3.0 license download functionality with comprehensive firewall configuration support for enterprise environments. - -## What Was Implemented - -### 1. GitHub Actions Workflow (`.github/workflows/download-license.yml`) - -A comprehensive workflow that: -- **Downloads GPL-3.0 license** from www.gnu.org automatically -- **Tests firewall connectivity** before attempting download -- **Validates license content** to ensure integrity -- **Adds copyright header** with project information -- **Commits and pushes** changes automatically -- **Provides fallback sources** (ftp.gnu.org) if primary fails -- **Includes detailed logging** and workflow summaries - -**Triggers:** -- Manual dispatch (can be run on-demand) -- Monthly schedule (1st of each month) -- When workflow file is modified - -**Key Features:** -- DNS resolution testing -- HTTPS connectivity validation -- License content verification (checks for GPL-3.0 markers) -- Automatic retry with fallback source -- Comprehensive error messages with troubleshooting steps - -### 2. Firewall Configuration Documentation (`docs/FIREWALL_CONFIGURATION.md`) - -Enterprise-ready documentation including: -- **Required network access** specifications -- **Firewall rule examples** for multiple platforms: - - iptables (Linux) - - UFW (Uncomplicated Firewall) - - firewalld (RHEL/CentOS/Fedora) - - Windows Firewall (PowerShell) - - AWS Security Groups - - Azure Network Security Groups - - Google Cloud Platform firewall rules -- **Connectivity testing** procedures -- **Troubleshooting guide** for common issues -- **Proxy configuration** instructions -- **Security considerations** and best practices - -### 3. Test Script (`scripts/test-firewall.sh`) - -A standalone testing utility that: -- Validates DNS resolution for gnu.org domains -- Tests HTTPS connectivity -- Attempts actual license download -- Provides clear pass/fail results -- Includes detailed error messages and remediation steps - -### 4. Documentation Updates - -- **README.md**: Added section about automated license management with link to firewall docs -- **.github/workflows/README.md**: Overview of available workflows and their requirements - -## Technical Details - -### Required Network Access - -**Primary Domain:** -- Domain: `www.gnu.org` -- Protocol: HTTPS (TCP) -- Port: 443 - -**Fallback Domain:** -- Domain: `ftp.gnu.org` -- Protocol: HTTPS (TCP) -- Port: 443 - -### Workflow Steps - -1. **Checkout repository** - Uses `actions/checkout@v4` -2. **Configure firewall allowlist** - Tests connectivity and displays requirements -3. **Download GPL-3.0 License** - Downloads from primary/fallback sources -4. **Validate license file** - Checks content, size, and format -5. **Add copyright header** - Prepends project copyright information -6. **Check for changes** - Determines if commit is needed -7. **Commit and push changes** - Automatically commits if license changed -8. **Workflow summary** - Provides detailed summary in GitHub Actions UI - -### Security Features - -- **Minimal permissions**: Only `contents: write` required -- **Input validation**: Verifies downloaded content before use -- **Content verification**: Checks for GPL-3.0 markers in downloaded file -- **Fallback sources**: Multiple trusted sources (www.gnu.org, ftp.gnu.org) -- **Skip CI tag**: Prevents infinite workflow loops - -### Enterprise Considerations - -The implementation addresses enterprise requirements: - -1. **Firewall compatibility**: Clear documentation for various firewall types -2. **Proxy support**: Instructions for corporate proxy configuration -3. **Self-hosted runners**: Support for on-premise GitHub Actions runners -4. **Troubleshooting**: Comprehensive guide for common enterprise network issues -5. **Manual fallback**: Instructions for manual license management if automated download not possible - -## Files Created/Modified - -### Created: -- `.github/workflows/download-license.yml` - Main workflow file -- `.github/workflows/README.md` - Workflow documentation -- `docs/FIREWALL_CONFIGURATION.md` - Firewall setup guide -- `scripts/test-firewall.sh` - Connectivity test script - -### Modified: -- `README.md` - Added automated license management section - -## Usage - -### For End Users - -Simply use the template repository. The workflow will: -- Run automatically on the 1st of each month -- Can be triggered manually from GitHub Actions tab -- Downloads/updates the LICENSE file as needed - -### For Enterprise Environments - -1. Review firewall configuration requirements in `docs/FIREWALL_CONFIGURATION.md` -2. Configure firewall rules to allow access to www.gnu.org:443 -3. Test connectivity using `scripts/test-firewall.sh` -4. Enable the workflow in GitHub Actions - -### Manual Testing - -```bash -# Test firewall connectivity -./scripts/test-firewall.sh - -# Manually trigger workflow -# Go to GitHub Actions → Download License → Run workflow -``` - -## Benefits - -1. **Compliance**: Ensures GPL-3.0 license is always present and up-to-date -2. **Automation**: No manual license management required -3. **Enterprise-ready**: Comprehensive firewall documentation and support -4. **Reliable**: Fallback sources and robust error handling -5. **Transparent**: Clear logging and validation steps -6. **Secure**: Minimal permissions and content verification - -## Future Enhancements (Optional) - -Potential improvements for future iterations: -- Support for additional license types -- Integration with license scanning tools -- Automated license compliance reporting -- License header insertion in source files -- Multi-license support for complex projects - -## References - -- [GNU GPL-3.0 License](https://www.gnu.org/licenses/gpl-3.0.html) -- [GitHub Actions Documentation](https://docs.github.com/en/actions) -- [GitHub Actions Workflow Syntax](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions) -- 2.52.0 From d7833ab689430dd0dd3f5d87787a730814f38896 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 16 May 2026 22:19:57 +0000 Subject: [PATCH 134/136] =?UTF-8?q?chore:=20remove=20docs/=20=E2=80=94=20d?= =?UTF-8?q?ocumentation=20lives=20in=20wiki=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/QUICKSTART.md | 172 --------------------------------------------- 1 file changed, 172 deletions(-) delete mode 100644 docs/QUICKSTART.md diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md deleted file mode 100644 index be7699e..0000000 --- a/docs/QUICKSTART.md +++ /dev/null @@ -1,172 +0,0 @@ -# Quick Start: License Sync Workflow - -This guide helps you get started with the manual GPL license sync workflow that downloads and maintains multiple GPL licenses. - -## For Template Users - -If you're using this template repository, the license sync workflow is available for manual use: - -1. **Create your repository from this template** -2. **Trigger the workflow manually when needed:** - - Go to Actions tab → Sync Licenses → Run workflow - - Automatic scheduling is disabled - you control when licenses are updated - -The workflow downloads and syncs: -- GPL-3.0 (primary license in LICENSE file) -- GPL-2.0 -- LGPL-3.0 -- LGPL-2.1 - -All licenses are stored in the `licenses/` directory with the primary GPL-3.0 in the root LICENSE file. - -**Note:** Automatic monthly updates are disabled. Licenses must be updated manually by triggering the workflow. - -## For Enterprise Environments - -If your organization has strict firewall rules, follow these steps: - -### Step 1: Test Connectivity - -Run the test script to check if your environment can access www.gnu.org: - -```bash -./scripts/test-firewall.sh -``` - -### Step 2: Configure Firewall (if needed) - -If the test fails, configure your firewall to allow: - -- **Domain:** `www.gnu.org` -- **Protocol:** HTTPS -- **Port:** 443 - -See [docs/FIREWALL_CONFIGURATION.md](FIREWALL_CONFIGURATION.md) for detailed firewall configuration examples. - -### Step 3: Retest - -After configuring the firewall, run the test again: - -```bash -./scripts/test-firewall.sh -``` - -All tests should pass before enabling the workflow. - -## Manual Trigger - -To manually run the workflow: - -1. Go to your repository on GitHub -2. Click the **Actions** tab -3. Select **Sync Licenses** workflow -4. Click **Run workflow** button -5. Click **Run workflow** to confirm - -## What Happens - -When the workflow runs: - -1. ✅ Tests connectivity to all license sources (www.gnu.org and ftp.gnu.org) -2. ✅ Downloads multiple GPL licenses (GPL-2.0, GPL-3.0, LGPL-2.1, LGPL-3.0) -3. ✅ Validates all downloaded licenses -4. ✅ Copies GPL-3.0 to root LICENSE file with copyright header -5. ✅ Stores additional licenses in `licenses/` directory -6. ✅ Commits and pushes if any licenses changed - -## Viewing Results - -After the workflow runs: - -- Check the **Actions** tab for workflow status -- View detailed logs by clicking on the workflow run -- The LICENSE file will be created/updated in your repository -- See the workflow summary for detailed information - -## Troubleshooting - -### Workflow Fails with "Cannot resolve www.gnu.org" - -**Cause:** DNS resolution is blocked or failing - -**Solution:** Ensure DNS queries are allowed in your firewall - -### Workflow Fails with "Cannot connect to www.gnu.org" - -**Cause:** HTTPS connections to www.gnu.org are blocked - -**Solution:** Configure firewall to allow HTTPS (port 443) to www.gnu.org - -### Workflow Succeeds but LICENSE Not Committed - -**Cause:** License content hasn't changed since last run - -**Solution:** This is normal. The workflow only commits when changes are detected. - -## Need Help? - -- 📖 [Full Firewall Configuration Guide](FIREWALL_CONFIGURATION.md) -- 📖 [Implementation Summary](IMPLEMENTATION_SUMMARY.md) -- 📖 [Workflow Documentation](../.github/workflows/README.md) -- 📧 Contact: hello@mokoconsulting.tech - -## Advanced Configuration - -### Enable Automatic Updates - -Automatic updates are currently disabled. To enable automatic monthly updates: - -1. Edit `.github/workflows/download-license.yml` -2. Uncomment the `schedule` trigger: - ```yaml - on: - workflow_dispatch: # Keep manual trigger - schedule: # Uncomment to enable automatic updates - - cron: '0 0 1 * *' # Run monthly - ``` - -### Change Schedule - -If you enable automatic updates, you can customize the schedule: - -```yaml -schedule: - # Run weekly (every Monday at midnight) - - cron: '0 0 * * 1' - - # Run daily at midnight - - cron: '0 0 * * *' - - # Run quarterly (1st day of Jan, Apr, Jul, Oct) - - cron: '0 0 1 1,4,7,10 *' -``` - -### Use with Self-Hosted Runners - -For self-hosted GitHub Actions runners: - -1. Ensure the runner has internet access -2. Configure firewall on the runner host -3. Test connectivity using `./scripts/test-firewall.sh` -4. If using a proxy, configure it in the workflow: - -```yaml -env: - HTTP_PROXY: http://proxy.example.com:8080 - HTTPS_PROXY: http://proxy.example.com:8080 -``` - -## Security Notes - -- Workflow uses minimal permissions (`contents: write` only) -- Downloads only from trusted gnu.org domains -- Validates downloaded content before committing -- All steps are logged for audit purposes - -## Best Practices - -1. ✅ Test connectivity before deploying to production -2. ✅ Review firewall logs if workflow fails -3. ✅ Keep workflow file in version control -4. ✅ Monitor workflow runs in the Actions tab -5. ✅ Update firewall rules if gnu.org IPs change -- 2.52.0 From 530cb31edf8f09ba16661b6e56c453003273d54b Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 16 May 2026 22:19:57 +0000 Subject: [PATCH 135/136] =?UTF-8?q?chore:=20remove=20docs/=20=E2=80=94=20d?= =?UTF-8?q?ocumentation=20lives=20in=20wiki=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/index.md | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 docs/index.md diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 520d813..0000000 --- a/docs/index.md +++ /dev/null @@ -1,16 +0,0 @@ -# Docs Index: /templates/repos/joomla/module/docs - -## Purpose - -This index provides navigation to documentation within this folder. - -## Metadata - -- **Document Type:** index -- **Auto-generated:** This file is automatically generated by rebuild_indexes.py - -## Revision History - -| Change | Notes | Author | -| --- | --- | --- | -| Automated update | Generated by documentation index automation | rebuild_indexes.py | -- 2.52.0 From a9b174cbe2a0fabe3a6a7981c9b2661ccd75510f Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 16 May 2026 22:19:58 +0000 Subject: [PATCH 136/136] =?UTF-8?q?chore:=20remove=20docs/=20=E2=80=94=20d?= =?UTF-8?q?ocumentation=20lives=20in=20wiki=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/update-server.md | 119 ------------------------------------------ 1 file changed, 119 deletions(-) delete mode 100644 docs/update-server.md diff --git a/docs/update-server.md b/docs/update-server.md deleted file mode 100644 index 2d3ff7f..0000000 --- a/docs/update-server.md +++ /dev/null @@ -1,119 +0,0 @@ - - -# Joomla Update Server - -[![MokoStandards](https://img.shields.io/badge/MokoStandards-04.04.00-blue)](https://github.com/mokoconsulting-tech/MokoStandards) - -This document explains how `update.xml` is automatically managed for this Joomla extension following the [Joomla Update Server specification](https://docs.joomla.org/Deploying_an_Update_Server). - -## How It Works - -Joomla checks for extension updates by fetching an XML file from the URL defined in the `` tag in the extension's XML manifest. MokoStandards generates this file automatically. - -### Automatic Generation - -| Event | Workflow | `` | `` | -|-------|----------|---------|-------------| -| Merge to `main` | `auto-release.yml` | `stable` | `XX.YY.ZZ` | -| Push to `dev/**` | `deploy-dev.yml` | `development` | `development` | -| Push to `rc/**` | `deploy-dev.yml` | `rc` | `XX.YY.ZZ-rc` | - -### Generated XML Structure - -```xml - - - - Extension Name - Extension Name update - com_extensionname - component - 01.02.03 - site - system - - stable - - https://github.com/.../releases/tag/v01.02.03 - - https://github.com/.../releases/download/v01.02.03/com_ext-01.02.03.zip - - - 8.2 - Moko Consulting - https://mokoconsulting.tech - - -``` - -### Metadata Source - -All metadata is extracted from the extension's XML manifest (`src/*.xml`) at build time: - -| XML Element | Source | Notes | -|-------------|--------|-------| -| `` | `` in manifest | Extension display name | -| `` | `` in manifest | Must match installed extension identifier | -| `` | `type` attribute on `` | `component`, `module`, `plugin`, `library`, `package`, `template` | -| `` | `client` attribute on `` | `site` or `administrator` — **required for plugins and modules** | -| `` | `group` attribute on `` | Plugin group (e.g., `system`, `content`) — **required for plugins** | -| `` | `` in manifest | Falls back to Joomla 5.x / 6.x if not specified | -| `` | `` in manifest | Included only if present | - -### Extension Manifest Setup - -Your XML manifest must include an `` tag pointing to the `update.xml` on the `main` branch: - -```xml - - My Extension - com_myextension - - - - https://raw.githubusercontent.com/mokoconsulting-tech/MokoJoomHero/main/update.xml - - - -``` - -### Branch Lifecycle - -``` -dev/XX.YY.ZZ → rc/XX.YY.ZZ → main → version/XX.YY -(development) (rc) (stable) (frozen snapshot) -``` - -1. **Development** (`dev/**`): `update.xml` with `development`, download points to branch archive -2. **Release Candidate** (`rc/**`): `update.xml` with `rc`, version set to `XX.YY.ZZ-rc` -3. **Stable Release** (merge to `main`): `update.xml` with `stable`, download points to GitHub Release asset -4. **Frozen Snapshot** (`version/XX.YY`): immutable, never force-pushed - -### Health Checks - -The `repo_health.yml` workflow verifies on every commit: - -- `update.xml` exists in the repository root -- XML manifest exists with `` tag -- ``, ``, ``, `` tags present -- Extension `type` attribute is valid -- Language `.ini` files exist -- `index.html` directory listing protection in `src/`, `src/admin/`, `src/site/` - ---- - -*Managed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). See [docs/workflows/update-server.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/workflows/update-server.md) for the full specification.* -- 2.52.0