From 7df3c7afd73ca7f140ef163a6eda697835db5aa4 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sat, 30 May 2026 22:25:29 +0000 Subject: [PATCH 01/27] chore: sync updates.xml 01.07.00 from main [skip ci] --- updates.xml | 52 ++++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/updates.xml b/updates.xml index 4c0efa7..b4070ae 100644 --- a/updates.xml +++ b/updates.xml @@ -1,29 +1,10 @@ - - MokoJoomHero - MokoJoomHero stable build. - mod_mokojoomhero - module - site - 01.06.00 - 2026-05-30 - https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/tag/stable - - https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/download/stable/mod_mokojoomhero-01.06.00.zip - - de21e4010e19323746c9aeff12d6a240dc29a2a0c3ef1e091549a2331e710bc7 - stable - https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/raw/branch/main/CHANGELOG.md - Moko Consulting - https://mokoconsulting.tech - - Module - MokoJoomHero Module - MokoJoomHero dev build. @@ -36,7 +17,7 @@ https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/download/development/mod_mokojoomhero-01.07.00-dev.zip - 32301fad78e9ad31613d2d02a8f2dd0519f5118b7683335b36532a4e1e5fe969 + 56ae99ad18e12ee52c60298adef5983aef788fe867d3e3a36957b314ad7eb386 dev https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/raw/branch/main/CHANGELOG.md Moko Consulting @@ -55,7 +36,7 @@ https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/download/alpha/mod_mokojoomhero-01.07.00-alpha.zip - 32301fad78e9ad31613d2d02a8f2dd0519f5118b7683335b36532a4e1e5fe969 + 56ae99ad18e12ee52c60298adef5983aef788fe867d3e3a36957b314ad7eb386 alpha https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/raw/branch/main/CHANGELOG.md Moko Consulting @@ -74,7 +55,7 @@ https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/download/beta/mod_mokojoomhero-01.07.00-beta.zip - 32301fad78e9ad31613d2d02a8f2dd0519f5118b7683335b36532a4e1e5fe969 + 56ae99ad18e12ee52c60298adef5983aef788fe867d3e3a36957b314ad7eb386 beta https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/raw/branch/main/CHANGELOG.md Moko Consulting @@ -89,15 +70,34 @@ site 01.07.00-rc 2026-05-30 - https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/tag/release-candidate + https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/tag/release-candidate - https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/download/release-candidate/mod_mokojoomhero-01.07.00-rc.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/download/release-candidate/mod_mokojoomhero-01.07.00-rc.zip - 32301fad78e9ad31613d2d02a8f2dd0519f5118b7683335b36532a4e1e5fe969 + 56ae99ad18e12ee52c60298adef5983aef788fe867d3e3a36957b314ad7eb386 rc https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/raw/branch/main/CHANGELOG.md Moko Consulting https://mokoconsulting.tech + + + + Module - MokoJoomHero + Module - MokoJoomHero stable build. + mod_mokojoomhero + module + site + 01.07.00 + 2026-05-30 + https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/tag/stable + + https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/download/stable/mod_mokojoomhero-01.07.00.zip + + 56ae99ad18e12ee52c60298adef5983aef788fe867d3e3a36957b314ad7eb386 + stable + https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/raw/branch/main/CHANGELOG.md + Moko Consulting + https://mokoconsulting.tech -- 2.52.0 From 2f2832c6617e8d435e8e329fc80999dcede16e36 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 30 May 2026 19:11:11 -0500 Subject: [PATCH 02/27] chore(manifest): fix display-name structure and update CONTRIBUTING.md Standardize manifest.xml identity block: ensure contains only the machine identifier (PascalCase) and contains the human-readable label with Joomla extension type prefix. Remove duplicate tags where present. Update CONTRIBUTING.md from moko-platform default. Authored-by: Moko Consulting --- .mokogitea/manifest.xml | 3 +- CONTRIBUTING.md | 249 +++++++++++++++++++++++----------------- 2 files changed, 143 insertions(+), 109 deletions(-) diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml index 8f78ab4..dde7d5b 100644 --- a/.mokogitea/manifest.xml +++ b/.mokogitea/manifest.xml @@ -6,7 +6,8 @@ --> - Module - MokoJoomHero + MokoJoomHero + Module - MokoJoomHero MokoConsulting A Joomla Module designed to provide a random image from a folder with content on top as a Hero. 01.06.00 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8faeb17..c0b4858 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,128 +1,161 @@ - - -# Contributing - -Thank you for your interest in contributing to **MokoJoomHero**! - -This repository is governed by **[MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards)** — the authoritative source of coding standards, workflows, and policies for all Moko Consulting repositories. - -## Branch Strategy - -| Branch | Purpose | Deploys To | -|--------|---------|------------| -| `main` | Bleeding edge — all development merges here | CI only | -| `dev/XX.YY.ZZ` | Feature development | Dev server (version: "development") | -| `version/XX.YY` | Stable frozen snapshot | Demo + RS servers | - -### Development Workflow +## Branching Workflow ``` -1. Create branch: git checkout -b dev/XX.YY.ZZ/my-feature -2. Develop + test (dev server auto-deploys on push) -3. Open PR → main (squash merge only) -4. Auto-release (version branch + tag + GitHub Release created automatically) +feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge──> main ``` -### Branch Naming +### Step by step -| Prefix | Use | -|--------|-----| -| `dev/XX.YY.ZZ` | Feature development (e.g., `dev/02.00.00/add-extrafields`) | -| `version/XX.YY` | Stable release (auto-created, never manually pushed) | -| `chore/` | Automated sync branches (managed by MokoStandards) | +1. **Create a feature branch** from `dev`: + ```bash + git checkout dev && git pull + git checkout -b feature/my-change + ``` -> **Never use** `feature/`, `hotfix/`, or `release/` prefixes — they are not part of the MokoStandards branch strategy. +2. **Work and commit** on your feature branch. Push to origin. -## Commit Conventions +3. **Open a PR**: `feature/my-change` → `dev`. After review and checks, merge it. -Use [conventional commits](https://www.conventionalcommits.org/): +4. **When ready for release**, open a **draft PR**: `dev` → `main`. + - This automatically renames the source branch to `rc` (release candidate) + - An RC pre-release is built and uploaded + +5. **Alpha and beta branches** are created by manually renaming the branch before the RC stage: + - Rename `dev` to `alpha` for early testing → alpha pre-release is built + - Rename `alpha` to `beta` for feature-complete testing → beta pre-release is built + - When the draft PR is created, the branch is renamed to `rc` + +6. **Once PR checks pass** on the `rc` branch, mark the PR as ready and merge to `main`. + +7. **Merging to main** triggers the stable release pipeline: + - Minor version bump (e.g., `02.09.xx` → `02.10.00`) + - Stability suffix stripped (clean version) + - Gitea release created with ZIP/tar.gz packages + - `updates.xml` updated (Joomla extensions) + - `dev` branch recreated from `main` + +### Branch summary + +| Branch | Purpose | Created by | +|--------|---------|-----------| +| `feature/*` | New features and fixes | Developer | +| `dev` | Integration branch | Auto-recreated after release | +| `alpha` | Alpha pre-release testing | Manual rename from `dev` | +| `beta` | Beta pre-release testing | Manual rename from `alpha` | +| `rc` | Release candidate | Auto-renamed on draft PR to main | +| `main` | Stable releases | Protected, merge only | +| `version/XX.YY.ZZ` | Archived release snapshots | Auto-created by CI | + +### Protected branches + +| Branch | Direct push | Merge via | +|--------|------------|-----------| +| `main` | Blocked (CI bot whitelisted) | PR merge only | +| `dev` | Blocked (CI bot whitelisted) | PR merge from feature/* | +| `rc` | Blocked (CI bot whitelisted) | Auto-created on draft PR | +| `alpha` | Blocked (CI bot whitelisted) | Manual rename | +| `beta` | Blocked (CI bot whitelisted) | Manual rename | +| `feature/*` | Open | N/A (source branch) | + +## Version Policy + +### Format + +All versions use `XX.YY.ZZ` — three two-digit segments, zero-padded: + +- **XX** — Major version (breaking changes) +- **YY** — Minor version (new features, bumped on release to main) +- **ZZ** — Patch version (auto-incremented on every push to dev/feature branches) + +Rollover: patch `99` → `00` increments minor; minor `99` → `00` increments major. + +### Stability suffixes + +Each branch appends a suffix to indicate stability: + +| Branch | Suffix | Example | +|--------|--------|---------| +| `main` | (none) | `02.09.00` | +| `dev` | `-dev` | `02.09.01-dev` | +| `feature/*` | `-dev` | `02.09.01-dev` | +| `alpha` | `-alpha` | `02.09.01-alpha` | +| `beta` | `-beta` | `02.09.01-beta` | +| `rc` | `-rc` | `02.09.01-rc` | + +### Auto version bump + +On every push to `dev`, `feature/*`, or `patch/*`: + +1. Patch version incremented +2. Stability suffix `-dev` applied +3. All version-bearing files updated (manifests, CHANGELOG, PHP headers, etc.) +4. Commit created with `[skip ci]` to avoid loops + +### Release version flow + +Version bumps happen at specific release events: + +| Event | Bump | Example | +|-------|------|---------| +| Feature merged to dev | Patch bump after dev release | `02.09.01-dev` → release → `02.09.02-dev` | +| Dev promoted to RC | Minor bump | `02.09.02-dev` → `02.10.00-rc` | +| RC merged to main | Minor bump | `02.10.00-rc` → `02.11.00` (stable) | +| Dev recreated from main | Patch bump | `02.11.00` → `02.11.01-dev` | + +### Release stream copies + +When a higher-stability release is published, copies are created for all lesser streams with the same base version: + +- **RC `02.10.00-rc`** also creates: `02.10.00-dev`, `02.10.00-alpha`, `02.10.00-beta` +- **Stable `02.11.00`** also creates: `02.11.00-dev`, `02.11.00-alpha`, `02.11.00-beta`, `02.11.00-rc` + +This ensures Joomla sites on ANY stability channel see the update (Joomla only shows versions higher than what's installed). + +### Version files + +The version tools update all files containing version stamps: + +- `.mokogitea/manifest.xml` (canonical source) +- Joomla XML manifests (`` tag) +- `README.md`, `CHANGELOG.md` (`VERSION:` pattern) +- `package.json`, `pyproject.toml` +- Any text file with a `VERSION: XX.YY.ZZ` label + +Files synced from other repos (with a `# REPO:` header) are not touched. + +## Code Standards + +- **PHP**: PSR-12, tabs for indentation +- **Copyright**: all files must include the Moko Consulting copyright header +- **License**: SPDX identifier `GPL-3.0-or-later` (or as specified per repo) +- **Attribution**: use `Authored-by: Moko Consulting` in commits, not individual names + +## Commit Messages + +Use conventional commit format: ``` -feat(scope): add new extrafield for invoice tracking -fix(sql): correct column type in llx_mytable -docs(readme): update installation instructions -chore(deps): bump enterprise library to 04.02.30 +type(scope): short description + +Optional body with context. + +Authored-by: Moko Consulting ``` -**Valid types:** `feat` | `fix` | `docs` | `chore` | `ci` | `refactor` | `style` | `test` | `perf` | `revert` | `build` +Types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `ci` -## Pull Request Workflow +Special flags in commit messages: +- `[skip ci]` — skip all CI workflows +- `[skip bump]` — skip auto version bump only -1. **Branch** from `main` using `dev/XX.YY.ZZ/description` format -2. **Bump** the patch version in `README.md` before opening the PR -3. **Title** must be a valid conventional commit subject line -4. **Target** `main` — squash merge only (merge commits are disabled) -5. **CI checks** must pass before merge +## Reporting Issues -### What Happens on Merge - -When your PR is merged to `main`, these workflows run automatically: - -1. **sync-version-on-merge** — auto-bumps patch version, propagates to all file headers -2. **auto-release** — creates `version/XX.YY` branch, git tag, and GitHub Release -3. **deploy-demo / deploy-rs** — deploys to demo and RS servers (if `src/**` changed) - -## Coding Standards - -All contributions must follow [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards): - -| Standard | Reference | -|----------|-----------| -| Coding Style | [coding-style-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md) | -| File Headers | [file-header-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/file-header-standards.md) | -| Branching | [branch-release-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/branch-release-strategy.md) | -| Merge Strategy | [merge-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/merge-strategy.md) | -| Scripting | [scripting-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/scripting-standards.md) | -| Build & Release | [build-release.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/workflows/build-release.md) | - -## PR Checklist - -- [ ] Branch named `dev/XX.YY.ZZ/description` -- [ ] Patch version bumped in `README.md` -- [ ] Conventional commit format for PR title -- [ ] All new files have FILE INFORMATION headers -- [ ] `declare(strict_types=1)` in all PHP files -- [ ] PHPDoc on all public methods -- [ ] Tests pass -- [ ] CHANGELOG.md updated -- [ ] No secrets, tokens, or credentials committed - -## Custom Workflows - -Place repo-specific workflows in `.github/workflows/custom/` — they are **never overwritten or deleted** by MokoStandards sync: - -``` -.github/workflows/ -├── deploy-dev.yml ← Synced from MokoStandards -├── auto-release.yml ← Synced from MokoStandards -└── custom/ ← Your custom workflows (safe) - └── my-custom-ci.yml -``` - -## License - -By contributing, you agree that your contributions will be licensed under the [GPL-3.0-or-later](LICENSE) license. +Use the repository's issue tracker with the appropriate template. --- -*This file is synced from [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). Do not edit directly — changes will be overwritten on the next sync.* +*Moko Consulting * -- 2.52.0 From c583ebd56e2fb12fb697ab8e2667f19245cccdf0 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sun, 31 May 2026 01:45:07 +0000 Subject: [PATCH 03/27] chore: sync .mokogitea/workflows/cascade-dev.yml from moko-platform [skip ci] --- .mokogitea/workflows/cascade-dev.yml | 217 +-------------------------- 1 file changed, 7 insertions(+), 210 deletions(-) diff --git a/.mokogitea/workflows/cascade-dev.yml b/.mokogitea/workflows/cascade-dev.yml index f7f0b3c..5f7c1d7 100644 --- a/.mokogitea/workflows/cascade-dev.yml +++ b/.mokogitea/workflows/cascade-dev.yml @@ -1,213 +1,10 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Maintenance -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform -# 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 - +# DISABLED — auto-release Step 11 recreates dev from main after every release. +# Cascade-dev is redundant and causes version conflicts when both main and dev +# have different version numbers in templateDetails.xml / manifest.xml. +name: "Cascade Main → Dev (DISABLED)" +on: workflow_dispatch jobs: - cascade: - name: Cascade main → branches + noop: 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.MOKOGITEA_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 ${GITEA_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.MOKOGITEA_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 ${GITEA_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 ${GITEA_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 ${GITEA_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 ${GITEA_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 ${GITEA_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 + - run: echo "Cascade disabled — auto-release handles dev recreation" -- 2.52.0 From bc31cad73a39735faa6a13598e5299c3b2f0b9d1 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sun, 31 May 2026 03:47:18 +0000 Subject: [PATCH 04/27] chore(ci): remove auto-release.yml for update server migration [skip ci] --- .mokogitea/workflows/auto-release.yml | 270 -------------------------- 1 file changed, 270 deletions(-) delete mode 100644 .mokogitea/workflows/auto-release.yml diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml deleted file mode 100644 index 1227ff8..0000000 --- a/.mokogitea/workflows/auto-release.yml +++ /dev/null @@ -1,270 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Release -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform -# 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: [opened, closed] - branches: - - main - workflow_dispatch: - inputs: - action: - description: 'Action to perform' - required: false - type: choice - default: release - options: - - release - - promote-rc - -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: - # ── PR Opened → Rename branch to RC and build RC release ───────────────────── - promote-rc: - name: Promote to RC - runs-on: release - if: >- - (github.event.action == 'opened' && github.event.pull_request.merged != true) || - (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.MOKOGITEA_TOKEN }} - fetch-depth: 1 - - - name: Setup moko-platform tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - run: | - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - # Always fetch latest CLI tools — never use stale cache from previous runs - rm -rf /tmp/moko-platform-api - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ - /tmp/moko-platform-api - cd /tmp/moko-platform-api - composer install --no-dev --no-interaction --quiet - - - name: Rename branch to rc - run: | - php /tmp/moko-platform-api/cli/branch_rename.php \ - --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \ - --pr "${{ github.event.pull_request.number }}" - - - name: Checkout rc and configure git - run: | - git fetch origin rc - git checkout rc - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - - - name: Publish RC release - run: | - php /tmp/moko-platform-api/cli/release_publish.php \ - --path . --stability rc --bump minor --branch rc \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" - - - name: Summary - if: always() - run: | - echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY - echo "Branch renamed to rc, minor bump, RC + lesser stream releases built, updates.xml synced" >> $GITHUB_STEP_SUMMARY - - # ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── - release: - name: Build & Release Pipeline - runs-on: release - if: >- - github.event.pull_request.merged == true || - (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc') - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.MOKOGITEA_TOKEN }} - fetch-depth: 0 - - - name: Configure git for bot pushes - run: | - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - - - name: Setup moko-platform tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_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 - # Always fetch latest CLI tools — never use stale cache from previous runs - rm -rf /tmp/moko-platform-api - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ - /tmp/moko-platform-api - cd /tmp/moko-platform-api - composer install --no-dev --no-interaction --quiet - - - - name: "Publish stable release" - run: | - php /tmp/moko-platform-api/cli/release_publish.php \ - --path . --stability stable --bump minor --branch main \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" - - # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- - - name: "Step 9: Mirror release to GitHub" - if: >- - steps.version.outputs.skip != 'true' && - secrets.GH_MIRROR_TOKEN != '' - continue-on-error: true - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/release_mirror.php \ - --version "$VERSION" --tag "$RELEASE_TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \ - --branch main 2>&1 || true - echo "GitHub mirror updated" >> $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_MIRROR_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_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ - git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_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" - - - name: "Step 11: Delete rc branch and recreate dev 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.MOKOGITEA_TOKEN }}" - - # Delete rc branch (ephemeral — created by promote-rc) - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/branches/rc" 2>/dev/null \ - && echo "Deleted rc branch" || echo "rc branch not found" - - # 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 "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY - - - name: "Step 12: Create version 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.MOKOGITEA_TOKEN }}" - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - BRANCH_NAME="version/${VERSION}" - MAIN_SHA=$(git rev-parse HEAD) - - # Delete old version branch if it exists (same version re-release) - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}" - - # Create version/XX.YY.ZZ from main - curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed" - - echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY - - - - # -- Dolibarr post-release: Reset dev version ----------------------------- - - name: "Post-release: Reset dev version" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/version_reset_dev.php \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \ - --branch dev --path . 2>&1 || true - - # -- 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 519c735e204f738a96e943dfad303dd4d39b832f Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sun, 31 May 2026 03:47:24 +0000 Subject: [PATCH 05/27] chore(ci): remove pre-release.yml for update server migration [skip ci] --- .mokogitea/workflows/pre-release.yml | 233 --------------------------- 1 file changed, 233 deletions(-) delete mode 100644 .mokogitea/workflows/pre-release.yml diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml deleted file mode 100644 index 162b08f..0000000 --- a/.mokogitea/workflows/pre-release.yml +++ /dev/null @@ -1,233 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Release -# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform -# PATH: /templates/workflows/universal/pre-release.yml.template -# VERSION: 05.01.00 -# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch - -name: "Universal: Pre-Release" - -on: - pull_request: - types: [closed] - branches: - - dev - 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 || 'development' }})" - runs-on: release - if: >- - github.event_name == 'workflow_dispatch' || - (github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.MOKOGITEA_TOKEN }} - - - name: Setup moko-platform tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - run: | - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - # Always fetch latest CLI tools — never use stale cache from previous runs - rm -rf /tmp/moko-platform-api - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ - /tmp/moko-platform-api - cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet - echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" - - - name: Detect platform - id: platform - run: | - php ${MOKO_CLI}/manifest_read.php --path . --github-output - - - name: Resolve metadata and bump version - id: meta - run: | - STABILITY="${{ inputs.stability || 'development' }}" - - 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 current version (bump already handled by push workflow) - VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null) - [ -z "$VERSION" ] && VERSION="00.00.01" - - # Strip any existing suffix from version before applying stability - VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') - - php ${MOKO_CLI}/version_set_platform.php \ - --path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true - - # Verify version consistency across all files - php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true - - # Update VERSION variable with suffix - if [ -n "$SUFFIX" ]; then - VERSION="${VERSION}${SUFFIX}" - 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://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - git add -A - git diff --cached --quiet || { - git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]" - git push origin HEAD 2>&1 - } - - # Auto-detect element via manifest_element.php - php ${MOKO_CLI}/manifest_element.php \ - --path . --version "$VERSION" --stability "$STABILITY" \ - --repo "${GITEA_REPO}" --github-output - - # Read back element outputs - EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) - ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) - [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') - [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.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 "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ===" - - - name: Create release - id: release - run: | - TAG="${{ steps.meta.outputs.tag }}" - VERSION="${{ steps.meta.outputs.version }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php ${MOKO_CLI}/release_create.php \ - --path . --version "$VERSION" --tag "$TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --branch dev --prerelease - - - name: Build package and upload - id: package - run: | - VERSION="${{ steps.meta.outputs.version }}" - TAG="${{ steps.meta.outputs.tag }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php ${MOKO_CLI}/release_package.php \ - --path . --version "$VERSION" --tag "$TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --output /tmp || true - - - name: Update updates.xml - if: steps.platform.outputs.platform == 'joomla' - run: | - VERSION="${{ steps.meta.outputs.version }}" - STABILITY="${{ steps.meta.outputs.stability }}" - SHA256="${{ steps.package.outputs.sha256_zip }}" - - if [ ! -f "updates.xml" ]; then - echo "No updates.xml -- skipping" - exit 0 - fi - - SHA_FLAG="" - [ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}" - - php ${MOKO_CLI}/updates_xml_build.php \ - --path . --version "${VERSION}" --stability "${STABILITY}" \ - --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ - ${SHA_FLAG} - - # Commit and push - 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]" - - 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}" -- updates.xml 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.MOKOGITEA_TOKEN }}" - - php ${MOKO_CLI}/release_cascade.php \ - --stability "${{ steps.meta.outputs.stability }}" \ - --token "${TOKEN}" \ - --api-base "${API_BASE}" - - - name: Summary - if: always() - run: | - VERSION="${{ steps.meta.outputs.version }}" - STABILITY="${{ steps.meta.outputs.stability }}" - ZIP_NAME="${{ steps.meta.outputs.zip_name }}" - SHA256="${{ steps.package.outputs.sha256_zip }}" - echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY - echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY - echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY - echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY -- 2.52.0 From 79627c4f1d389c91b6d29dbd40756d28ebd0c1f6 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sun, 31 May 2026 03:47:29 +0000 Subject: [PATCH 06/27] chore(ci): remove auto-bump.yml for update server migration [skip ci] --- .mokogitea/workflows/auto-bump.yml | 66 ------------------------------ 1 file changed, 66 deletions(-) delete mode 100644 .mokogitea/workflows/auto-bump.yml diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml deleted file mode 100644 index fb9dc82..0000000 --- a/.mokogitea/workflows/auto-bump.yml +++ /dev/null @@ -1,66 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Release -# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform -# PATH: /.mokogitea/workflows/auto-bump.yml -# VERSION: 09.02.00 -# BRIEF: Auto patch-bump version on every push to dev (skips merge commits) - -name: "Universal: Auto Version Bump" - -on: - push: - branches: - - dev - - rc - - 'feature/**' - - 'patch/**' - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - -permissions: - contents: write - -jobs: - bump: - name: Version Bump - runs-on: release - if: >- - !contains(github.event.head_commit.message, '[skip ci]') && - !contains(github.event.head_commit.message, '[skip bump]') && - !startsWith(github.event.head_commit.message, 'Merge pull request') - - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.MOKOGITEA_TOKEN }} - fetch-depth: 1 - - - name: Setup moko-platform tools - run: | - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - if [ -d "/opt/moko-platform/cli" ]; then - echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV" - else - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \ - /tmp/moko-platform-api - cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet - echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" - fi - - - name: Bump version - run: | - php ${MOKO_CLI}/version_auto_bump.php \ - --path . --branch "${GITHUB_REF_NAME}" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" -- 2.52.0 From f1049fd289e49225e83a1346a67438d69ec31e16 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sun, 31 May 2026 03:47:31 +0000 Subject: [PATCH 07/27] chore(ci): remove cascade-dev.yml for update server migration [skip ci] --- .mokogitea/workflows/cascade-dev.yml | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 .mokogitea/workflows/cascade-dev.yml diff --git a/.mokogitea/workflows/cascade-dev.yml b/.mokogitea/workflows/cascade-dev.yml deleted file mode 100644 index 5f7c1d7..0000000 --- a/.mokogitea/workflows/cascade-dev.yml +++ /dev/null @@ -1,10 +0,0 @@ -# DISABLED — auto-release Step 11 recreates dev from main after every release. -# Cascade-dev is redundant and causes version conflicts when both main and dev -# have different version numbers in templateDetails.xml / manifest.xml. -name: "Cascade Main → Dev (DISABLED)" -on: workflow_dispatch -jobs: - noop: - runs-on: ubuntu-latest - steps: - - run: echo "Cascade disabled — auto-release handles dev recreation" -- 2.52.0 From 595bc94b4ba1318da7d2962a320c1705fc256690 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sun, 31 May 2026 03:47:33 +0000 Subject: [PATCH 08/27] chore(ci): remove update-server.yml for update server migration [skip ci] --- .mokogitea/workflows/update-server.yml | 312 ------------------------- 1 file changed, 312 deletions(-) delete mode 100644 .mokogitea/workflows/update-server.yml diff --git a/.mokogitea/workflows/update-server.yml b/.mokogitea/workflows/update-server.yml deleted file mode 100644 index 339d3f5..0000000 --- a/.mokogitea/workflows/update-server.yml +++ /dev/null @@ -1,312 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Universal -# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform -# PATH: /templates/workflows/update-server.yml -# VERSION: 05.00.00 -# BRIEF: Pre-release build + update server XML for dev/alpha/beta/rc branches -# -# Thin wrapper around moko-platform CLI tools. -# Builds packages, updates updates.xml, and optionally deploys via SFTP. -# -# Joomla filters update entries by the user's "Minimum Stability" setting. - -name: "Update Server" - -on: - push: - branches: - - 'dev' - - 'dev/**' - - 'alpha/**' - - 'beta/**' - - 'rc/**' - paths: - - 'src/**' - - 'htdocs/**' - pull_request: - types: [closed] - branches: - - 'dev' - - 'dev/**' - - 'alpha/**' - - 'beta/**' - - 'rc/**' - paths: - - 'src/**' - - 'htdocs/**' - workflow_dispatch: - inputs: - stability: - description: 'Stability tag' - required: true - default: 'development' - type: choice - options: - - development - - alpha - - beta - - rc - - stable - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} - -permissions: - contents: write - -jobs: - update-xml: - name: Update Server - runs-on: release - if: >- - github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push' - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - token: ${{ secrets.MOKOGITEA_TOKEN }} - fetch-depth: 0 - - - name: Setup moko-platform tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.MOKOGITEA_TOKEN }}"}}}' - run: | - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - # Always fetch latest CLI tools — never use stale cache from previous runs - rm -rf /tmp/moko-platform - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ - /tmp/moko-platform 2>/dev/null || true - if [ -d "/tmp/moko-platform" ] && [ -f "/tmp/moko-platform/composer.json" ]; then - cd /tmp/moko-platform && composer install --no-dev --no-interaction --quiet 2>/dev/null || true - fi - echo "MOKO_CLI=/tmp/moko-platform/cli" >> "$GITHUB_ENV" - - - name: Detect platform - id: platform - run: php ${MOKO_CLI}/manifest_read.php --path . --github-output - - - name: Resolve stability and bump version - id: meta - run: | - BRANCH="${{ github.ref_name }}" - - # Configure git for bot pushes - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" - git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - - # Auto-bump patch version - php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true - - VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "0.0.0") - - # Strip any existing suffix before applying stability - VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') - - # Determine stability from branch or manual input - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - STABILITY="${{ inputs.stability }}" - elif [[ "$BRANCH" == rc/* ]]; then - STABILITY="rc" - elif [[ "$BRANCH" == beta/* ]]; then - STABILITY="beta" - elif [[ "$BRANCH" == alpha/* ]]; then - STABILITY="alpha" - else - STABILITY="development" - fi - - # Version suffix per stability stream - case "$STABILITY" in - development) SUFFIX="-dev"; TAG="development" ;; - alpha) SUFFIX="-alpha"; TAG="alpha" ;; - beta) SUFFIX="-beta"; TAG="beta" ;; - rc) SUFFIX="-rc"; TAG="release-candidate" ;; - *) SUFFIX=""; TAG="stable" ;; - esac - - # Propagate version with stability suffix to all manifest files - php ${MOKO_CLI}/version_set_platform.php \ - --path . --version "$VERSION" --branch "$BRANCH" --stability "$STABILITY" 2>/dev/null || true - php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true - - # Re-read version (now includes suffix from version_set_platform) - if [ -n "$SUFFIX" ]; then - VERSION="${VERSION}${SUFFIX}" - fi - - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" - echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" - echo "tag=${TAG}" >> "$GITHUB_OUTPUT" - echo "display_version=${VERSION}" >> "$GITHUB_OUTPUT" - - # Commit version bump if changed - git add -A - git diff --cached --quiet || { - git commit -m "chore(version): auto-bump ${VERSION} [skip ci]" \ - --author="gitea-actions[bot] " - git push - } - - - name: Create release and upload package - id: package - run: | - VERSION="${{ steps.meta.outputs.version }}" - TAG="${{ steps.meta.outputs.tag }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - - # Create or update Gitea release - php ${MOKO_CLI}/release_create.php \ - --path . --version "$VERSION" --tag "$TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease - - # Build package and upload - php ${MOKO_CLI}/release_package.php \ - --path . --version "$VERSION" --tag "$TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --output /tmp || true - - - name: Update updates.xml - if: steps.platform.outputs.platform == 'joomla' - run: | - VERSION="${{ steps.meta.outputs.version }}" - STABILITY="${{ steps.meta.outputs.stability }}" - SHA256="${{ steps.package.outputs.sha256_zip }}" - - if [ ! -f "updates.xml" ]; then - echo "No updates.xml — skipping" - exit 0 - fi - - SHA_FLAG="" - [ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}" - - php ${MOKO_CLI}/updates_xml_build.php \ - --path . --version "${VERSION}" --stability "${STABILITY}" \ - --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ - ${SHA_FLAG} - - # Commit and push updates.xml - git add updates.xml - git diff --cached --quiet || { - git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]" - git push - } - - - name: Sync updates.xml to main - if: github.ref_name != 'main' && steps.platform.outputs.platform == 'joomla' - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - - FILE_SHA=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \ - "${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true) - - if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then - python3 -c " - import base64, json, urllib.request, sys - with open('updates.xml', 'rb') as f: - content = base64.b64encode(f.read()).decode() - payload = json.dumps({ - 'content': content, - 'sha': '${FILE_SHA}', - 'message': 'chore: sync updates.xml from ${{ steps.meta.outputs.stability }} [skip ci]', - 'branch': 'main' - }).encode() - req = urllib.request.Request( - '${API_BASE}/contents/updates.xml', - data=payload, method='PUT', - headers={ - 'Authorization': 'token ${GITEA_TOKEN}', - 'Content-Type': 'application/json' - }) - try: - urllib.request.urlopen(req) - print('updates.xml synced to main') - except Exception as e: - print(f'WARNING: sync to main failed: {e}', file=sys.stderr) - " - fi - - - name: SFTP deploy to dev server - if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev' - env: - DEV_HOST: ${{ vars.DEV_FTP_HOST }} - DEV_PATH: ${{ vars.DEV_FTP_PATH }} - DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }} - DEV_USER: ${{ vars.DEV_FTP_USERNAME }} - DEV_PORT: ${{ vars.DEV_FTP_PORT }} - DEV_KEY: ${{ secrets.DEV_FTP_KEY }} - DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }} - run: | - # Permission check: admin or maintain role required - ACTOR="${{ github.actor }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - - PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ - "${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \ - python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read") - case "$PERMISSION" in - admin|maintain|write) ;; - *) - echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write" - exit 0 - ;; - esac - - [ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; } - - SOURCE_DIR="src" - [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" - [ ! -d "$SOURCE_DIR" ] && exit 0 - - PORT="${DEV_PORT:-22}" - REMOTE="${DEV_PATH%/}" - [ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}" - - printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \ - "$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json - if [ -n "$DEV_KEY" ]; then - echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key - printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json - else - printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json - fi - - PLATFORM=$(php ${MOKO_CLI}/platform_detect.php --path . 2>/dev/null || true) - if [ "$PLATFORM" = "waas-component" ] && [ -f "${MOKO_CLI}/../deploy/deploy-joomla.php" ]; then - php ${MOKO_CLI}/../deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json - elif [ -f "${MOKO_CLI}/../deploy/deploy-sftp.php" ]; then - php ${MOKO_CLI}/../deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json - fi - rm -f /tmp/deploy_key /tmp/sftp-config.json - echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY - - - name: Summary - if: always() - run: | - VERSION="${{ steps.meta.outputs.version }}" - STABILITY="${{ steps.meta.outputs.stability }}" - DISPLAY="${{ steps.meta.outputs.display_version }}" - echo "## Update Server" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY - echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Version | \`${DISPLAY}\` |" >> $GITHUB_STEP_SUMMARY -- 2.52.0 From b85921697f79ec7272e22dd11c28b2fcb9541f12 Mon Sep 17 00:00:00 2001 From: Moko Consulting Date: Tue, 2 Jun 2026 20:36:43 +0000 Subject: [PATCH 09/27] chore(ci): add CI issue reporter for auto-filing gate failures --- .mokogitea/workflows/repo-health.yml | 1586 +++++++++++++------------- 1 file changed, 817 insertions(+), 769 deletions(-) diff --git a/.mokogitea/workflows/repo-health.yml b/.mokogitea/workflows/repo-health.yml index be52e37..b23d971 100644 --- a/.mokogitea/workflows/repo-health.yml +++ b/.mokogitea/workflows/repo-health.yml @@ -1,769 +1,817 @@ -# ============================================================================ -# 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: moko-platform.Validation -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform -# 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: "Generic: Repo Health" - -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,.mokogitea/workflows/ - REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/ - REPO_DISALLOWED_DIRS: - REPO_DISALLOWED_FILES: TODO.md,todo.md - - # Extended checks toggles - EXTENDED_CHECKS: "true" - - # File / directory variables - DOCS_INDEX: docs/docs-index.md - SCRIPT_DIR: scripts - WORKFLOWS_DIR: .mokogitea/workflows - SHELLCHECK_PATTERN: '*.sh' - SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml' - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - access_check: - name: Access control - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: - contents: read - - outputs: - allowed: ${{ steps.perm.outputs.allowed }} - permission: ${{ steps.perm.outputs.permission }} - - steps: - - name: Check actor permission (admin only) - id: perm - env: - TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }} - REPO: ${{ github.repository }} - ACTOR: ${{ github.actor }} - run: | - set -euo pipefail - ALLOWED=false - PERMISSION=unknown - METHOD="" - - # Hardcoded authorized users — always allowed - case "$ACTOR" in - jmiller|gitea-actions[bot]) - ALLOWED=true - PERMISSION=admin - METHOD="hardcoded allowlist" - ;; - *) - # Detect platform and check permissions via API - API_BASE="${GITHUB_API_URL:-${GITEA_API_URL:-https://api.github.com}}" - RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}') - PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown") - if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "maintain" ] || [ "$PERMISSION" = "owner" ]; then - ALLOWED=true - fi - METHOD="collaborator API" - ;; - esac - - echo "permission=${PERMISSION}" >> "$GITHUB_OUTPUT" - echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT" - - { - echo "## Access Authorization" - echo "" - echo "| Field | Value |" - echo "|-------|-------|" - echo "| **Actor** | \`${ACTOR}\` |" - echo "| **Repository** | \`${REPO}\` |" - echo "| **Permission** | \`${PERMISSION}\` |" - echo "| **Method** | ${METHOD} |" - echo "| **Authorized** | ${ALLOWED} |" - echo "" - if [ "$ALLOWED" = "true" ]; then - echo "${ACTOR} authorized (${METHOD})" - else - echo "${ACTOR} is NOT authorized. Requires admin or maintain role." - fi - } >> "${GITHUB_STEP_SUMMARY}" - - - name: Deny execution when not permitted - if: ${{ steps.perm.outputs.allowed != 'true' }} - run: | - set -euo pipefail - printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}" - exit 1 - - 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 - - if [ -n "${SCRIPTS_REQUIRED_DIRS:-}" ]; then IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"; else required_dirs=(); fi - IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}" - - missing_dirs=() - unapproved_dirs=() - - for d in "${required_dirs[@]}"; do - req="${d%/}" - [ ! -d "${req}" ] && missing_dirs+=("${req}/") - done - - while IFS= read -r d; do - allowed=false - for a in "${allowed_dirs[@]}"; do - a_norm="${a%/}" - [ "${d%/}" = "${a_norm}" ] && allowed=true - done - [ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/") - done < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##') - - { - printf '%s\n' '### Scripts governance' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' '| Area | Status | Notes |' - printf '%s\n' '|---|---|---|' - - if [ "${#missing_dirs[@]}" -gt 0 ]; then - printf '%s\n' '| Required directories | Warning | Missing required subfolders |' - else - printf '%s\n' '| Required directories | OK | All required subfolders present |' - fi - - if [ "${#unapproved_dirs[@]}" -gt 0 ]; then - printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |' - else - printf '%s\n' '| Directory policy | OK | No unapproved directories |' - fi - - printf '%s\n' '| Enforcement mode | Advisory | scripts folder is optional |' - printf '\n' - - if [ "${#missing_dirs[@]}" -gt 0 ]; then - printf '%s\n' 'Missing required script directories:' - for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done - printf '\n' - else - printf '%s\n' 'Missing required script directories: none.' - printf '\n' - fi - - if [ "${#unapproved_dirs[@]}" -gt 0 ]; then - printf '%s\n' 'Unapproved script directories detected:' - for m in "${unapproved_dirs[@]}"; do printf '%s\n' "- ${m}"; done - printf '\n' - else - printf '%s\n' 'Unapproved script directories detected: none.' - printf '\n' - fi - - printf '%s\n' 'Scripts governance completed in advisory mode.' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - - repo_health: - name: Repository health - needs: access_check - if: ${{ needs.access_check.outputs.allowed == 'true' }} - runs-on: ubuntu-latest - timeout-minutes: 20 - permissions: - contents: read - - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - fetch-depth: 0 - - - name: Repository health checks - env: - PROFILE_RAW: ${{ github.event.inputs.profile }} - run: | - set -euo pipefail - - profile="${PROFILE_RAW:-all}" - case "${profile}" in - all|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 - - IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}" - IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}" - if [ -n "${REPO_DISALLOWED_DIRS:-}" ]; then IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"; else disallowed_dirs=(); fi - IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES:-}" - - missing_required=() - missing_optional=() - - # Source directory: src/ or htdocs/ (either is valid for extension repos) - SOURCE_DIR="" - if [ -d "src" ]; then - SOURCE_DIR="src" - elif [ -d "htdocs" ]; then - SOURCE_DIR="htdocs" - elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then - # Platform/tooling repos don't need src/ - SOURCE_DIR="" - else - missing_required+=("src/ or htdocs/ (source directory required)") - fi - - for item in "${required_artifacts[@]}"; do - if printf '%s' "${item}" | grep -q '/$'; then - d="${item%/}" - [ ! -d "${d}" ] && missing_required+=("${item}") - else - [ ! -f "${item}" ] && missing_required+=("${item}") - fi - done - - for f in "${optional_files[@]}"; do - if printf '%s' "${f}" | grep -q '/$'; then - d="${f%/}" - [ ! -d "${d}" ] && missing_optional+=("${f}") - else - [ ! -f "${f}" ] && missing_optional+=("${f}") - fi - done - - for d in "${disallowed_dirs[@]}"; do - d_norm="${d%/}" - [ -d "${d_norm}" ] && missing_required+=("${d_norm}/ (disallowed)") - done - - for f in "${disallowed_files[@]}"; do - [ -f "${f}" ] && missing_required+=("${f} (disallowed)") - done - - git fetch origin --prune - - dev_paths=() - dev_branches=() - - while IFS= read -r b; do - name="${b#origin/}" - if [ "${name}" = 'dev' ]; then - dev_branches+=("${name}") - else - dev_paths+=("${name}") - fi - done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//') - - if [ "${#dev_paths[@]}" -eq 0 ] && [ "${#dev_branches[@]}" -eq 0 ]; then - missing_required+=("dev or dev/* branch") - fi - - content_warnings=() - - if [ -f 'CHANGELOG.md' ] && ! grep -Eq '^# Changelog' CHANGELOG.md; then - content_warnings+=("CHANGELOG.md missing '# Changelog' header") - fi - - if [ -f 'CHANGELOG.md' ] && grep -Eq '^[# ]*Unreleased' CHANGELOG.md; then - content_warnings+=("CHANGELOG.md contains Unreleased section (review release readiness)") - fi - - if [ -f 'LICENSE' ] && ! grep -qiE 'GNU GENERAL PUBLIC LICENSE|GPL' LICENSE; then - content_warnings+=("LICENSE does not look like a GPL text") - fi - - if [ -f 'README.md' ] && ! grep -qiE 'moko|Moko' README.md; then - content_warnings+=("README.md missing expected brand keyword") - fi - - export PROFILE_RAW="${profile}" - export MISSING_REQUIRED="$(printf '%s\n' "${missing_required[@]:-}")" - export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")" - export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")" - - report_json=$(printf '{"profile":"%s","missing_required":%d,"missing_optional":%d,"content_warnings":%d}' "$profile" "${#missing_required[@]}" "${#missing_optional[@]}" "${#content_warnings[@]}") - - { - printf '%s\n' '### Repository health' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' '| Metric | Value |' - printf '%s\n' '|---|---|' - printf '%s\n' "| Missing required | ${#missing_required[@]} |" - printf '%s\n' "| Missing optional | ${#missing_optional[@]} |" - printf '%s\n' "| Content warnings | ${#content_warnings[@]} |" - printf '\n' - - printf '%s\n' '### Guardrails report (JSON)' - printf '%s\n' '```json' - printf '%s\n' "${report_json}" - printf '%s\n' '```' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - - if [ "${#missing_required[@]}" -gt 0 ]; then - { - printf '%s\n' '### Missing required repo artifacts' - for m in "${missing_required[@]}"; do printf '%s\n' "- ${m}"; done - printf '%s\n' 'ERROR: Guardrails failed. Missing required repository artifacts.' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - exit 1 - fi - - if [ "${#missing_optional[@]}" -gt 0 ]; then - { - printf '%s\n' '### Missing optional repo artifacts' - for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - if [ "${#content_warnings[@]}" -gt 0 ]; then - { - printf '%s\n' '### Repo content warnings' - for m in "${content_warnings[@]}"; do printf '%s\n' "- ${m}"; done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - # -- Joomla-specific checks -- - joomla_findings=() - - MANIFEST="$(find . -maxdepth 2 -name '*.xml' -exec grep -l '/dev/null | head -1 || true)" - if [ -z "${MANIFEST}" ]; then - joomla_findings+=("Joomla XML manifest not found (no *.xml with tag)") - else - if ! grep -qP '' "${MANIFEST}"; then - joomla_findings+=("XML manifest: tag missing") - fi - if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then - joomla_findings+=("XML manifest: type attribute missing or invalid") - fi - if ! grep -qP '' "${MANIFEST}"; then - joomla_findings+=("XML manifest: tag missing") - fi - if ! grep -qP '' "${MANIFEST}"; then - joomla_findings+=("XML manifest: tag missing") - fi - if ! grep -qP ' missing (required for Joomla 5+)") - fi - fi - - INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)" - if [ "${INI_COUNT}" -eq 0 ]; then - joomla_findings+=("No .ini language files found") - fi - - if [ ! -f 'updates.xml' ]; then - joomla_findings+=("updates.xml missing in root (required for Joomla update server)") - fi - - if [ -n "${SOURCE_DIR}" ]; then - INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site") - for dir in "${INDEX_DIRS[@]}"; do - if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then - joomla_findings+=("${dir}/index.html missing (directory listing protection)") - fi - done - fi - - if [ "${#joomla_findings[@]}" -gt 0 ]; then - { - printf '%s\n' '### Joomla extension checks' - printf '%s\n' '| Check | Status |' - printf '%s\n' '|---|---|' - for f in "${joomla_findings[@]}"; do - printf '%s\n' "| ${f} | Warning |" - done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - else - { - printf '%s\n' '### Joomla extension checks' - printf '%s\n' 'All Joomla-specific checks passed.' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - extended_enabled="${EXTENDED_CHECKS:-true}" - extended_findings=() - - if [ "${extended_enabled}" = 'true' ]; then - if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then - : - else - extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)") - fi - - if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then - bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)" - if [ -n "${bad_refs}" ]; then - extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt") - { - printf '%s\n' '### Workflow pinning advisory' - printf '%s\n' 'Found uses: entries pinned to main/master:' - printf '%s\n' '```' - printf '%s\n' "${bad_refs}" - printf '%s\n' '```' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - fi - - if [ -f "${DOCS_INDEX}" ]; then - missing_links="" - while IFS= read -r docline; do - for link in $(echo "$docline" | grep -oE '\]\([^)]+\)' | sed 's/\](//' | sed 's/)$//' || true); do - case "$link" in http://*|https://*|"#"*|mailto:*) continue ;; esac - linkpath="${link%%#*}" - linkpath="${linkpath%%\?*}" - [ -z "$linkpath" ] && continue - if [ "${linkpath:0:1}" = "/" ]; then - testpath="${linkpath#/}" - else - testpath="$(dirname "${DOCS_INDEX}")/${linkpath}" - fi - [ ! -e "$testpath" ] && missing_links="${missing_links}${testpath} " - done - done < "${DOCS_INDEX}" - if [ -n "${missing_links}" ]; then - extended_findings+=("docs/docs-index.md contains broken relative links") - { - printf '%s\n' '### Docs index link integrity' - printf '%s\n' 'Broken relative links:' - for bl in ${missing_links}; do - printf '%s\n' "- ${bl}" - done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - fi - - if [ -d "${SCRIPT_DIR}" ]; then - if ! command -v shellcheck >/dev/null 2>&1; then - sudo apt-get update -qq - sudo apt-get install -y shellcheck >/dev/null - fi - - sc_out='' - while IFS= read -r shf; do - [ -z "${shf}" ] && continue - out_one="$(shellcheck -S warning -x "${shf}" 2>/dev/null || true)" - if [ -n "${out_one}" ]; then - sc_out="${sc_out}${out_one}\n" - fi - done < <(find "${SCRIPT_DIR}" -type f -name "${SHELLCHECK_PATTERN}" 2>/dev/null | sort) - - if [ -n "${sc_out}" ]; then - extended_findings+=("ShellCheck warnings detected (advisory)") - sc_head="$(printf '%s' "${sc_out}" | head -n 200)" - { - printf '%s\n' '### ShellCheck (advisory)' - printf '%s\n' '```' - printf '%s\n' "${sc_head}" - printf '%s\n' '```' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - fi - - spdx_missing=() - IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}" - spdx_args=() - for g in "${spdx_globs[@]}"; do spdx_args+=("${g}"); done - - while IFS= read -r f; do - [ -z "${f}" ] && continue - if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then - spdx_missing+=("${f}") - fi - done < <(git ls-files "${spdx_args[@]}" 2>/dev/null || true) - - if [ "${#spdx_missing[@]}" -gt 0 ]; then - extended_findings+=("SPDX header missing in some tracked files (advisory)") - { - printf '%s\n' '### SPDX header advisory' - printf '%s\n' 'Files missing SPDX-License-Identifier (first 40 lines scan):' - for f in "${spdx_missing[@]}"; do printf '%s\n' "- ${f}"; done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - stale_cutoff_days=180 - stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 > days*86400) print $1}' | head -50)" - if [ -n "${stale_branches}" ]; then - extended_findings+=("Stale remote branches detected (advisory)") - { - printf '%s\n' '### Git hygiene advisory' - printf '%s\n' "Branches with last commit older than ${stale_cutoff_days} days (sample up to 50):" - while IFS= read -r b; do [ -n "${b}" ] && printf '%s\n' "- ${b}"; done <<< "${stale_branches}" - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - fi - - { - printf '%s\n' '### Guardrails coverage matrix' - printf '%s\n' '| Domain | Status | Notes |' - printf '%s\n' '|---|---|---|' - printf '%s\n' '| Access control | OK | Admin-only execution gate |' - printf '%s\n' '| Release 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}" - - - site-health: - name: Site Health - runs-on: ubuntu-latest - if: github.event_name == 'workflow_dispatch' - steps: - - uses: actions/checkout@v4 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.3' - - - name: Uptime check - if: env.URLS != '' - run: | - echo "$URLS" > /tmp/urls.txt - php monitoring/uptime-probe.php --urls /tmp/urls.txt --timeout 15 || echo "::warning::Some sites are down" - rm -f /tmp/urls.txt - env: - URLS: ${{ vars.MONITORED_URLS }} - - - name: SSL certificate check - if: env.DOMAINS != '' - run: | - echo "$DOMAINS" > /tmp/domains.txt - php monitoring/ssl-check.php --domains /tmp/domains.txt --warn-days 30 || echo "::warning::SSL certificates expiring soon" - rm -f /tmp/domains.txt - env: - DOMAINS: ${{ vars.MONITORED_DOMAINS }} - - - name: Summary - if: always() - run: | - echo "### Site Health" >> $GITHUB_STEP_SUMMARY - echo "Uptime and SSL checks completed." >> $GITHUB_STEP_SUMMARY - +# ============================================================================ +# 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: moko-platform.Validation +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform +# PATH: /templates/workflows/joomla/repo_health.yml.template +# VERSION: 09.23.00 +# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts. +# ============================================================================ + +name: "Generic: Repo Health" + +defaults: + run: + shell: bash + +on: + workflow_dispatch: + inputs: + profile: + description: 'Validation profile: all, 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,.mokogitea/workflows/ + REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/ + REPO_DISALLOWED_DIRS: + REPO_DISALLOWED_FILES: TODO.md,todo.md + + # Extended checks toggles + EXTENDED_CHECKS: "true" + + # File / directory variables + DOCS_INDEX: docs/docs-index.md + SCRIPT_DIR: scripts + WORKFLOWS_DIR: .mokogitea/workflows + SHELLCHECK_PATTERN: '*.sh' + SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml' + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + access_check: + name: Access control + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + + outputs: + allowed: ${{ steps.perm.outputs.allowed }} + permission: ${{ steps.perm.outputs.permission }} + + steps: + - name: Check actor permission (admin only) + id: perm + env: + TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }} + REPO: ${{ github.repository }} + ACTOR: ${{ github.actor }} + run: | + set -euo pipefail + ALLOWED=false + PERMISSION=unknown + METHOD="" + + # Hardcoded authorized users — always allowed + case "$ACTOR" in + jmiller|gitea-actions[bot]) + ALLOWED=true + PERMISSION=admin + METHOD="hardcoded allowlist" + ;; + *) + # Detect platform and check permissions via API + API_BASE="${GITHUB_API_URL:-${GITEA_API_URL:-https://api.github.com}}" + RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}') + PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown") + if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "maintain" ] || [ "$PERMISSION" = "owner" ]; then + ALLOWED=true + fi + METHOD="collaborator API" + ;; + esac + + echo "permission=${PERMISSION}" >> "$GITHUB_OUTPUT" + echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT" + + { + echo "## Access Authorization" + echo "" + echo "| Field | Value |" + echo "|-------|-------|" + echo "| **Actor** | \`${ACTOR}\` |" + echo "| **Repository** | \`${REPO}\` |" + echo "| **Permission** | \`${PERMISSION}\` |" + echo "| **Method** | ${METHOD} |" + echo "| **Authorized** | ${ALLOWED} |" + echo "" + if [ "$ALLOWED" = "true" ]; then + echo "${ACTOR} authorized (${METHOD})" + else + echo "${ACTOR} is NOT authorized. Requires admin or maintain role." + fi + } >> "${GITHUB_STEP_SUMMARY}" + + - name: Deny execution when not permitted + if: ${{ steps.perm.outputs.allowed != 'true' }} + run: | + set -euo pipefail + printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}" + exit 1 + + 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 + + if [ -n "${SCRIPTS_REQUIRED_DIRS:-}" ]; then IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"; else required_dirs=(); fi + IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}" + + missing_dirs=() + unapproved_dirs=() + + for d in "${required_dirs[@]}"; do + req="${d%/}" + [ ! -d "${req}" ] && missing_dirs+=("${req}/") + done + + while IFS= read -r d; do + allowed=false + for a in "${allowed_dirs[@]}"; do + a_norm="${a%/}" + [ "${d%/}" = "${a_norm}" ] && allowed=true + done + [ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/") + done < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##') + + { + printf '%s\n' '### Scripts governance' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' '| Area | Status | Notes |' + printf '%s\n' '|---|---|---|' + + if [ "${#missing_dirs[@]}" -gt 0 ]; then + printf '%s\n' '| Required directories | Warning | Missing required subfolders |' + else + printf '%s\n' '| Required directories | OK | All required subfolders present |' + fi + + if [ "${#unapproved_dirs[@]}" -gt 0 ]; then + printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |' + else + printf '%s\n' '| Directory policy | OK | No unapproved directories |' + fi + + printf '%s\n' '| Enforcement mode | Advisory | scripts folder is optional |' + printf '\n' + + if [ "${#missing_dirs[@]}" -gt 0 ]; then + printf '%s\n' 'Missing required script directories:' + for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + else + printf '%s\n' 'Missing required script directories: none.' + printf '\n' + fi + + if [ "${#unapproved_dirs[@]}" -gt 0 ]; then + printf '%s\n' 'Unapproved script directories detected:' + for m in "${unapproved_dirs[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + else + printf '%s\n' 'Unapproved script directories detected: none.' + printf '\n' + fi + + printf '%s\n' 'Scripts governance completed in advisory mode.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + repo_health: + name: Repository health + needs: access_check + if: ${{ needs.access_check.outputs.allowed == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + + - name: Repository health checks + env: + PROFILE_RAW: ${{ github.event.inputs.profile }} + run: | + set -euo pipefail + + profile="${PROFILE_RAW:-all}" + case "${profile}" in + all|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 + + IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}" + IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}" + if [ -n "${REPO_DISALLOWED_DIRS:-}" ]; then IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"; else disallowed_dirs=(); fi + IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES:-}" + + missing_required=() + missing_optional=() + + # Source directory: src/ or htdocs/ (either is valid for extension repos) + SOURCE_DIR="" + if [ -d "src" ]; then + SOURCE_DIR="src" + elif [ -d "htdocs" ]; then + SOURCE_DIR="htdocs" + elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then + # Platform/tooling repos don't need src/ + SOURCE_DIR="" + else + missing_required+=("src/ or htdocs/ (source directory required)") + fi + + for item in "${required_artifacts[@]}"; do + if printf '%s' "${item}" | grep -q '/$'; then + d="${item%/}" + [ ! -d "${d}" ] && missing_required+=("${item}") + else + [ ! -f "${item}" ] && missing_required+=("${item}") + fi + done + + for f in "${optional_files[@]}"; do + if printf '%s' "${f}" | grep -q '/$'; then + d="${f%/}" + [ ! -d "${d}" ] && missing_optional+=("${f}") + else + [ ! -f "${f}" ] && missing_optional+=("${f}") + fi + done + + for d in "${disallowed_dirs[@]}"; do + d_norm="${d%/}" + [ -d "${d_norm}" ] && missing_required+=("${d_norm}/ (disallowed)") + done + + for f in "${disallowed_files[@]}"; do + [ -f "${f}" ] && missing_required+=("${f} (disallowed)") + done + + git fetch origin --prune + + dev_paths=() + dev_branches=() + + while IFS= read -r b; do + name="${b#origin/}" + if [ "${name}" = 'dev' ]; then + dev_branches+=("${name}") + else + dev_paths+=("${name}") + fi + done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//') + + if [ "${#dev_paths[@]}" -eq 0 ] && [ "${#dev_branches[@]}" -eq 0 ]; then + missing_required+=("dev or dev/* branch") + fi + + content_warnings=() + + if [ -f 'CHANGELOG.md' ] && ! grep -Eq '^# Changelog' CHANGELOG.md; then + content_warnings+=("CHANGELOG.md missing '# Changelog' header") + fi + + if [ -f 'CHANGELOG.md' ] && grep -Eq '^[# ]*Unreleased' CHANGELOG.md; then + content_warnings+=("CHANGELOG.md contains Unreleased section (review release readiness)") + fi + + if [ -f 'LICENSE' ] && ! grep -qiE 'GNU GENERAL PUBLIC LICENSE|GPL' LICENSE; then + content_warnings+=("LICENSE does not look like a GPL text") + fi + + if [ -f 'README.md' ] && ! grep -qiE 'moko|Moko' README.md; then + content_warnings+=("README.md missing expected brand keyword") + fi + + export PROFILE_RAW="${profile}" + export MISSING_REQUIRED="$(printf '%s\n' "${missing_required[@]:-}")" + export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")" + export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")" + + report_json=$(printf '{"profile":"%s","missing_required":%d,"missing_optional":%d,"content_warnings":%d}' "$profile" "${#missing_required[@]}" "${#missing_optional[@]}" "${#content_warnings[@]}") + + { + printf '%s\n' '### Repository health' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' '| Metric | Value |' + printf '%s\n' '|---|---|' + printf '%s\n' "| Missing required | ${#missing_required[@]} |" + printf '%s\n' "| Missing optional | ${#missing_optional[@]} |" + printf '%s\n' "| Content warnings | ${#content_warnings[@]} |" + printf '\n' + + printf '%s\n' '### Guardrails report (JSON)' + printf '%s\n' '```json' + printf '%s\n' "${report_json}" + printf '%s\n' '```' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + if [ "${#missing_required[@]}" -gt 0 ]; then + { + printf '%s\n' '### Missing required repo artifacts' + for m in "${missing_required[@]}"; do printf '%s\n' "- ${m}"; done + printf '%s\n' 'ERROR: Guardrails failed. Missing required repository artifacts.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 1 + fi + + if [ "${#missing_optional[@]}" -gt 0 ]; then + { + printf '%s\n' '### Missing optional repo artifacts' + for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + if [ "${#content_warnings[@]}" -gt 0 ]; then + { + printf '%s\n' '### Repo content warnings' + for m in "${content_warnings[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + # -- Joomla-specific checks -- + joomla_findings=() + + MANIFEST="$(find . -maxdepth 2 -name '*.xml' -exec grep -l '/dev/null | head -1 || true)" + if [ -z "${MANIFEST}" ]; then + joomla_findings+=("Joomla XML manifest not found (no *.xml with tag)") + else + if ! grep -qP '' "${MANIFEST}"; then + joomla_findings+=("XML manifest: tag missing") + fi + if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then + joomla_findings+=("XML manifest: type attribute missing or invalid") + fi + if ! grep -qP '' "${MANIFEST}"; then + joomla_findings+=("XML manifest: tag missing") + fi + if ! grep -qP '' "${MANIFEST}"; then + joomla_findings+=("XML manifest: tag missing") + fi + if ! grep -qP ' missing (required for Joomla 5+)") + fi + fi + + INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)" + if [ "${INI_COUNT}" -eq 0 ]; then + joomla_findings+=("No .ini language files found") + fi + + if [ ! -f 'updates.xml' ]; then + joomla_findings+=("updates.xml missing in root (required for Joomla update server)") + fi + + if [ -n "${SOURCE_DIR}" ]; then + INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site") + for dir in "${INDEX_DIRS[@]}"; do + if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then + joomla_findings+=("${dir}/index.html missing (directory listing protection)") + fi + done + fi + + if [ "${#joomla_findings[@]}" -gt 0 ]; then + { + printf '%s\n' '### Joomla extension checks' + printf '%s\n' '| Check | Status |' + printf '%s\n' '|---|---|' + for f in "${joomla_findings[@]}"; do + printf '%s\n' "| ${f} | Warning |" + done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + else + { + printf '%s\n' '### Joomla extension checks' + printf '%s\n' 'All Joomla-specific checks passed.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + extended_enabled="${EXTENDED_CHECKS:-true}" + extended_findings=() + + if [ "${extended_enabled}" = 'true' ]; then + if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then + : + else + extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)") + fi + + if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then + bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)" + if [ -n "${bad_refs}" ]; then + extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt") + { + printf '%s\n' '### Workflow pinning advisory' + printf '%s\n' 'Found uses: entries pinned to main/master:' + printf '%s\n' '```' + printf '%s\n' "${bad_refs}" + printf '%s\n' '```' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + if [ -f "${DOCS_INDEX}" ]; then + missing_links="" + while IFS= read -r docline; do + for link in $(echo "$docline" | grep -oE '\]\([^)]+\)' | sed 's/\](//' | sed 's/)$//' || true); do + case "$link" in http://*|https://*|"#"*|mailto:*) continue ;; esac + linkpath="${link%%#*}" + linkpath="${linkpath%%\?*}" + [ -z "$linkpath" ] && continue + if [ "${linkpath:0:1}" = "/" ]; then + testpath="${linkpath#/}" + else + testpath="$(dirname "${DOCS_INDEX}")/${linkpath}" + fi + [ ! -e "$testpath" ] && missing_links="${missing_links}${testpath} " + done + done < "${DOCS_INDEX}" + if [ -n "${missing_links}" ]; then + extended_findings+=("docs/docs-index.md contains broken relative links") + { + printf '%s\n' '### Docs index link integrity' + printf '%s\n' 'Broken relative links:' + for bl in ${missing_links}; do + printf '%s\n' "- ${bl}" + done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + if [ -d "${SCRIPT_DIR}" ]; then + if ! command -v shellcheck >/dev/null 2>&1; then + sudo apt-get update -qq + sudo apt-get install -y shellcheck >/dev/null + fi + + sc_out='' + while IFS= read -r shf; do + [ -z "${shf}" ] && continue + out_one="$(shellcheck -S warning -x "${shf}" 2>/dev/null || true)" + if [ -n "${out_one}" ]; then + sc_out="${sc_out}${out_one}\n" + fi + done < <(find "${SCRIPT_DIR}" -type f -name "${SHELLCHECK_PATTERN}" 2>/dev/null | sort) + + if [ -n "${sc_out}" ]; then + extended_findings+=("ShellCheck warnings detected (advisory)") + sc_head="$(printf '%s' "${sc_out}" | head -n 200)" + { + printf '%s\n' '### ShellCheck (advisory)' + printf '%s\n' '```' + printf '%s\n' "${sc_head}" + printf '%s\n' '```' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + spdx_missing=() + IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}" + spdx_args=() + for g in "${spdx_globs[@]}"; do spdx_args+=("${g}"); done + + while IFS= read -r f; do + [ -z "${f}" ] && continue + if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then + spdx_missing+=("${f}") + fi + done < <(git ls-files "${spdx_args[@]}" 2>/dev/null || true) + + if [ "${#spdx_missing[@]}" -gt 0 ]; then + extended_findings+=("SPDX header missing in some tracked files (advisory)") + { + printf '%s\n' '### SPDX header advisory' + printf '%s\n' 'Files missing SPDX-License-Identifier (first 40 lines scan):' + for f in "${spdx_missing[@]}"; do printf '%s\n' "- ${f}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + stale_cutoff_days=180 + stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 > days*86400) print $1}' | head -50)" + if [ -n "${stale_branches}" ]; then + extended_findings+=("Stale remote branches detected (advisory)") + { + printf '%s\n' '### Git hygiene advisory' + printf '%s\n' "Branches with last commit older than ${stale_cutoff_days} days (sample up to 50):" + while IFS= read -r b; do [ -n "${b}" ] && printf '%s\n' "- ${b}"; done <<< "${stale_branches}" + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + { + printf '%s\n' '### Guardrails coverage matrix' + printf '%s\n' '| Domain | Status | Notes |' + printf '%s\n' '|---|---|---|' + printf '%s\n' '| Access control | OK | Admin-only execution gate |' + printf '%s\n' '| Release 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}" + + + site-health: + name: Site Health + runs-on: ubuntu-latest + if: github.event_name == 'workflow_dispatch' + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + + - name: Uptime check + if: env.URLS != '' + run: | + echo "$URLS" > /tmp/urls.txt + php monitoring/uptime-probe.php --urls /tmp/urls.txt --timeout 15 || echo "::warning::Some sites are down" + rm -f /tmp/urls.txt + env: + URLS: ${{ vars.MONITORED_URLS }} + + - name: SSL certificate check + if: env.DOMAINS != '' + run: | + echo "$DOMAINS" > /tmp/domains.txt + php monitoring/ssl-check.php --domains /tmp/domains.txt --warn-days 30 || echo "::warning::SSL certificates expiring soon" + rm -f /tmp/domains.txt + env: + DOMAINS: ${{ vars.MONITORED_DOMAINS }} + + - name: Summary + if: always() + run: | + echo "### Site Health" >> $GITHUB_STEP_SUMMARY + echo "Uptime and SSL checks completed." >> $GITHUB_STEP_SUMMARY + + # ═══════════════════════════════════════════════════════════════════════ + # Issue Reporter — file issues for failed gates + # ═══════════════════════════════════════════════════════════════════════ + report-issues: + name: "Report Issues" + runs-on: ubuntu-latest + needs: [access_check, release_config, scripts_governance, repo_health] + if: >- + always() && + (needs.release_config.result == 'failure' || + needs.scripts_governance.result == 'failure' || + needs.repo_health.result == 'failure') + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + sparse-checkout: automation/ci-issue-reporter.sh + sparse-checkout-cone-mode: false + + - name: "File issues for failed gates" + env: + GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + run: | + chmod +x automation/ci-issue-reporter.sh + REPORTER="./automation/ci-issue-reporter.sh" + WF="Repo Health" + + report_gate() { + local gate="$1" result="$2" details="$3" + if [ "$result" = "failure" ]; then + "$REPORTER" --gate "$gate" --details "$details" --workflow "$WF" --severity error + fi + } + + report_gate "Release Configuration" \ + "${{ needs.release_config.result }}" \ + "Required repository variables are missing (RS_FTP_PATH_SUFFIX). Check repository settings." + + report_gate "Scripts Governance" \ + "${{ needs.scripts_governance.result }}" \ + "Scripts directory policy violations detected. Review required and allowed directories." + + report_gate "Repository Health" \ + "${{ needs.repo_health.result }}" \ + "Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary." + -- 2.52.0 From fe908b68a210b8e141e69c65cd373892a28c053e Mon Sep 17 00:00:00 2001 From: Moko Consulting Date: Tue, 2 Jun 2026 20:36:43 +0000 Subject: [PATCH 10/27] chore(ci): add CI issue reporter for auto-filing gate failures --- .mokogitea/workflows/pr-check.yml | 500 ++++++++++++++++-------------- 1 file changed, 264 insertions(+), 236 deletions(-) diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index ce64a27..e2c82ef 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -1,236 +1,264 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.CI -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform -# PATH: /templates/workflows/universal/pr-check.yml.template -# VERSION: 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 - ;; - patch/*) - if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then - ALLOWED=false - REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'" - fi - ;; - hotfix/*) - if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then - ALLOWED=false - REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'" - fi - ;; - rc) - if [ "$BASE" != "main" ]; then - ALLOWED=false - REASON="RC branch can only merge into 'main', not '${BASE}'" - fi - ;; - dev) - if [ "$BASE" != "main" ]; then - ALLOWED=false - REASON="Dev branch can only merge into 'main', not '${BASE}'" - fi - ;; - esac - - if [ "$ALLOWED" = false ]; then - echo "::error::${REASON}" - echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "${REASON}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY - echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY - echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY - echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY - echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY - echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - echo "Branch policy: OK (${HEAD} → ${BASE})" - echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY - - # ── 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/manifest.xml 2>/dev/null | head -1) - [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]') - [ -z "$PLATFORM" ] && PLATFORM="generic" - echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" - - - name: Setup PHP - if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' - run: | - if ! command -v php &> /dev/null; then - sudo apt-get update -qq - sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1 - fi - - - name: PHP syntax check - if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' - run: | - ERRORS=0 - while IFS= read -r -d '' file; do - if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then - ERRORS=$((ERRORS + 1)) - fi - done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0) - echo "PHP lint: ${ERRORS} error(s)" - [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; } - - - name: 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: Check changelog has unreleased entry - run: | - if [ ! -f "CHANGELOG.md" ]; then - echo "::warning::No CHANGELOG.md found" - exit 0 - fi - # Check for content under [Unreleased] section - if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then - echo "::error::CHANGELOG.md missing [Unreleased] section" - exit 1 - fi - # Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased - UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true) - if [ "$UNRELEASED_CONTENT" -eq 0 ]; then - echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes." - echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY - echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY - echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]" - - - name: Verify package source - run: | - SOURCE_DIR="src" - [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" - if [ ! -d "$SOURCE_DIR" ]; then - echo "::warning::No src/ or htdocs/ directory" - exit 0 - fi - FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l) - echo "Source: ${FILE_COUNT} files" - [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; } - - # ── Pre-Release RC Build ───────────────────────────────────────────────── - pre-release: - name: Build RC Package - runs-on: ubuntu-latest - needs: [branch-policy, validate] - - steps: - - name: Trigger RC pre-release - env: - GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - REPO: ${{ github.repository }} - BRANCH: ${{ github.head_ref }} - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - run: | - curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}" - echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY - echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.CI +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform +# PATH: /templates/workflows/universal/pr-check.yml.template +# VERSION: 09.23.00 +# BRIEF: PR gate — branch policy + code validation before merge + +name: "Universal: PR Check" + +on: + pull_request: + types: [opened, synchronize, reopened, edited] + +permissions: + contents: read + pull-requests: write + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + # ── Branch Policy ────────────────────────────────────────────────────── + branch-policy: + name: Branch Policy + runs-on: ubuntu-latest + steps: + - name: Check branch merge target + run: | + HEAD="${{ github.head_ref }}" + BASE="${{ github.base_ref }}" + + echo "PR: ${HEAD} → ${BASE}" + + ALLOWED=true + REASON="" + + case "$HEAD" in + feature/*|feat/*) + if [ "$BASE" != "dev" ]; then + ALLOWED=false + REASON="Feature branches must target 'dev', not '${BASE}'" + fi + ;; + fix/*|bugfix/*) + if [ "$BASE" != "dev" ]; then + ALLOWED=false + REASON="Fix branches must target 'dev', not '${BASE}'" + fi + ;; + patch/*) + if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then + ALLOWED=false + REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'" + fi + ;; + hotfix/*) + if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'" + fi + ;; + rc) + if [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="RC branch can only merge into 'main', not '${BASE}'" + fi + ;; + dev) + if [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Dev branch can only merge into 'main', not '${BASE}'" + fi + ;; + esac + + if [ "$ALLOWED" = false ]; then + echo "::error::${REASON}" + echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "${REASON}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY + echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY + echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY + echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY + echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY + echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + echo "Branch policy: OK (${HEAD} → ${BASE})" + echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY + + # ── 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/manifest.xml 2>/dev/null | head -1) + [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]') + [ -z "$PLATFORM" ] && PLATFORM="generic" + echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" + + - name: Setup PHP + if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' + run: | + if ! command -v php &> /dev/null; then + sudo apt-get update -qq + sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1 + fi + + - name: PHP syntax check + if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' + run: | + ERRORS=0 + while IFS= read -r -d '' file; do + if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then + ERRORS=$((ERRORS + 1)) + fi + done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0) + echo "PHP lint: ${ERRORS} error(s)" + [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; } + + - name: 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: Check changelog has unreleased entry + run: | + if [ ! -f "CHANGELOG.md" ]; then + echo "::warning::No CHANGELOG.md found" + exit 0 + fi + # Check for content under [Unreleased] section + if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then + echo "::error::CHANGELOG.md missing [Unreleased] section" + exit 1 + fi + # Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased + UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true) + if [ "$UNRELEASED_CONTENT" -eq 0 ]; then + echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes." + echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY + echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY + echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]" + + - name: Verify package source + run: | + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + if [ ! -d "$SOURCE_DIR" ]; then + echo "::warning::No src/ or htdocs/ directory" + exit 0 + fi + FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l) + echo "Source: ${FILE_COUNT} files" + [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; } + + # ── Pre-Release RC Build ───────────────────────────────────────────────── + pre-release: + name: Build RC Package + runs-on: ubuntu-latest + needs: [branch-policy, validate] + + steps: + - name: Trigger RC pre-release + env: + GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + REPO: ${{ github.repository }} + BRANCH: ${{ github.head_ref }} + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + run: | + curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}" + echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY + echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY + + # ── Issue Reporter ────────────────────────────────────────────────────── + report-issues: + name: Report Issues + runs-on: ubuntu-latest + needs: [branch-policy, validate] + if: >- + always() && + needs.validate.result == 'failure' + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + sparse-checkout: automation/ci-issue-reporter.sh + sparse-checkout-cone-mode: false + + - name: "File issue for PR validation failure" + env: + GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + run: | + chmod +x automation/ci-issue-reporter.sh + ./automation/ci-issue-reporter.sh \ + --gate "PR Validation" \ + --workflow "PR Check" \ + --severity error \ + --details "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed." -- 2.52.0 From 518f8f88507fd3467379a39926f08ae4fdf6f98e Mon Sep 17 00:00:00 2001 From: Moko Consulting Date: Tue, 2 Jun 2026 20:36:43 +0000 Subject: [PATCH 11/27] chore(ci): add CI issue reporter for auto-filing gate failures --- automation/ci-issue-reporter.sh | 237 ++++++++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 automation/ci-issue-reporter.sh diff --git a/automation/ci-issue-reporter.sh b/automation/ci-issue-reporter.sh new file mode 100644 index 0000000..65c47ba --- /dev/null +++ b/automation/ci-issue-reporter.sh @@ -0,0 +1,237 @@ +#!/usr/bin/env bash +# ============================================================================ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Automation.CI +# INGROUP: moko-platform.Automation +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform +# PATH: /automation/ci-issue-reporter.sh +# VERSION: 09.23.00 +# BRIEF: Creates or updates a Gitea issue when a CI gate fails. +# Deduplicates by searching open issues with the "ci-auto" label +# whose title matches the gate. If a matching issue exists, a comment +# is appended instead of opening a duplicate. +# ============================================================================ + +set -euo pipefail + +# ── Defaults ──────────────────────────────────────────────────────────────── +GITEA_URL="${GITEA_URL:-https://git.mokoconsulting.tech}" +GITEA_TOKEN="${GITEA_TOKEN:-}" +REPO="${GITHUB_REPOSITORY:-}" +RUN_URL="${GITHUB_SERVER_URL:-${GITEA_URL}}/${REPO}/actions/runs/${GITHUB_RUN_ID:-0}" +LABEL_NAME="ci-auto" +LABEL_COLOR="#e11d48" + +GATE="" +DETAILS="" +SEVERITY="error" +WORKFLOW="" + +# ── Parse arguments ───────────────────────────────────────────────────────── +usage() { + cat </dev/null || echo "000") + + if [[ "$exists" == "200" ]]; then + # Check if label already exists + local found + found=$(curl -sf \ + -H "Authorization: token ${GITEA_TOKEN}" \ + "${API}/labels" 2>/dev/null \ + | grep -o "\"name\":\"${LABEL_NAME}\"" || true) + + if [[ -z "$found" ]]; then + curl -sf -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/labels" \ + -d "{\"name\":\"${LABEL_NAME}\",\"color\":\"${LABEL_COLOR}\",\"description\":\"Auto-created by CI issue reporter\"}" \ + > /dev/null 2>&1 || true + fi + fi +} + +# ── Search for existing open issue ────────────────────────────────────────── +find_existing_issue() { + # URL-encode the gate name for the query + local query + query=$(printf '%s' "[CI] ${GATE}" | sed 's/ /%20/g; s/\[/%5B/g; s/\]/%5D/g') + + local response + response=$(curl -sf \ + -H "Authorization: token ${GITEA_TOKEN}" \ + "${API}/issues?type=issues&state=open&labels=${LABEL_NAME}&q=${query}&limit=5" \ + 2>/dev/null || echo "[]") + + # Extract the first matching issue number + echo "$response" \ + | grep -oP '"number":\s*\K[0-9]+' \ + | head -1 +} + +# ── Build issue body ──────────────────────────────────────────────────────── +build_body() { + local severity_badge + if [[ "$SEVERITY" == "error" ]]; then + severity_badge="**Severity:** Error" + else + severity_badge="**Severity:** Warning" + fi + + cat </dev/null) + + HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/issues/${EXISTING}/comments" \ + -d "${COMMENT_JSON}" 2>/dev/null || echo "000") + + if [[ "$HTTP" == "201" ]]; then + echo "Commented on existing issue #${EXISTING}" + else + echo "WARNING: Failed to comment on issue #${EXISTING} (HTTP ${HTTP})" + fi +else + # Create new issue + ISSUE_BODY=$(build_body) + ISSUE_JSON=$(python3 -c " +import sys, json +body = sys.stdin.read() +print(json.dumps({ + 'title': sys.argv[1], + 'body': body, + 'labels': [] +}))" "$TITLE" <<< "$ISSUE_BODY" 2>/dev/null) + + # Create the issue + RESPONSE=$(curl -sf -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/issues" \ + -d "${ISSUE_JSON}" 2>/dev/null || echo "{}") + + ISSUE_NUM=$(echo "$RESPONSE" | grep -oP '"number":\s*\K[0-9]+' | head -1) + + if [[ -n "$ISSUE_NUM" ]]; then + # Apply label (separate call — more reliable across Gitea versions) + LABEL_ID=$(curl -sf \ + -H "Authorization: token ${GITEA_TOKEN}" \ + "${API}/labels" 2>/dev/null \ + | grep -oP "\"id\":\s*\K[0-9]+(?=[^}]*\"name\":\s*\"${LABEL_NAME}\")" \ + | head -1 || true) + + if [[ -n "$LABEL_ID" ]]; then + curl -sf -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/issues/${ISSUE_NUM}/labels" \ + -d "{\"labels\":[${LABEL_ID}]}" \ + > /dev/null 2>&1 || true + fi + + echo "Created issue #${ISSUE_NUM}: ${TITLE}" + else + echo "WARNING: Failed to create issue" + echo "Response: ${RESPONSE}" + fi +fi -- 2.52.0 From cfe69452db1319bda18947cdfea8521a2585f0f4 Mon Sep 17 00:00:00 2001 From: Moko Consulting Date: Tue, 2 Jun 2026 21:32:29 +0000 Subject: [PATCH 12/27] chore(ci): sync CI issue reporter from Template-Joomla -- 2.52.0 From fe84d02827b0f92c7b47f5e646a28d5952dba1c9 Mon Sep 17 00:00:00 2001 From: Moko Consulting Date: Tue, 2 Jun 2026 21:32:30 +0000 Subject: [PATCH 13/27] chore(ci): sync CI issue reporter from Template-Joomla -- 2.52.0 From ea1391ab384d71c5d2a8cafd73d72f8355580afa Mon Sep 17 00:00:00 2001 From: Moko Consulting Date: Tue, 2 Jun 2026 21:32:32 +0000 Subject: [PATCH 14/27] chore(ci): sync CI issue reporter from Template-Joomla -- 2.52.0 From 46bfeaa2e1dfa091825b1ebc056473fdd7457433 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 2 Jun 2026 21:51:19 +0000 Subject: [PATCH 15/27] chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] --- .mokogitea/workflows/pr-check.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index e2c82ef..0ac0ef1 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -105,6 +105,19 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Check for merge conflict markers + run: | + CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true) + if [ -n "$CONFLICTS" ]; then + echo "::error::Merge conflict markers found in source files" + echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "No conflict markers found" + - name: Detect platform id: platform run: | -- 2.52.0 From 61cd30471fc4d508ffa66d80bfaea7b1be0e5c1f Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 2 Jun 2026 23:47:02 +0000 Subject: [PATCH 16/27] chore: add .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] --- .mokogitea/workflows/auto-release.yml | 283 ++++++++++++++++++++++++++ 1 file changed, 283 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..2325032 --- /dev/null +++ b/.mokogitea/workflows/auto-release.yml @@ -0,0 +1,283 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Release +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform +# 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: [opened, closed] + branches: + - main + workflow_dispatch: + inputs: + action: + description: 'Action to perform' + required: false + type: choice + default: release + options: + - release + - promote-rc + +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: + # ── PR Opened → Rename branch to RC and build RC release ───────────────────── + promote-rc: + name: Promote to RC + runs-on: release + if: >- + (github.event.action == 'opened' && github.event.pull_request.merged != true) || + (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.MOKOGITEA_TOKEN }} + fetch-depth: 1 + + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + run: | + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + # Always fetch latest CLI tools — never use stale cache from previous runs + rm -rf /tmp/moko-platform-api + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ + /tmp/moko-platform-api + cd /tmp/moko-platform-api + composer install --no-dev --no-interaction --quiet + + - name: Rename branch to rc + run: | + php /tmp/moko-platform-api/cli/branch_rename.php \ + --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \ + --pr "${{ github.event.pull_request.number }}" + + - name: Checkout rc and configure git + run: | + git fetch origin rc + git checkout rc + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + + - name: Publish RC release + run: | + php /tmp/moko-platform-api/cli/release_publish.php \ + --path . --stability rc --bump minor --branch rc \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" + + - name: Summary + if: always() + run: | + echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY + echo "Branch renamed to rc, minor bump, RC + lesser stream releases built, updates.xml synced" >> $GITHUB_STEP_SUMMARY + + # ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── + release: + name: Build & Release Pipeline + runs-on: release + if: >- + github.event.pull_request.merged == true || + (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc') + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.MOKOGITEA_TOKEN }} + fetch-depth: 0 + + - name: Configure git for bot pushes + run: | + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + + - name: Check for merge conflict markers + run: | + CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true) + if [ -n "$CONFLICTS" ]; then + echo "::error::Merge conflict markers found — aborting release" + echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "No conflict markers found" + + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_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 + # Always fetch latest CLI tools — never use stale cache from previous runs + rm -rf /tmp/moko-platform-api + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ + /tmp/moko-platform-api + cd /tmp/moko-platform-api + composer install --no-dev --no-interaction --quiet + + + - name: "Publish stable release" + run: | + php /tmp/moko-platform-api/cli/release_publish.php \ + --path . --stability stable --bump minor --branch main \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" + + # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- + - name: "Step 9: Mirror release to GitHub" + if: >- + steps.version.outputs.skip != 'true' && + secrets.GH_MIRROR_TOKEN != '' + continue-on-error: true + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php /tmp/moko-platform-api/cli/release_mirror.php \ + --version "$VERSION" --tag "$RELEASE_TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \ + --branch main 2>&1 || true + echo "GitHub mirror updated" >> $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_MIRROR_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_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ + git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_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" + + - name: "Step 11: Delete rc branch and recreate dev 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.MOKOGITEA_TOKEN }}" + + # Delete rc branch (ephemeral — created by promote-rc) + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/branches/rc" 2>/dev/null \ + && echo "Deleted rc branch" || echo "rc branch not found" + + # 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 "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY + + - name: "Step 12: Create version 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.MOKOGITEA_TOKEN }}" + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + BRANCH_NAME="version/${VERSION}" + MAIN_SHA=$(git rev-parse HEAD) + + # Delete old version branch if it exists (same version re-release) + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}" + + # Create version/XX.YY.ZZ from main + curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed" + + echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY + + + + # -- Dolibarr post-release: Reset dev version ----------------------------- + - name: "Post-release: Reset dev version" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php /tmp/moko-platform-api/cli/version_reset_dev.php \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \ + --branch dev --path . 2>&1 || true + + # -- 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 ae19e32407da3d8522498130a1e3c644a52f2e70 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Wed, 3 Jun 2026 03:10:24 +0000 Subject: [PATCH 17/27] chore: sync .mokogitea/workflows/repo-health.yml from moko-platform [skip ci] --- .mokogitea/workflows/repo-health.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.mokogitea/workflows/repo-health.yml b/.mokogitea/workflows/repo-health.yml index b23d971..d7743f0 100644 --- a/.mokogitea/workflows/repo-health.yml +++ b/.mokogitea/workflows/repo-health.yml @@ -41,7 +41,8 @@ permissions: env: # Release policy - Repository Variables Only - RELEASE_REQUIRED_REPO_VARS: RS_FTP_PATH_SUFFIX + # RS_FTP_PATH_SUFFIX removed — MokoGitea handles all releases now + RELEASE_REQUIRED_REPO_VARS: RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX # Scripts governance policy -- 2.52.0 From d085c79d9ede36461e16197c1e079e69889ab329 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Wed, 3 Jun 2026 09:36:44 +0000 Subject: [PATCH 18/27] chore: sync .mokogitea/workflows/repo-health.yml from moko-platform [skip ci] --- .mokogitea/workflows/repo-health.yml | 125 ++------------------------- 1 file changed, 9 insertions(+), 116 deletions(-) diff --git a/.mokogitea/workflows/repo-health.yml b/.mokogitea/workflows/repo-health.yml index d7743f0..8d57aaf 100644 --- a/.mokogitea/workflows/repo-health.yml +++ b/.mokogitea/workflows/repo-health.yml @@ -11,7 +11,7 @@ # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform # PATH: /templates/workflows/joomla/repo_health.yml.template # VERSION: 09.23.00 -# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts. +# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts. # ============================================================================ name: "Generic: Repo Health" @@ -24,13 +24,12 @@ on: workflow_dispatch: inputs: profile: - description: 'Validation profile: all, release, scripts, or repo' + description: 'Validation profile: all, scripts, or repo' required: true default: all type: choice options: - all - - release - scripts - repo pull_request: @@ -40,11 +39,6 @@ permissions: contents: read env: - # Release policy - Repository Variables Only - # RS_FTP_PATH_SUFFIX removed — MokoGitea handles all releases now - RELEASE_REQUIRED_REPO_VARS: - 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 @@ -139,101 +133,6 @@ jobs: 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 @@ -257,14 +156,14 @@ jobs: profile="${PROFILE_RAW:-all}" case "${profile}" in - all|release|scripts|repo) ;; + all|scripts|repo) ;; *) printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" exit 1 ;; esac - if [ "${profile}" = 'release' ] || [ "${profile}" = 'repo' ]; then + if [ "${profile}" = 'repo' ]; then { printf '%s\n' '### Scripts governance' printf '%s\n' "Profile: ${profile}" @@ -371,14 +270,14 @@ jobs: profile="${PROFILE_RAW:-all}" case "${profile}" in - all|release|scripts|repo) ;; + all|scripts|repo) ;; *) printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" exit 1 ;; esac - if [ "${profile}" = 'release' ] || [ "${profile}" = 'scripts' ]; then + if [ "${profile}" = 'scripts' ]; then { printf '%s\n' '### Repository health' printf '%s\n' "Profile: ${profile}" @@ -705,7 +604,7 @@ jobs: 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' '| Release policy | N/A | Releases handled by MokoGitea |' printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |' printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |' printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |' @@ -774,11 +673,10 @@ jobs: report-issues: name: "Report Issues" runs-on: ubuntu-latest - needs: [access_check, release_config, scripts_governance, repo_health] + needs: [access_check, scripts_governance, repo_health] if: >- always() && - (needs.release_config.result == 'failure' || - needs.scripts_governance.result == 'failure' || + (needs.scripts_governance.result == 'failure' || needs.repo_health.result == 'failure') steps: @@ -804,10 +702,6 @@ jobs: fi } - report_gate "Release Configuration" \ - "${{ needs.release_config.result }}" \ - "Required repository variables are missing (RS_FTP_PATH_SUFFIX). Check repository settings." - report_gate "Scripts Governance" \ "${{ needs.scripts_governance.result }}" \ "Scripts directory policy violations detected. Review required and allowed directories." @@ -815,4 +709,3 @@ jobs: report_gate "Repository Health" \ "${{ needs.repo_health.result }}" \ "Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary." - -- 2.52.0 From 902321de47c20819ad81258183a6c467d3fb4e38 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Wed, 3 Jun 2026 21:19:18 -0500 Subject: [PATCH 19/27] feat: restructure as package extension with solid color and gradient hero modes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructure from standalone module to package extension (pkg_mokojoomhero) containing mod_mokojoomhero and plg_system_mokojoomhero. Add two new hero modes — solid color and gradient — with color pickers and angle controls, using Joomla showon for conditional field display. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .mokogitea/manifest.xml | 2 +- CLAUDE.md | 38 +++-- Makefile | 134 +++++------------- src/index.html | 1 + src/index.md | 16 --- src/packages/index.html | 1 + src/packages/mod_mokojoomhero/index.html | 1 + .../language/en-GB/index.html | 1 + .../language/en-GB/mod_mokojoomhero.ini | 16 ++- .../language/en-GB/mod_mokojoomhero.sys.ini | 0 .../language/en-US/index.html | 1 + .../language/en-US/mod_mokojoomhero.ini | 16 ++- .../language/en-US/mod_mokojoomhero.sys.ini | 0 .../mod_mokojoomhero/language/index.html | 1 + .../mod_mokojoomhero/media/css/index.html | 1 + .../media/css/mod_mokojoomhero.css | 8 ++ .../mod_mokojoomhero/media/index.html | 1 + .../mod_mokojoomhero}/media/joomla.asset.json | 0 .../mod_mokojoomhero/media/js/index.html | 1 + .../media/js/mod_mokojoomhero.js | 0 .../mod_mokojoomhero}/mod_mokojoomhero.php | 10 ++ .../mod_mokojoomhero}/mod_mokojoomhero.xml | 42 +++++- .../mod_mokojoomhero}/script.php | 0 .../mod_mokojoomhero}/tmpl/default.php | 12 +- .../mod_mokojoomhero}/tmpl/index.html | 0 .../plg_system_mokojoomhero/index.html | 1 + .../language/en-GB/index.html | 1 + .../en-GB/plg_system_mokojoomhero.ini | 2 + .../en-GB/plg_system_mokojoomhero.sys.ini | 5 + .../language/en-US/index.html | 1 + .../en-US/plg_system_mokojoomhero.ini | 2 + .../en-US/plg_system_mokojoomhero.sys.ini | 5 + .../language/index.html | 1 + .../plg_system_mokojoomhero/mokojoomhero.php | 12 ++ .../plg_system_mokojoomhero/mokojoomhero.xml | 34 +++++ .../services/index.html | 1 + .../services/provider.php | 39 +++++ .../src/Extension/MokoJoomHero.php | 90 ++++++++++++ .../src/Extension/index.html | 1 + .../plg_system_mokojoomhero/src/index.html | 1 + src/pkg_mokojoomhero.xml | 30 ++++ src/pkg_script.php | 43 ++++++ updates.xml | 20 +++ 43 files changed, 457 insertions(+), 135 deletions(-) create mode 100644 src/index.html delete mode 100644 src/index.md create mode 100644 src/packages/index.html create mode 100644 src/packages/mod_mokojoomhero/index.html create mode 100644 src/packages/mod_mokojoomhero/language/en-GB/index.html rename src/{ => packages/mod_mokojoomhero}/language/en-GB/mod_mokojoomhero.ini (81%) rename src/{ => packages/mod_mokojoomhero}/language/en-GB/mod_mokojoomhero.sys.ini (100%) create mode 100644 src/packages/mod_mokojoomhero/language/en-US/index.html rename src/{ => packages/mod_mokojoomhero}/language/en-US/mod_mokojoomhero.ini (81%) rename src/{ => packages/mod_mokojoomhero}/language/en-US/mod_mokojoomhero.sys.ini (100%) create mode 100644 src/packages/mod_mokojoomhero/language/index.html create mode 100644 src/packages/mod_mokojoomhero/media/css/index.html rename src/{ => packages/mod_mokojoomhero}/media/css/mod_mokojoomhero.css (95%) create mode 100644 src/packages/mod_mokojoomhero/media/index.html rename src/{ => packages/mod_mokojoomhero}/media/joomla.asset.json (100%) create mode 100644 src/packages/mod_mokojoomhero/media/js/index.html rename src/{ => packages/mod_mokojoomhero}/media/js/mod_mokojoomhero.js (100%) rename src/{ => packages/mod_mokojoomhero}/mod_mokojoomhero.php (89%) rename src/{ => packages/mod_mokojoomhero}/mod_mokojoomhero.xml (85%) rename src/{ => packages/mod_mokojoomhero}/script.php (100%) rename src/{ => packages/mod_mokojoomhero}/tmpl/default.php (85%) rename src/{ => packages/mod_mokojoomhero}/tmpl/index.html (100%) create mode 100644 src/packages/plg_system_mokojoomhero/index.html create mode 100644 src/packages/plg_system_mokojoomhero/language/en-GB/index.html create mode 100644 src/packages/plg_system_mokojoomhero/language/en-GB/plg_system_mokojoomhero.ini create mode 100644 src/packages/plg_system_mokojoomhero/language/en-GB/plg_system_mokojoomhero.sys.ini create mode 100644 src/packages/plg_system_mokojoomhero/language/en-US/index.html create mode 100644 src/packages/plg_system_mokojoomhero/language/en-US/plg_system_mokojoomhero.ini create mode 100644 src/packages/plg_system_mokojoomhero/language/en-US/plg_system_mokojoomhero.sys.ini create mode 100644 src/packages/plg_system_mokojoomhero/language/index.html create mode 100644 src/packages/plg_system_mokojoomhero/mokojoomhero.php create mode 100644 src/packages/plg_system_mokojoomhero/mokojoomhero.xml create mode 100644 src/packages/plg_system_mokojoomhero/services/index.html create mode 100644 src/packages/plg_system_mokojoomhero/services/provider.php create mode 100644 src/packages/plg_system_mokojoomhero/src/Extension/MokoJoomHero.php create mode 100644 src/packages/plg_system_mokojoomhero/src/Extension/index.html create mode 100644 src/packages/plg_system_mokojoomhero/src/index.html create mode 100644 src/pkg_mokojoomhero.xml create mode 100644 src/pkg_script.php diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml index dde7d5b..d0f91d9 100644 --- a/.mokogitea/manifest.xml +++ b/.mokogitea/manifest.xml @@ -7,7 +7,7 @@ MokoJoomHero - Module - MokoJoomHero + Package - MokoJoomHero MokoConsulting A Joomla Module designed to provide a random image from a folder with content on top as a Hero. 01.06.00 diff --git a/CLAUDE.md b/CLAUDE.md index d40f799..6598488 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code when working with this repository. ## Project Overview -**MokoJoomHero** -- A Joomla Module designed to provide a random image from a folder with content on top as a Hero. +**MokoJoomHero** -- Random hero image slideshow/video with content overlay for Joomla | Field | Value | |---|---| @@ -32,20 +32,40 @@ composer install # Install PHP dependencies ## Architecture -This is a Joomla extension. Key directories: -- `src/` -- extension source (deployed to Joomla) -- `src/*.xml` -- manifest file (version, files, params) -- `src/src/` or `src/services/` -- PHP classes -- `src/language/` -- translation strings -- `src/media/` -- CSS/JS/images +This is a Joomla **package** extension (`pkg_mokojoomhero`) containing two sub-extensions: + +### mod_mokojoomhero (Site Module) +- Random hero image slideshow or background video with content overlay +- Supports image folders, YouTube, Vimeo, and local video +- Configurable overlay, text alignment, card animation +- **Requires** `plg_system_mokojoomhero` to be enabled — module silently skips rendering if the system plugin is disabled + +### plg_system_mokojoomhero (System Plugin) +- License key validation — warns admin once per session if no download key configured +- Auto-enabled on package install via `pkg_script.php` +- Namespace: `Joomla\Plugin\System\MokoJoomHero` + +### Key files +- `src/pkg_mokojoomhero.xml` — package manifest +- `src/pkg_script.php` — auto-enables system plugin on install +- `src/packages/mod_mokojoomhero/` — hero module source +- `src/packages/plg_system_mokojoomhero/` — system plugin source +- `updates.xml` — Joomla update server (includes legacy module entries for migration) ## Rules - **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`) - - **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, or `*.min.css`/`*.min.js` - **Attribution**: use `Authored-by: Moko Consulting` in commits - **Branch strategy**: develop on `dev`, merge to `main` for release -- **Minification**: handled at build time (CI) and runtime (MokoMinifyHelper for Joomla templates) +- **Minification**: handled at build time (CI) - **Wiki**: documentation lives in the Gitea wiki, not in `docs/` files - **Standards**: this repo follows [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home) + +## Coding Standards + +- PHP 8.1+ minimum +- Joomla 5/6 DI container pattern: `services/provider.php` → Extension class +- Legacy stub `.php` file required for plugin loader but empty +- `SubscriberInterface` for event subscription (not `on*` method naming) +- SPDX license headers on all PHP files diff --git a/Makefile b/Makefile index 02bdb18..5819862 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ # Extension Configuration EXTENSION_NAME := mokojoomhero -EXTENSION_TYPE := module +EXTENSION_TYPE := package # Options: module, plugin, component, package, template EXTENSION_VERSION := 1.0.0 @@ -26,7 +26,7 @@ PLUGIN_GROUP := system # Options: system, content, user, authentication, etc. # Directories -SRC_DIR := . +SRC_DIR := src BUILD_DIR := build DIST_DIR := dist DOCS_DIR := docs @@ -155,62 +155,30 @@ clean: ## Clean build artifacts @echo "$(COLOR_GREEN)✓ Build artifacts cleaned$(COLOR_RESET)" .PHONY: build -build: clean validate ## Build extension package - @echo "$(COLOR_BLUE)Building Joomla extension package...$(COLOR_RESET)" - @mkdir -p $(DIST_DIR) $(BUILD_DIR) - - # Determine package prefix based on extension type - @case "$(EXTENSION_TYPE)" in \ - module) \ - PACKAGE_PREFIX="mod_$(EXTENSION_NAME)"; \ - BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \ - ;; \ - plugin) \ - PACKAGE_PREFIX="plg_$(PLUGIN_GROUP)_$(EXTENSION_NAME)"; \ - BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \ - ;; \ - component) \ - PACKAGE_PREFIX="com_$(EXTENSION_NAME)"; \ - BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \ - ;; \ - package) \ - PACKAGE_PREFIX="pkg_$(EXTENSION_NAME)"; \ - BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \ - ;; \ - template) \ - PACKAGE_PREFIX="tpl_$(EXTENSION_NAME)"; \ - BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \ - ;; \ - *) \ - echo "$(COLOR_RED)✗ Unknown extension type: $(EXTENSION_TYPE)$(COLOR_RESET)"; \ - exit 1; \ - ;; \ - esac; \ - \ - mkdir -p "$$BUILD_TARGET"; \ - \ - echo "Building $$PACKAGE_PREFIX..."; \ - \ - rsync -av --progress \ - --exclude='$(BUILD_DIR)' \ - --exclude='$(DIST_DIR)' \ - --exclude='.git*' \ - --exclude='vendor/' \ - --exclude='node_modules/' \ - --exclude='tests/' \ - --exclude='Makefile' \ - --exclude='composer.json' \ - --exclude='composer.lock' \ - --exclude='package.json' \ - --exclude='package-lock.json' \ - --exclude='phpunit.xml' \ - --exclude='*.md' \ - --exclude='.editorconfig' \ - . "$$BUILD_TARGET/"; \ - \ - cd $(BUILD_DIR) && $(ZIP) -r "../$(DIST_DIR)/$${PACKAGE_PREFIX}-$(EXTENSION_VERSION).zip" "$${PACKAGE_PREFIX}"; \ - \ - echo "$(COLOR_GREEN)✓ Package created: $(DIST_DIR)/$${PACKAGE_PREFIX}-$(EXTENSION_VERSION).zip$(COLOR_RESET)" +build: clean ## Build extension package + @echo "$(COLOR_BLUE)Building Joomla package extension...$(COLOR_RESET)" + @mkdir -p $(DIST_DIR) $(BUILD_DIR)/packages + + @# --- Build each sub-extension as a separate ZIP --- + @for EXT_DIR in $(SRC_DIR)/packages/*/; do \ + EXT_NAME=$$(basename "$$EXT_DIR"); \ + [ "$$EXT_NAME" = "index.html" ] && continue; \ + echo " Packaging $$EXT_NAME..."; \ + cd "$$EXT_DIR" && $(ZIP) -r "$(CURDIR)/$(BUILD_DIR)/packages/$${EXT_NAME}.zip" . \ + -x "*.git*" -x "*/index.html" 2>/dev/null; \ + cd "$(CURDIR)"; \ + done + + @# --- Build the outer package ZIP --- + @echo " Assembling pkg_$(EXTENSION_NAME)..." + @cp $(SRC_DIR)/pkg_mokojoomhero.xml $(BUILD_DIR)/pkg_mokojoomhero.xml + @cp $(SRC_DIR)/pkg_script.php $(BUILD_DIR)/pkg_script.php + @cd $(BUILD_DIR) && $(ZIP) -r "$(CURDIR)/$(DIST_DIR)/pkg_$(EXTENSION_NAME)-$(EXTENSION_VERSION).zip" \ + pkg_mokojoomhero.xml pkg_script.php packages/ + + @echo "$(COLOR_GREEN)✓ Package created: $(DIST_DIR)/pkg_$(EXTENSION_NAME)-$(EXTENSION_VERSION).zip$(COLOR_RESET)" + @echo " Contents:" + @unzip -l "$(DIST_DIR)/pkg_$(EXTENSION_NAME)-$(EXTENSION_VERSION).zip" | tail -n +4 | head -20 .PHONY: package package: build ## Alias for build @@ -325,49 +293,15 @@ security-check: ## Run security checks on dependencies $(NPM) audit || echo "$(COLOR_YELLOW)⚠ Vulnerabilities found$(COLOR_RESET)"; \ fi -.PHONY: deploy -deploy: ## Deploy to a Joomla site via SSH (usage: make deploy HOST=user@host WEBROOT=/path/to/joomla) - @if [ -z "$(HOST)" ] || [ -z "$(WEBROOT)" ]; then \ - echo "$(COLOR_RED)✗ Usage: make deploy HOST=user@host WEBROOT=/path/to/joomla [KEY=~/.ssh/id_rsa]$(COLOR_RESET)"; \ - exit 1; \ +.PHONY: minify +minify: ## Minify CSS/JS assets + @echo "Minifying assets..." + @MOKO_PLATFORM=$$(echo ../moko-platform $$HOME/moko-platform /opt/moko-platform | tr ' ' '\n' | while read p; do [ -d "$$p" ] && echo "$$p" && break; done); \ + if [ -n "$$MOKO_PLATFORM" ] && [ -f "$$MOKO_PLATFORM/build/minify.js" ]; then \ + node "$$MOKO_PLATFORM/build/minify.js" $(SRC_DIR); \ + else \ + echo "No minify script found"; \ fi - @SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=10"; \ - if [ -n "$(KEY)" ]; then SSH_OPTS="$$SSH_OPTS -i $(KEY)"; fi; \ - echo "$(COLOR_BLUE)Deploying mod_$(EXTENSION_NAME) to $(HOST):$(WEBROOT)...$(COLOR_RESET)"; \ - ssh $$SSH_OPTS $(HOST) "\ - W=$(WEBROOT) && \ - cp -r \$$W/modules/mod_$(EXTENSION_NAME)/language/en-US/* /dev/null 2>&1; \ - true" && \ - for f in src/mod_mokojoomhero.php src/mod_mokojoomhero.xml src/script.php; do \ - scp $$SSH_OPTS $$f $(HOST):$(WEBROOT)/modules/mod_$(EXTENSION_NAME)/$$(basename $$f); \ - done && \ - scp -r $$SSH_OPTS src/tmpl/* $(HOST):$(WEBROOT)/modules/mod_$(EXTENSION_NAME)/tmpl/ && \ - scp -r $$SSH_OPTS src/language/* $(HOST):$(WEBROOT)/modules/mod_$(EXTENSION_NAME)/language/ && \ - scp $$SSH_OPTS src/media/joomla.asset.json $(HOST):$(WEBROOT)/media/mod_$(EXTENSION_NAME)/ && \ - scp -r $$SSH_OPTS src/media/css/* $(HOST):$(WEBROOT)/media/mod_$(EXTENSION_NAME)/css/ && \ - scp -r $$SSH_OPTS src/media/js/* $(HOST):$(WEBROOT)/media/mod_$(EXTENSION_NAME)/js/ && \ - ssh $$SSH_OPTS $(HOST) "\ - W=$(WEBROOT) && \ - mkdir -p \$$W/images/heroes && \ - for lang in en-US en-GB; do \ - for ini in mod_mokojoomhero.ini mod_mokojoomhero.sys.ini; do \ - src=\$$W/modules/mod_$(EXTENSION_NAME)/language/\$$lang/\$$ini; \ - if [ -f \$$src ]; then \ - cp \$$src \$$W/administrator/language/\$$lang/\$$ini 2>/dev/null; \ - cp \$$src \$$W/language/\$$lang/\$$ini 2>/dev/null; \ - fi; \ - done; \ - done && \ - echo 'OK'" && \ - echo "$(COLOR_GREEN)✓ Deployed to $(HOST)$(COLOR_RESET)" - -.PHONY: deploy-all -deploy-all: ## Deploy to all configured sites (requires SITES_FILE or inline) - @echo "$(COLOR_BLUE)Deploying to all sites...$(COLOR_RESET)" - @echo "$(COLOR_YELLOW)Usage: Create a sites.conf with HOST:WEBROOT per line, then:$(COLOR_RESET)" - @echo " while IFS=: read -r host webroot; do" - @echo " make deploy HOST=\$$host WEBROOT=\$$webroot KEY=path/to/key" - @echo " done < sites.conf" .PHONY: all all: install-deps validate test build ## Run complete build pipeline diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/index.html @@ -0,0 +1 @@ + diff --git a/src/index.md b/src/index.md deleted file mode 100644 index c700eac..0000000 --- a/src/index.md +++ /dev/null @@ -1,16 +0,0 @@ -# Docs Index: /templates/repos/joomla/module/src - -## 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 | diff --git a/src/packages/index.html b/src/packages/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/index.html @@ -0,0 +1 @@ + diff --git a/src/packages/mod_mokojoomhero/index.html b/src/packages/mod_mokojoomhero/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/mod_mokojoomhero/index.html @@ -0,0 +1 @@ + diff --git a/src/packages/mod_mokojoomhero/language/en-GB/index.html b/src/packages/mod_mokojoomhero/language/en-GB/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/mod_mokojoomhero/language/en-GB/index.html @@ -0,0 +1 @@ + diff --git a/src/language/en-GB/mod_mokojoomhero.ini b/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.ini similarity index 81% rename from src/language/en-GB/mod_mokojoomhero.ini rename to src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.ini index 800b864..2d480a4 100644 --- a/src/language/en-GB/mod_mokojoomhero.ini +++ b/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.ini @@ -20,10 +20,12 @@ MOD_MOKOJOOMHERO_SHOW_CARD_DESC="Wrap the content in a card with a white backgro ; Hero mode MOD_MOKOJOOMHERO_MODE_LABEL="Hero Mode" -MOD_MOKOJOOMHERO_MODE_DESC="Choose between a slideshow of images, an embedded video (YouTube/Vimeo), or a local video file." +MOD_MOKOJOOMHERO_MODE_DESC="Choose between a slideshow of images, an embedded video (YouTube/Vimeo), a local video file, a solid colour, or a gradient." MOD_MOKOJOOMHERO_MODE_IMAGES="Images" MOD_MOKOJOOMHERO_MODE_VIDEO="Video (YouTube/Vimeo)" MOD_MOKOJOOMHERO_MODE_LOCALVIDEO="Local Video" +MOD_MOKOJOOMHERO_MODE_COLOR="Solid Colour" +MOD_MOKOJOOMHERO_MODE_GRADIENT="Gradient" ; Image settings MOD_MOKOJOOMHERO_IMAGE_FOLDER_LABEL="Image Folder" @@ -49,6 +51,18 @@ MOD_MOKOJOOMHERO_CARD_DELAY_DESC="Delay in milliseconds before the content card MOD_MOKOJOOMHERO_MUTE_TOGGLE_LABEL="Show Mute Toggle" MOD_MOKOJOOMHERO_MUTE_TOGGLE_DESC="Show a mute/unmute button on the hero video. Videos always start muted (required for autoplay)." +; Solid colour background +MOD_MOKOJOOMHERO_BG_COLOR_LABEL="Background Colour" +MOD_MOKOJOOMHERO_BG_COLOR_DESC="Solid background colour for the hero section." + +; Gradient background +MOD_MOKOJOOMHERO_GRADIENT_START_LABEL="Gradient Start Colour" +MOD_MOKOJOOMHERO_GRADIENT_START_DESC="Starting colour of the gradient." +MOD_MOKOJOOMHERO_GRADIENT_END_LABEL="Gradient End Colour" +MOD_MOKOJOOMHERO_GRADIENT_END_DESC="Ending colour of the gradient." +MOD_MOKOJOOMHERO_GRADIENT_ANGLE_LABEL="Gradient Angle" +MOD_MOKOJOOMHERO_GRADIENT_ANGLE_DESC="Direction of the gradient in degrees (0 = bottom to top, 90 = left to right, 135 = diagonal)." + ; Hero height MOD_MOKOJOOMHERO_HERO_HEIGHT_LABEL="Hero Height" MOD_MOKOJOOMHERO_HERO_HEIGHT_DESC="Height of the hero section. Use px for fixed pixels (e.g. 400px) or vh for viewport height (e.g. 60vh for 60%% of screen)." diff --git a/src/language/en-GB/mod_mokojoomhero.sys.ini b/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.sys.ini similarity index 100% rename from src/language/en-GB/mod_mokojoomhero.sys.ini rename to src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.sys.ini diff --git a/src/packages/mod_mokojoomhero/language/en-US/index.html b/src/packages/mod_mokojoomhero/language/en-US/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/mod_mokojoomhero/language/en-US/index.html @@ -0,0 +1 @@ + diff --git a/src/language/en-US/mod_mokojoomhero.ini b/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.ini similarity index 81% rename from src/language/en-US/mod_mokojoomhero.ini rename to src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.ini index 541ee4b..a6f1ba4 100644 --- a/src/language/en-US/mod_mokojoomhero.ini +++ b/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.ini @@ -20,10 +20,12 @@ MOD_MOKOJOOMHERO_SHOW_CARD_DESC="Wrap the content in a card with a white backgro ; Hero mode MOD_MOKOJOOMHERO_MODE_LABEL="Hero Mode" -MOD_MOKOJOOMHERO_MODE_DESC="Choose between a slideshow of images, an embedded video (YouTube/Vimeo), or a local video file." +MOD_MOKOJOOMHERO_MODE_DESC="Choose between a slideshow of images, an embedded video (YouTube/Vimeo), a local video file, a solid color, or a gradient." MOD_MOKOJOOMHERO_MODE_IMAGES="Images" MOD_MOKOJOOMHERO_MODE_VIDEO="Video (YouTube/Vimeo)" MOD_MOKOJOOMHERO_MODE_LOCALVIDEO="Local Video" +MOD_MOKOJOOMHERO_MODE_COLOR="Solid Color" +MOD_MOKOJOOMHERO_MODE_GRADIENT="Gradient" ; Image settings MOD_MOKOJOOMHERO_IMAGE_FOLDER_LABEL="Image Folder" @@ -41,6 +43,18 @@ MOD_MOKOJOOMHERO_VIDEO_FILE_DESC="YouTube or Vimeo URL. The module auto-detects MOD_MOKOJOOMHERO_LOCAL_VIDEO_LABEL="Video File" MOD_MOKOJOOMHERO_LOCAL_VIDEO_DESC="Select a video file from the Media Manager (mp4, webm, ogg)." +; Solid color background +MOD_MOKOJOOMHERO_BG_COLOR_LABEL="Background Color" +MOD_MOKOJOOMHERO_BG_COLOR_DESC="Solid background color for the hero section." + +; Gradient background +MOD_MOKOJOOMHERO_GRADIENT_START_LABEL="Gradient Start Color" +MOD_MOKOJOOMHERO_GRADIENT_START_DESC="Starting color of the gradient." +MOD_MOKOJOOMHERO_GRADIENT_END_LABEL="Gradient End Color" +MOD_MOKOJOOMHERO_GRADIENT_END_DESC="Ending color of the gradient." +MOD_MOKOJOOMHERO_GRADIENT_ANGLE_LABEL="Gradient Angle" +MOD_MOKOJOOMHERO_GRADIENT_ANGLE_DESC="Direction of the gradient in degrees (0 = bottom to top, 90 = left to right, 135 = diagonal)." + ; Hero height MOD_MOKOJOOMHERO_HERO_HEIGHT_LABEL="Hero Height" MOD_MOKOJOOMHERO_HERO_HEIGHT_DESC="Height of the hero section. Use px for fixed pixels (e.g. 400px) or vh for viewport height (e.g. 60vh for 60% of screen)." diff --git a/src/language/en-US/mod_mokojoomhero.sys.ini b/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.sys.ini similarity index 100% rename from src/language/en-US/mod_mokojoomhero.sys.ini rename to src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.sys.ini diff --git a/src/packages/mod_mokojoomhero/language/index.html b/src/packages/mod_mokojoomhero/language/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/mod_mokojoomhero/language/index.html @@ -0,0 +1 @@ + diff --git a/src/packages/mod_mokojoomhero/media/css/index.html b/src/packages/mod_mokojoomhero/media/css/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/mod_mokojoomhero/media/css/index.html @@ -0,0 +1 @@ + diff --git a/src/media/css/mod_mokojoomhero.css b/src/packages/mod_mokojoomhero/media/css/mod_mokojoomhero.css similarity index 95% rename from src/media/css/mod_mokojoomhero.css rename to src/packages/mod_mokojoomhero/media/css/mod_mokojoomhero.css index 9e560d8..ffda708 100644 --- a/src/media/css/mod_mokojoomhero.css +++ b/src/packages/mod_mokojoomhero/media/css/mod_mokojoomhero.css @@ -22,6 +22,14 @@ justify-content: center; } +/* ============================================================ + Solid colour / gradient background + ============================================================ */ +.mokojoomhero__color { + position: absolute; + inset: 0; +} + /* ============================================================ Image slides ============================================================ */ diff --git a/src/packages/mod_mokojoomhero/media/index.html b/src/packages/mod_mokojoomhero/media/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/mod_mokojoomhero/media/index.html @@ -0,0 +1 @@ + diff --git a/src/media/joomla.asset.json b/src/packages/mod_mokojoomhero/media/joomla.asset.json similarity index 100% rename from src/media/joomla.asset.json rename to src/packages/mod_mokojoomhero/media/joomla.asset.json diff --git a/src/packages/mod_mokojoomhero/media/js/index.html b/src/packages/mod_mokojoomhero/media/js/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/mod_mokojoomhero/media/js/index.html @@ -0,0 +1 @@ + diff --git a/src/media/js/mod_mokojoomhero.js b/src/packages/mod_mokojoomhero/media/js/mod_mokojoomhero.js similarity index 100% rename from src/media/js/mod_mokojoomhero.js rename to src/packages/mod_mokojoomhero/media/js/mod_mokojoomhero.js diff --git a/src/mod_mokojoomhero.php b/src/packages/mod_mokojoomhero/mod_mokojoomhero.php similarity index 89% rename from src/mod_mokojoomhero.php rename to src/packages/mod_mokojoomhero/mod_mokojoomhero.php index a57ed9a..e264f9e 100644 --- a/src/mod_mokojoomhero.php +++ b/src/packages/mod_mokojoomhero/mod_mokojoomhero.php @@ -11,8 +11,14 @@ defined('_JEXEC') or die; use Joomla\CMS\Helper\ModuleHelper; +use Joomla\CMS\Plugin\PluginHelper; use Joomla\CMS\Uri\Uri; +// Require the system plugin to be installed and enabled +if (!PluginHelper::isEnabled('system', 'mokojoomhero')) { + return; +} + /** @var \Joomla\CMS\Application\SiteApplication $app */ /** @var \stdClass $module */ /** @var \Joomla\Registry\Registry $params */ @@ -38,6 +44,10 @@ $showCard = (bool) $params->get('showCard', 1); $cardDelay = (int) $params->get('cardDelay', 0); $showMuteToggle = (bool) $params->get('showMuteToggle', 0); $localVideoFile = $params->get('localVideoFile', ''); +$bgColor = $params->get('bgColor', '#003366'); +$gradientStart = $params->get('gradientStart', '#003366'); +$gradientEnd = $params->get('gradientEnd', '#006699'); +$gradientAngle = (int) $params->get('gradientAngle', 135); // Collect hero images $heroImages = []; diff --git a/src/mod_mokojoomhero.xml b/src/packages/mod_mokojoomhero/mod_mokojoomhero.xml similarity index 85% rename from src/mod_mokojoomhero.xml rename to src/packages/mod_mokojoomhero/mod_mokojoomhero.xml index 06d58e3..36a9544 100644 --- a/src/mod_mokojoomhero.xml +++ b/src/packages/mod_mokojoomhero/mod_mokojoomhero.xml @@ -61,7 +61,44 @@ + + + + + + - - - https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/raw/branch/main/updates.xml - - diff --git a/src/script.php b/src/packages/mod_mokojoomhero/script.php similarity index 100% rename from src/script.php rename to src/packages/mod_mokojoomhero/script.php diff --git a/src/tmpl/default.php b/src/packages/mod_mokojoomhero/tmpl/default.php similarity index 85% rename from src/tmpl/default.php rename to src/packages/mod_mokojoomhero/tmpl/default.php index ef6c2b5..62f0195 100644 --- a/src/tmpl/default.php +++ b/src/packages/mod_mokojoomhero/tmpl/default.php @@ -27,6 +27,10 @@ use Joomla\CMS\Language\Text; /** @var bool $showCard */ /** @var int $cardDelay */ /** @var bool $showMuteToggle */ +/** @var string $bgColor */ +/** @var string $gradientStart */ +/** @var string $gradientEnd */ +/** @var int $gradientAngle */ /** @var string $content */ $moduleId = 'mod-mokojoomhero-' . $module->id; @@ -45,8 +49,12 @@ $heightAttr = htmlspecialchars($heroHeight, ENT_QUOTES, 'UTF-8'); data-interval="" > - - + + +
+ +
+ diff --git a/src/tmpl/index.html b/src/packages/mod_mokojoomhero/tmpl/index.html similarity index 100% rename from src/tmpl/index.html rename to src/packages/mod_mokojoomhero/tmpl/index.html diff --git a/src/packages/plg_system_mokojoomhero/index.html b/src/packages/plg_system_mokojoomhero/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/plg_system_mokojoomhero/index.html @@ -0,0 +1 @@ + diff --git a/src/packages/plg_system_mokojoomhero/language/en-GB/index.html b/src/packages/plg_system_mokojoomhero/language/en-GB/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/plg_system_mokojoomhero/language/en-GB/index.html @@ -0,0 +1 @@ + diff --git a/src/packages/plg_system_mokojoomhero/language/en-GB/plg_system_mokojoomhero.ini b/src/packages/plg_system_mokojoomhero/language/en-GB/plg_system_mokojoomhero.ini new file mode 100644 index 0000000..d31b03f --- /dev/null +++ b/src/packages/plg_system_mokojoomhero/language/en-GB/plg_system_mokojoomhero.ini @@ -0,0 +1,2 @@ +; Copyright (C) 2026 Moko Consulting +; SPDX-License-Identifier: GPL-3.0-or-later diff --git a/src/packages/plg_system_mokojoomhero/language/en-GB/plg_system_mokojoomhero.sys.ini b/src/packages/plg_system_mokojoomhero/language/en-GB/plg_system_mokojoomhero.sys.ini new file mode 100644 index 0000000..7dc0e9f --- /dev/null +++ b/src/packages/plg_system_mokojoomhero/language/en-GB/plg_system_mokojoomhero.sys.ini @@ -0,0 +1,5 @@ +; Copyright (C) 2026 Moko Consulting +; SPDX-License-Identifier: GPL-3.0-or-later + +PLG_SYSTEM_MOKOJOOMHERO="System - MokoJoomHero" +PLG_SYSTEM_MOKOJOOMHERO_DESCRIPTION="System plugin for MokoJoomHero — license key validation" diff --git a/src/packages/plg_system_mokojoomhero/language/en-US/index.html b/src/packages/plg_system_mokojoomhero/language/en-US/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/plg_system_mokojoomhero/language/en-US/index.html @@ -0,0 +1 @@ + diff --git a/src/packages/plg_system_mokojoomhero/language/en-US/plg_system_mokojoomhero.ini b/src/packages/plg_system_mokojoomhero/language/en-US/plg_system_mokojoomhero.ini new file mode 100644 index 0000000..d31b03f --- /dev/null +++ b/src/packages/plg_system_mokojoomhero/language/en-US/plg_system_mokojoomhero.ini @@ -0,0 +1,2 @@ +; Copyright (C) 2026 Moko Consulting +; SPDX-License-Identifier: GPL-3.0-or-later diff --git a/src/packages/plg_system_mokojoomhero/language/en-US/plg_system_mokojoomhero.sys.ini b/src/packages/plg_system_mokojoomhero/language/en-US/plg_system_mokojoomhero.sys.ini new file mode 100644 index 0000000..7dc0e9f --- /dev/null +++ b/src/packages/plg_system_mokojoomhero/language/en-US/plg_system_mokojoomhero.sys.ini @@ -0,0 +1,5 @@ +; Copyright (C) 2026 Moko Consulting +; SPDX-License-Identifier: GPL-3.0-or-later + +PLG_SYSTEM_MOKOJOOMHERO="System - MokoJoomHero" +PLG_SYSTEM_MOKOJOOMHERO_DESCRIPTION="System plugin for MokoJoomHero — license key validation" diff --git a/src/packages/plg_system_mokojoomhero/language/index.html b/src/packages/plg_system_mokojoomhero/language/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/plg_system_mokojoomhero/language/index.html @@ -0,0 +1 @@ + diff --git a/src/packages/plg_system_mokojoomhero/mokojoomhero.php b/src/packages/plg_system_mokojoomhero/mokojoomhero.php new file mode 100644 index 0000000..70bada5 --- /dev/null +++ b/src/packages/plg_system_mokojoomhero/mokojoomhero.php @@ -0,0 +1,12 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +defined('_JEXEC') or die; diff --git a/src/packages/plg_system_mokojoomhero/mokojoomhero.xml b/src/packages/plg_system_mokojoomhero/mokojoomhero.xml new file mode 100644 index 0000000..e8782d2 --- /dev/null +++ b/src/packages/plg_system_mokojoomhero/mokojoomhero.xml @@ -0,0 +1,34 @@ + + + + plg_system_mokojoomhero + 01.04.01-dev + 2026-06-02 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_SYSTEM_MOKOJOOMHERO_DESCRIPTION + + Joomla\Plugin\System\MokoJoomHero + + + mokojoomhero.php + services + src + + + + language/en-GB/plg_system_mokojoomhero.ini + language/en-GB/plg_system_mokojoomhero.sys.ini + language/en-US/plg_system_mokojoomhero.ini + language/en-US/plg_system_mokojoomhero.sys.ini + + diff --git a/src/packages/plg_system_mokojoomhero/services/index.html b/src/packages/plg_system_mokojoomhero/services/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/plg_system_mokojoomhero/services/index.html @@ -0,0 +1 @@ + diff --git a/src/packages/plg_system_mokojoomhero/services/provider.php b/src/packages/plg_system_mokojoomhero/services/provider.php new file mode 100644 index 0000000..8c861e2 --- /dev/null +++ b/src/packages/plg_system_mokojoomhero/services/provider.php @@ -0,0 +1,39 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Extension\PluginInterface; +use Joomla\CMS\Factory; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\DI\Container; +use Joomla\DI\ServiceProviderInterface; +use Joomla\Event\DispatcherInterface; +use Joomla\Plugin\System\MokoJoomHero\Extension\MokoJoomHero; + +return new class implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new MokoJoomHero( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('system', 'mokojoomhero') + ); + + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/src/packages/plg_system_mokojoomhero/src/Extension/MokoJoomHero.php b/src/packages/plg_system_mokojoomhero/src/Extension/MokoJoomHero.php new file mode 100644 index 0000000..810af7f --- /dev/null +++ b/src/packages/plg_system_mokojoomhero/src/Extension/MokoJoomHero.php @@ -0,0 +1,90 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +namespace Joomla\Plugin\System\MokoJoomHero\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Event\SubscriberInterface; + +class MokoJoomHero extends CMSPlugin implements SubscriberInterface +{ + public static function getSubscribedEvents(): array + { + return [ + 'onAfterRoute' => 'onAfterRoute', + ]; + } + + public function onAfterRoute(): void + { + $app = $this->getApplication(); + + if ($app->isClient('administrator')) { + $this->warnMissingLicenseKey(); + } + } + + /** + * Warn administrators once per session when no license key is configured. + * + * @return void + */ + private function warnMissingLicenseKey(): void + { + $session = Factory::getSession(); + + if ($session->get('mokojoomhero.license_warned', false)) { + return; + } + + $user = Factory::getUser(); + + if ($user->guest || !$user->authorise('core.manage')) { + return; + } + + $session->set('mokojoomhero.license_warned', true); + + try { + $db = Factory::getDbo(); + + $query = $db->getQuery(true) + ->select($db->quoteName('extra_query')) + ->from($db->quoteName('#__update_sites')) + ->where($db->quoteName('name') . ' = ' . $db->quote('MokoJoomHero Updates')) + ->setLimit(1); + $db->setQuery($query); + $extraQuery = (string) $db->loadResult(); + + if (!empty($extraQuery)) { + parse_str($extraQuery, $parsed); + + if (!empty($parsed['dlid']) && preg_match('/^MOKO-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/', $parsed['dlid'])) { + return; + } + } + + $this->getApplication()->enqueueMessage( + 'Moko Consulting License Key Required — ' + . 'No download key is configured. Updates will not be available until a valid license key is entered. ' + . 'Go to System → Update Sites ' + . 'and enter your license key (MOKO-XXXX-XXXX-XXXX-XXXX) in the Download Key field ' + . 'for the MokoJoomHero update site.', + 'warning' + ); + } catch (\Throwable $e) { + // Don't break admin over a license check + } + } +} diff --git a/src/packages/plg_system_mokojoomhero/src/Extension/index.html b/src/packages/plg_system_mokojoomhero/src/Extension/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/plg_system_mokojoomhero/src/Extension/index.html @@ -0,0 +1 @@ + diff --git a/src/packages/plg_system_mokojoomhero/src/index.html b/src/packages/plg_system_mokojoomhero/src/index.html new file mode 100644 index 0000000..94906bc --- /dev/null +++ b/src/packages/plg_system_mokojoomhero/src/index.html @@ -0,0 +1 @@ + diff --git a/src/pkg_mokojoomhero.xml b/src/pkg_mokojoomhero.xml new file mode 100644 index 0000000..87a3b76 --- /dev/null +++ b/src/pkg_mokojoomhero.xml @@ -0,0 +1,30 @@ + + + + Package - MokoJoomHero + mokojoomhero + 01.04.01-dev + 2026-06-02 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PKG_MOKOJOOMHERO_DESCRIPTION + + pkg_script.php + + + mod_mokojoomhero.zip + plg_system_mokojoomhero.zip + + + + https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/raw/branch/main/updates.xml + + diff --git a/src/pkg_script.php b/src/pkg_script.php new file mode 100644 index 0000000..1901b3a --- /dev/null +++ b/src/pkg_script.php @@ -0,0 +1,43 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\Installer\InstallerAdapter; + +class Pkg_MokoJoomHeroInstallerScript +{ + /** + * Called after install/update. + * + * @param string $type Action type + * @param InstallerAdapter $parent Installer adapter + * + * @return void + */ + public function postflight(string $type, InstallerAdapter $parent): void + { + if ($type === 'install') { + $db = Factory::getDbo(); + + // Enable the system plugin automatically on fresh install + $query = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('enabled') . ' = 1') + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) + ->where($db->quoteName('element') . ' = ' . $db->quote('mokojoomhero')); + + $db->setQuery($query); + $db->execute(); + } + } +} diff --git a/updates.xml b/updates.xml index b4070ae..e53d9ba 100644 --- a/updates.xml +++ b/updates.xml @@ -5,6 +5,26 @@ --> + + + Package - MokoJoomHero + Package - MokoJoomHero development build. + pkg_mokojoomhero + package + 01.07.00-dev + 2026-06-02 + https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/tag/development + + https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/download/development/pkg_mokojoomhero-01.07.00-dev.zip + + + dev + Moko Consulting + https://mokoconsulting.tech + + + + Module - MokoJoomHero Module - MokoJoomHero dev build. -- 2.52.0 From 601cf771703c82cd21b4f21898b467725d2dd277 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 4 Jun 2026 06:46:47 -0500 Subject: [PATCH 20/27] fix: address PR review issues and add configurable slide transitions Fix CSS injection on heroHeight with regex validation, add missing language keys to .sys.ini files, fix plugin manifest languages folder attribute and display name, narrow catch to \Exception with logging, add error handling to pkg_script auto-enable, fix mobile CSS for color/gradient modes, add SPDX headers, remove dead code, and update stale file path headers. Add configurable transition type for image slideshow: crossfade, slide, fade-to-black, and zoom (Ken Burns). Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../language/en-GB/mod_mokojoomhero.ini | 8 +++ .../language/en-GB/mod_mokojoomhero.sys.ini | 24 +++++++- .../language/en-US/mod_mokojoomhero.ini | 8 +++ .../language/en-US/mod_mokojoomhero.sys.ini | 24 +++++++- .../media/css/mod_mokojoomhero.css | 55 +++++++++++++++++-- .../media/js/mod_mokojoomhero.js | 55 +++++++++++++++---- .../mod_mokojoomhero/mod_mokojoomhero.php | 10 +++- .../mod_mokojoomhero/mod_mokojoomhero.xml | 15 ++++- src/packages/mod_mokojoomhero/script.php | 1 + .../mod_mokojoomhero/tmpl/default.php | 7 +-- .../plg_system_mokojoomhero/mokojoomhero.xml | 12 ++-- .../src/Extension/MokoJoomHero.php | 8 ++- src/pkg_script.php | 38 +++++++++---- 13 files changed, 220 insertions(+), 45 deletions(-) diff --git a/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.ini b/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.ini index 2d480a4..3accf02 100644 --- a/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.ini +++ b/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.ini @@ -27,6 +27,14 @@ MOD_MOKOJOOMHERO_MODE_LOCALVIDEO="Local Video" MOD_MOKOJOOMHERO_MODE_COLOR="Solid Colour" MOD_MOKOJOOMHERO_MODE_GRADIENT="Gradient" +; Transition type +MOD_MOKOJOOMHERO_FADE_TYPE_LABEL="Transition Type" +MOD_MOKOJOOMHERO_FADE_TYPE_DESC="How images transition between slides." +MOD_MOKOJOOMHERO_FADE_CROSSFADE="Crossfade" +MOD_MOKOJOOMHERO_FADE_SLIDE="Slide" +MOD_MOKOJOOMHERO_FADE_BLACK="Fade to Black" +MOD_MOKOJOOMHERO_FADE_ZOOM="Zoom (Ken Burns)" + ; Image settings MOD_MOKOJOOMHERO_IMAGE_FOLDER_LABEL="Image Folder" MOD_MOKOJOOMHERO_IMAGE_FOLDER_DESC="Path to folder containing hero images, relative to Joomla root (e.g. images/heroes)." diff --git a/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.sys.ini b/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.sys.ini index 0b8aeff..b4cb5e6 100644 --- a/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.sys.ini +++ b/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.sys.ini @@ -21,10 +21,20 @@ MOD_MOKOJOOMHERO_SHOW_CARD_DESC="Wrap the content in a card with a white backgro ; Hero mode MOD_MOKOJOOMHERO_MODE_LABEL="Hero Mode" -MOD_MOKOJOOMHERO_MODE_DESC="Choose between a slideshow of images, an embedded video (YouTube/Vimeo), or a local video file." +MOD_MOKOJOOMHERO_MODE_DESC="Choose between a slideshow of images, an embedded video (YouTube/Vimeo), a local video file, a solid colour, or a gradient." MOD_MOKOJOOMHERO_MODE_IMAGES="Images" MOD_MOKOJOOMHERO_MODE_VIDEO="Video (YouTube/Vimeo)" MOD_MOKOJOOMHERO_MODE_LOCALVIDEO="Local Video" +MOD_MOKOJOOMHERO_MODE_COLOR="Solid Colour" +MOD_MOKOJOOMHERO_MODE_GRADIENT="Gradient" + +; Transition type +MOD_MOKOJOOMHERO_FADE_TYPE_LABEL="Transition Type" +MOD_MOKOJOOMHERO_FADE_TYPE_DESC="How images transition between slides." +MOD_MOKOJOOMHERO_FADE_CROSSFADE="Crossfade" +MOD_MOKOJOOMHERO_FADE_SLIDE="Slide" +MOD_MOKOJOOMHERO_FADE_BLACK="Fade to Black" +MOD_MOKOJOOMHERO_FADE_ZOOM="Zoom (Ken Burns)" ; Image settings MOD_MOKOJOOMHERO_IMAGE_FOLDER_LABEL="Image Folder" @@ -50,6 +60,18 @@ MOD_MOKOJOOMHERO_CARD_DELAY_DESC="Delay in milliseconds before the content card MOD_MOKOJOOMHERO_MUTE_TOGGLE_LABEL="Show Mute Toggle" MOD_MOKOJOOMHERO_MUTE_TOGGLE_DESC="Show a mute/unmute button on the hero video. Videos always start muted (required for autoplay)." +; Solid colour background +MOD_MOKOJOOMHERO_BG_COLOR_LABEL="Background Colour" +MOD_MOKOJOOMHERO_BG_COLOR_DESC="Solid background colour for the hero section." + +; Gradient background +MOD_MOKOJOOMHERO_GRADIENT_START_LABEL="Gradient Start Colour" +MOD_MOKOJOOMHERO_GRADIENT_START_DESC="Starting colour of the gradient." +MOD_MOKOJOOMHERO_GRADIENT_END_LABEL="Gradient End Colour" +MOD_MOKOJOOMHERO_GRADIENT_END_DESC="Ending colour of the gradient." +MOD_MOKOJOOMHERO_GRADIENT_ANGLE_LABEL="Gradient Angle" +MOD_MOKOJOOMHERO_GRADIENT_ANGLE_DESC="Direction of the gradient in degrees (0 = bottom to top, 90 = left to right, 135 = diagonal)." + ; Hero height MOD_MOKOJOOMHERO_HERO_HEIGHT_LABEL="Hero Height" MOD_MOKOJOOMHERO_HERO_HEIGHT_DESC="Height of the hero section. Use px for fixed pixels (e.g. 400px) or vh for viewport height (e.g. 60vh for 60%% of screen)." diff --git a/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.ini b/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.ini index a6f1ba4..2c4ae6d 100644 --- a/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.ini +++ b/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.ini @@ -27,6 +27,14 @@ MOD_MOKOJOOMHERO_MODE_LOCALVIDEO="Local Video" MOD_MOKOJOOMHERO_MODE_COLOR="Solid Color" MOD_MOKOJOOMHERO_MODE_GRADIENT="Gradient" +; Transition type +MOD_MOKOJOOMHERO_FADE_TYPE_LABEL="Transition Type" +MOD_MOKOJOOMHERO_FADE_TYPE_DESC="How images transition between slides." +MOD_MOKOJOOMHERO_FADE_CROSSFADE="Crossfade" +MOD_MOKOJOOMHERO_FADE_SLIDE="Slide" +MOD_MOKOJOOMHERO_FADE_BLACK="Fade to Black" +MOD_MOKOJOOMHERO_FADE_ZOOM="Zoom (Ken Burns)" + ; Image settings MOD_MOKOJOOMHERO_IMAGE_FOLDER_LABEL="Image Folder" MOD_MOKOJOOMHERO_IMAGE_FOLDER_DESC="Path to folder containing hero images, relative to Joomla root (e.g. images/heroes)." diff --git a/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.sys.ini b/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.sys.ini index 18cc2ab..9bf627a 100644 --- a/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.sys.ini +++ b/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.sys.ini @@ -21,10 +21,20 @@ MOD_MOKOJOOMHERO_SHOW_CARD_DESC="Wrap the content in a card with a white backgro ; Hero mode MOD_MOKOJOOMHERO_MODE_LABEL="Hero Mode" -MOD_MOKOJOOMHERO_MODE_DESC="Choose between a slideshow of images, an embedded video (YouTube/Vimeo), or a local video file." +MOD_MOKOJOOMHERO_MODE_DESC="Choose between a slideshow of images, an embedded video (YouTube/Vimeo), a local video file, a solid color, or a gradient." MOD_MOKOJOOMHERO_MODE_IMAGES="Images" MOD_MOKOJOOMHERO_MODE_VIDEO="Video (YouTube/Vimeo)" MOD_MOKOJOOMHERO_MODE_LOCALVIDEO="Local Video" +MOD_MOKOJOOMHERO_MODE_COLOR="Solid Color" +MOD_MOKOJOOMHERO_MODE_GRADIENT="Gradient" + +; Transition type +MOD_MOKOJOOMHERO_FADE_TYPE_LABEL="Transition Type" +MOD_MOKOJOOMHERO_FADE_TYPE_DESC="How images transition between slides." +MOD_MOKOJOOMHERO_FADE_CROSSFADE="Crossfade" +MOD_MOKOJOOMHERO_FADE_SLIDE="Slide" +MOD_MOKOJOOMHERO_FADE_BLACK="Fade to Black" +MOD_MOKOJOOMHERO_FADE_ZOOM="Zoom (Ken Burns)" ; Image settings MOD_MOKOJOOMHERO_IMAGE_FOLDER_LABEL="Image Folder" @@ -50,6 +60,18 @@ MOD_MOKOJOOMHERO_CARD_DELAY_DESC="Delay in milliseconds before the content card MOD_MOKOJOOMHERO_MUTE_TOGGLE_LABEL="Show Mute Toggle" MOD_MOKOJOOMHERO_MUTE_TOGGLE_DESC="Show a mute/unmute button on the hero video. Videos always start muted (required for autoplay)." +; Solid color background +MOD_MOKOJOOMHERO_BG_COLOR_LABEL="Background Color" +MOD_MOKOJOOMHERO_BG_COLOR_DESC="Solid background color for the hero section." + +; Gradient background +MOD_MOKOJOOMHERO_GRADIENT_START_LABEL="Gradient Start Color" +MOD_MOKOJOOMHERO_GRADIENT_START_DESC="Starting color of the gradient." +MOD_MOKOJOOMHERO_GRADIENT_END_LABEL="Gradient End Color" +MOD_MOKOJOOMHERO_GRADIENT_END_DESC="Ending color of the gradient." +MOD_MOKOJOOMHERO_GRADIENT_ANGLE_LABEL="Gradient Angle" +MOD_MOKOJOOMHERO_GRADIENT_ANGLE_DESC="Direction of the gradient in degrees (0 = bottom to top, 90 = left to right, 135 = diagonal)." + ; Hero height MOD_MOKOJOOMHERO_HERO_HEIGHT_LABEL="Hero Height" MOD_MOKOJOOMHERO_HERO_HEIGHT_DESC="Height of the hero section. Use px for fixed pixels (e.g. 400px) or vh for viewport height (e.g. 60vh for 60% of screen)." diff --git a/src/packages/mod_mokojoomhero/media/css/mod_mokojoomhero.css b/src/packages/mod_mokojoomhero/media/css/mod_mokojoomhero.css index ffda708..7ffda51 100644 --- a/src/packages/mod_mokojoomhero/media/css/mod_mokojoomhero.css +++ b/src/packages/mod_mokojoomhero/media/css/mod_mokojoomhero.css @@ -6,9 +6,9 @@ * DEFGROUP: MokoJoomHero.Module.Assets * INGROUP: MokoJoomHero.Module * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero - * PATH: /src/css/template.css + * PATH: /src/packages/mod_mokojoomhero/media/css/mod_mokojoomhero.css * VERSION: 01.04.01 - * BRIEF: Hero module stylesheet — slideshow, video background, overlay + * BRIEF: Hero module stylesheet — slideshow, video, colour/gradient, overlay, card, mute toggle, responsive */ /* ============================================================ @@ -31,7 +31,7 @@ } /* ============================================================ - Image slides + Image slides — base ============================================================ */ .mokojoomhero__slide { position: absolute; @@ -40,13 +40,51 @@ background-position: center; background-repeat: no-repeat; opacity: 0; - transition: opacity 1s ease; } .mokojoomhero__slide--active { opacity: 1; } +/* ── Crossfade (default) ── */ +.mokojoomhero[data-transition="crossfade"] .mokojoomhero__slide { + transition: opacity 1s ease; +} + +/* ── Slide ── */ +.mokojoomhero[data-transition="slide"] .mokojoomhero__slide { + opacity: 1; + transform: translateX(100%); + transition: transform 0.8s ease; +} + +.mokojoomhero[data-transition="slide"] .mokojoomhero__slide--active { + transform: translateX(0); +} + +.mokojoomhero[data-transition="slide"] .mokojoomhero__slide--exit { + transform: translateX(-100%); +} + +/* ── Fade to black ── */ +.mokojoomhero[data-transition="fade-black"] .mokojoomhero__slide { + transition: opacity 0.6s ease; +} + +/* ── Zoom (Ken Burns) ── */ +.mokojoomhero[data-transition="zoom"] .mokojoomhero__slide { + transition: opacity 1s ease; +} + +.mokojoomhero[data-transition="zoom"] .mokojoomhero__slide--active { + animation: mokojoomhero-zoom 8s ease forwards; +} + +@keyframes mokojoomhero-zoom { + from { transform: scale(1); } + to { transform: scale(1.08); } +} + /* ============================================================ Video background ============================================================ */ @@ -186,8 +224,17 @@ iframe.mokojoomhero__video { display: none; } + /* Keep colour/gradient backgrounds visible on mobile */ + .mokojoomhero__color { + position: relative; + } + .mokojoomhero__overlay { padding: 1rem; + } + + /* Only clear overlay background when media is hidden (image/video modes) */ + .mokojoomhero:not(:has(.mokojoomhero__color)) .mokojoomhero__overlay { background-color: transparent !important; } diff --git a/src/packages/mod_mokojoomhero/media/js/mod_mokojoomhero.js b/src/packages/mod_mokojoomhero/media/js/mod_mokojoomhero.js index c5ddbe8..17dbfdc 100644 --- a/src/packages/mod_mokojoomhero/media/js/mod_mokojoomhero.js +++ b/src/packages/mod_mokojoomhero/media/js/mod_mokojoomhero.js @@ -7,9 +7,9 @@ * DEFGROUP: MokoJoomHero.Module.Assets * INGROUP: MokoJoomHero.Module * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero - * PATH: /src/js/template.js + * PATH: /src/packages/mod_mokojoomhero/media/js/mod_mokojoomhero.js * VERSION: 01.04.01 - * BRIEF: Hero module JavaScript — image slideshow crossfade + * BRIEF: Hero module JavaScript — slideshow crossfade, video viewport control, mute toggle */ 'use strict'; @@ -22,23 +22,54 @@ document.addEventListener('DOMContentLoaded', function () { // ── Image slideshow ── document.querySelectorAll('.mokojoomhero[data-slides]').forEach(function (hero) { - var slides = hero.querySelectorAll('.mokojoomhero__slide'); - var interval = parseInt(hero.dataset.interval, 10) || 5000; - var current = 0; + var slides = hero.querySelectorAll('.mokojoomhero__slide'); + var interval = parseInt(hero.dataset.interval, 10) || 5000; + var transition = hero.dataset.transition || 'crossfade'; + var current = 0; if (slides.length < 2) { return; } - setInterval(function () { - slides[current].classList.remove('mokojoomhero__slide--active'); - slides[current].setAttribute('aria-hidden', 'true'); - + function advanceSlide() { + var prev = current; current = (current + 1) % slides.length; - slides[current].classList.add('mokojoomhero__slide--active'); - slides[current].setAttribute('aria-hidden', 'false'); - }, interval); + if (transition === 'slide') { + slides[prev].classList.add('mokojoomhero__slide--exit'); + slides[prev].classList.remove('mokojoomhero__slide--active'); + slides[prev].setAttribute('aria-hidden', 'true'); + + slides[current].classList.add('mokojoomhero__slide--active'); + slides[current].setAttribute('aria-hidden', 'false'); + + // Reset exiting slide after transition completes + setTimeout(function () { + slides[prev].classList.remove('mokojoomhero__slide--exit'); + }, 800); + + } else if (transition === 'fade-black') { + // Phase 1: fade out current + slides[prev].classList.remove('mokojoomhero__slide--active'); + slides[prev].setAttribute('aria-hidden', 'true'); + + // Phase 2: fade in next after a brief black gap + setTimeout(function () { + slides[current].classList.add('mokojoomhero__slide--active'); + slides[current].setAttribute('aria-hidden', 'false'); + }, 600); + + } else { + // Crossfade and zoom use the same JS logic + slides[prev].classList.remove('mokojoomhero__slide--active'); + slides[prev].setAttribute('aria-hidden', 'true'); + + slides[current].classList.add('mokojoomhero__slide--active'); + slides[current].setAttribute('aria-hidden', 'false'); + } + } + + setInterval(advanceSlide, interval); }); // ── Pause/resume videos when out of viewport ── diff --git a/src/packages/mod_mokojoomhero/mod_mokojoomhero.php b/src/packages/mod_mokojoomhero/mod_mokojoomhero.php index e264f9e..4ae2aa1 100644 --- a/src/packages/mod_mokojoomhero/mod_mokojoomhero.php +++ b/src/packages/mod_mokojoomhero/mod_mokojoomhero.php @@ -6,6 +6,7 @@ * * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GPL-3.0-or-later + * SPDX-License-Identifier: GPL-3.0-or-later */ defined('_JEXEC') or die; @@ -33,8 +34,14 @@ $heroMode = $params->get('heroMode', 'images'); $imageFolder = $params->get('imageFolder', 'images/heroes'); $imageCount = (int) $params->get('imageCount', 5); $slideInterval = (int) $params->get('slideInterval', 5000); +$fadeType = $params->get('fadeType', 'crossfade'); $videoFile = $params->get('videoFile', ''); $heroHeight = $params->get('heroHeight', '60vh'); + +// Validate heroHeight to prevent CSS injection +if (!preg_match('/^\d+(\.\d+)?(px|vh|vw|em|rem|%)$/', $heroHeight)) { + $heroHeight = '60vh'; +} $overlayColor = $params->get('overlayColor', '#000000'); $overlayOpacity = (float) $params->get('overlayOpacity', 0.5); $textAlign = $params->get('textAlign', 'center'); @@ -98,7 +105,4 @@ if ($heroMode === 'localvideo' && $localVideoFile) { } } -// Module content from the editor (overlay text) -$content = $module->content ?? ''; - require ModuleHelper::getLayoutPath('mod_mokojoomhero', $params->get('layout', 'default')); diff --git a/src/packages/mod_mokojoomhero/mod_mokojoomhero.xml b/src/packages/mod_mokojoomhero/mod_mokojoomhero.xml index 36a9544..3e47e1b 100644 --- a/src/packages/mod_mokojoomhero/mod_mokojoomhero.xml +++ b/src/packages/mod_mokojoomhero/mod_mokojoomhero.xml @@ -10,7 +10,7 @@ DEFGROUP: MokoJoomHero.Module INGROUP: MokoJoomHero REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero - PATH: /src/mod_mokojoomhero.xml + PATH: /src/packages/mod_mokojoomhero/mod_mokojoomhero.xml VERSION: 01.00.20 BRIEF: Joomla module manifest — random hero image with content overlay --> @@ -99,6 +99,19 @@ step="15" showon="heroMode:gradient" /> + + + + + + id; // Convert hex overlay colour to rgba @@ -47,6 +45,7 @@ $heightAttr = htmlspecialchars($heroHeight, ENT_QUOTES, 'UTF-8'); 1) : ?> data-slides="" data-interval="" + data-transition="" > diff --git a/src/packages/plg_system_mokojoomhero/mokojoomhero.xml b/src/packages/plg_system_mokojoomhero/mokojoomhero.xml index e8782d2..d03d5cc 100644 --- a/src/packages/plg_system_mokojoomhero/mokojoomhero.xml +++ b/src/packages/plg_system_mokojoomhero/mokojoomhero.xml @@ -7,7 +7,7 @@ * @license GNU General Public License version 3 or later; see LICENSE --> - plg_system_mokojoomhero + PLG_SYSTEM_MOKOJOOMHERO 01.04.01-dev 2026-06-02 Moko Consulting @@ -25,10 +25,10 @@ src - - language/en-GB/plg_system_mokojoomhero.ini - language/en-GB/plg_system_mokojoomhero.sys.ini - language/en-US/plg_system_mokojoomhero.ini - language/en-US/plg_system_mokojoomhero.sys.ini + + en-GB/plg_system_mokojoomhero.ini + en-GB/plg_system_mokojoomhero.sys.ini + en-US/plg_system_mokojoomhero.ini + en-US/plg_system_mokojoomhero.sys.ini diff --git a/src/packages/plg_system_mokojoomhero/src/Extension/MokoJoomHero.php b/src/packages/plg_system_mokojoomhero/src/Extension/MokoJoomHero.php index 810af7f..85be0eb 100644 --- a/src/packages/plg_system_mokojoomhero/src/Extension/MokoJoomHero.php +++ b/src/packages/plg_system_mokojoomhero/src/Extension/MokoJoomHero.php @@ -83,8 +83,12 @@ class MokoJoomHero extends CMSPlugin implements SubscriberInterface . 'for the MokoJoomHero update site.', 'warning' ); - } catch (\Throwable $e) { - // Don't break admin over a license check + } catch (\Exception $e) { + // Don't break admin over a license check — log for diagnostics + $this->getApplication()->getLogger()->warning( + 'MokoJoomHero license check failed: ' . $e->getMessage(), + ['exception' => $e] + ); } } } diff --git a/src/pkg_script.php b/src/pkg_script.php index 1901b3a..5976c07 100644 --- a/src/pkg_script.php +++ b/src/pkg_script.php @@ -16,7 +16,7 @@ use Joomla\CMS\Installer\InstallerAdapter; class Pkg_MokoJoomHeroInstallerScript { /** - * Called after install/update. + * Called after install/update — only enables the system plugin on fresh install. * * @param string $type Action type * @param InstallerAdapter $parent Installer adapter @@ -26,18 +26,34 @@ class Pkg_MokoJoomHeroInstallerScript public function postflight(string $type, InstallerAdapter $parent): void { if ($type === 'install') { - $db = Factory::getDbo(); + try { + $db = Factory::getDbo(); - // Enable the system plugin automatically on fresh install - $query = $db->getQuery(true) - ->update($db->quoteName('#__extensions')) - ->set($db->quoteName('enabled') . ' = 1') - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) - ->where($db->quoteName('element') . ' = ' . $db->quote('mokojoomhero')); + // Enable the system plugin automatically on fresh install + $query = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('enabled') . ' = 1') + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) + ->where($db->quoteName('element') . ' = ' . $db->quote('mokojoomhero')); - $db->setQuery($query); - $db->execute(); + $db->setQuery($query); + $db->execute(); + + if ($db->getAffectedRows() === 0) { + Factory::getApplication()->enqueueMessage( + 'MokoJoomHero: The system plugin could not be auto-enabled. ' + . 'Please enable it manually in Extensions → Plugins.', + 'warning' + ); + } + } catch (\Exception $e) { + Factory::getApplication()->enqueueMessage( + 'MokoJoomHero: Failed to auto-enable system plugin: ' . $e->getMessage() + . ' — Please enable it manually in Extensions → Plugins.', + 'warning' + ); + } } } } -- 2.52.0 From fe3abf6ddbd0fad90d31290d4ea657648db2e9ad Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 4 Jun 2026 07:13:04 -0500 Subject: [PATCH 21/27] feat: vertical alignment, mobile height, and gradient overlay (#53, #54, #55) Add vertical text alignment (top/center/bottom) for overlay content, mobile-specific hero height via CSS custom property, and directional gradient overlay (dark at bottom/top/left/right) reusing existing overlay colour controls. Language strings added to all locale files. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../language/en-GB/mod_mokojoomhero.ini | 19 ++++++++++- .../language/en-GB/mod_mokojoomhero.sys.ini | 19 ++++++++++- .../language/en-US/mod_mokojoomhero.ini | 19 ++++++++++- .../language/en-US/mod_mokojoomhero.sys.ini | 19 ++++++++++- .../media/css/mod_mokojoomhero.css | 3 +- .../mod_mokojoomhero/mod_mokojoomhero.php | 7 ++++ .../mod_mokojoomhero/mod_mokojoomhero.xml | 33 +++++++++++++++++++ .../mod_mokojoomhero/tmpl/default.php | 30 +++++++++++++++-- 8 files changed, 141 insertions(+), 8 deletions(-) diff --git a/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.ini b/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.ini index 3accf02..8ed7f4b 100644 --- a/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.ini +++ b/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.ini @@ -76,18 +76,35 @@ MOD_MOKOJOOMHERO_HERO_HEIGHT_LABEL="Hero Height" MOD_MOKOJOOMHERO_HERO_HEIGHT_DESC="Height of the hero section. Use px for fixed pixels (e.g. 400px) or vh for viewport height (e.g. 60vh for 60%% of screen)." MOD_MOKOJOOMHERO_HERO_HEIGHT_HINT="e.g. 60vh or 400px" +; Hero height (mobile) +MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_LABEL="Mobile Hero Height" +MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_DESC="Height of the hero on mobile devices. Leave empty for auto height. Uses the same units as Hero Height." +MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_HINT="e.g. 40vh or 300px (empty = auto)" + ; Overlay fieldset MOD_MOKOJOOMHERO_FIELDSET_OVERLAY="Overlay & Text" +MOD_MOKOJOOMHERO_OVERLAY_TYPE_LABEL="Overlay Type" +MOD_MOKOJOOMHERO_OVERLAY_TYPE_DESC="How the overlay is applied. Solid fills evenly; gradient fades from transparent to opaque in the chosen direction." +MOD_MOKOJOOMHERO_OVERLAY_SOLID="Solid" +MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_BOTTOM="Gradient (dark at bottom)" +MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_TOP="Gradient (dark at top)" +MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_LEFT="Gradient (dark at left)" +MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_RIGHT="Gradient (dark at right)" MOD_MOKOJOOMHERO_OVERLAY_COLOR_LABEL="Overlay Colour" MOD_MOKOJOOMHERO_OVERLAY_COLOR_DESC="Background colour of the overlay on top of the hero image." MOD_MOKOJOOMHERO_OVERLAY_OPACITY_LABEL="Overlay Opacity" MOD_MOKOJOOMHERO_OVERLAY_OPACITY_DESC="Transparency of the overlay (0 = fully transparent, 1 = fully opaque)." MOD_MOKOJOOMHERO_TEXT_ALIGN_LABEL="Text Alignment" MOD_MOKOJOOMHERO_TEXT_ALIGN_DESC="Horizontal alignment of the overlay text." +MOD_MOKOJOOMHERO_VALIGN_LABEL="Vertical Alignment" +MOD_MOKOJOOMHERO_VALIGN_DESC="Vertical position of the content within the hero." +MOD_MOKOJOOMHERO_VALIGN_TOP="Top" +MOD_MOKOJOOMHERO_VALIGN_CENTER="Centre" +MOD_MOKOJOOMHERO_VALIGN_BOTTOM="Bottom" MOD_MOKOJOOMHERO_TEXT_COLOR_LABEL="Text Colour" MOD_MOKOJOOMHERO_TEXT_COLOR_DESC="Colour of the text displayed over the hero image." -; Alignment options +; Horizontal alignment options MOD_MOKOJOOMHERO_ALIGN_LEFT="Left" MOD_MOKOJOOMHERO_ALIGN_CENTER="Centre" MOD_MOKOJOOMHERO_ALIGN_RIGHT="Right" diff --git a/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.sys.ini b/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.sys.ini index b4cb5e6..74d7d4d 100644 --- a/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.sys.ini +++ b/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.sys.ini @@ -77,18 +77,35 @@ MOD_MOKOJOOMHERO_HERO_HEIGHT_LABEL="Hero Height" MOD_MOKOJOOMHERO_HERO_HEIGHT_DESC="Height of the hero section. Use px for fixed pixels (e.g. 400px) or vh for viewport height (e.g. 60vh for 60%% of screen)." MOD_MOKOJOOMHERO_HERO_HEIGHT_HINT="e.g. 60vh or 400px" +; Hero height (mobile) +MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_LABEL="Mobile Hero Height" +MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_DESC="Height of the hero on mobile devices. Leave empty for auto height." +MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_HINT="e.g. 40vh or 300px (empty = auto)" + ; Overlay fieldset MOD_MOKOJOOMHERO_FIELDSET_OVERLAY="Overlay & Text" +MOD_MOKOJOOMHERO_OVERLAY_TYPE_LABEL="Overlay Type" +MOD_MOKOJOOMHERO_OVERLAY_TYPE_DESC="How the overlay is applied. Solid fills evenly; gradient fades from transparent to opaque in the chosen direction." +MOD_MOKOJOOMHERO_OVERLAY_SOLID="Solid" +MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_BOTTOM="Gradient (dark at bottom)" +MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_TOP="Gradient (dark at top)" +MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_LEFT="Gradient (dark at left)" +MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_RIGHT="Gradient (dark at right)" MOD_MOKOJOOMHERO_OVERLAY_COLOR_LABEL="Overlay Colour" MOD_MOKOJOOMHERO_OVERLAY_COLOR_DESC="Background colour of the overlay on top of the hero image." MOD_MOKOJOOMHERO_OVERLAY_OPACITY_LABEL="Overlay Opacity" MOD_MOKOJOOMHERO_OVERLAY_OPACITY_DESC="Transparency of the overlay (0 = fully transparent, 1 = fully opaque)." MOD_MOKOJOOMHERO_TEXT_ALIGN_LABEL="Text Alignment" MOD_MOKOJOOMHERO_TEXT_ALIGN_DESC="Horizontal alignment of the overlay text." +MOD_MOKOJOOMHERO_VALIGN_LABEL="Vertical Alignment" +MOD_MOKOJOOMHERO_VALIGN_DESC="Vertical position of the content within the hero." +MOD_MOKOJOOMHERO_VALIGN_TOP="Top" +MOD_MOKOJOOMHERO_VALIGN_CENTER="Centre" +MOD_MOKOJOOMHERO_VALIGN_BOTTOM="Bottom" MOD_MOKOJOOMHERO_TEXT_COLOR_LABEL="Text Colour" MOD_MOKOJOOMHERO_TEXT_COLOR_DESC="Colour of the text displayed over the hero image." -; Alignment options +; Horizontal alignment options MOD_MOKOJOOMHERO_ALIGN_LEFT="Left" MOD_MOKOJOOMHERO_ALIGN_CENTER="Centre" MOD_MOKOJOOMHERO_ALIGN_RIGHT="Right" diff --git a/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.ini b/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.ini index 2c4ae6d..3586845 100644 --- a/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.ini +++ b/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.ini @@ -76,18 +76,35 @@ MOD_MOKOJOOMHERO_CARD_DELAY_DESC="Delay in milliseconds before the content card MOD_MOKOJOOMHERO_MUTE_TOGGLE_LABEL="Show Mute Toggle" MOD_MOKOJOOMHERO_MUTE_TOGGLE_DESC="Show a mute/unmute button on the hero video. Videos always start muted (required for autoplay)." +; Hero height (mobile) +MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_LABEL="Mobile Hero Height" +MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_DESC="Height of the hero on mobile devices. Leave empty for auto height. Uses the same units as Hero Height." +MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_HINT="e.g. 40vh or 300px (empty = auto)" + ; Overlay fieldset MOD_MOKOJOOMHERO_FIELDSET_OVERLAY="Overlay & Text" +MOD_MOKOJOOMHERO_OVERLAY_TYPE_LABEL="Overlay Type" +MOD_MOKOJOOMHERO_OVERLAY_TYPE_DESC="How the overlay is applied. Solid fills evenly; gradient fades from transparent to opaque in the chosen direction." +MOD_MOKOJOOMHERO_OVERLAY_SOLID="Solid" +MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_BOTTOM="Gradient (dark at bottom)" +MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_TOP="Gradient (dark at top)" +MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_LEFT="Gradient (dark at left)" +MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_RIGHT="Gradient (dark at right)" MOD_MOKOJOOMHERO_OVERLAY_COLOR_LABEL="Overlay Color" MOD_MOKOJOOMHERO_OVERLAY_COLOR_DESC="Background color of the overlay on top of the hero image." MOD_MOKOJOOMHERO_OVERLAY_OPACITY_LABEL="Overlay Opacity" MOD_MOKOJOOMHERO_OVERLAY_OPACITY_DESC="Transparency of the overlay (0 = fully transparent, 1 = fully opaque)." MOD_MOKOJOOMHERO_TEXT_ALIGN_LABEL="Text Alignment" MOD_MOKOJOOMHERO_TEXT_ALIGN_DESC="Horizontal alignment of the overlay text." +MOD_MOKOJOOMHERO_VALIGN_LABEL="Vertical Alignment" +MOD_MOKOJOOMHERO_VALIGN_DESC="Vertical position of the content within the hero." +MOD_MOKOJOOMHERO_VALIGN_TOP="Top" +MOD_MOKOJOOMHERO_VALIGN_CENTER="Center" +MOD_MOKOJOOMHERO_VALIGN_BOTTOM="Bottom" MOD_MOKOJOOMHERO_TEXT_COLOR_LABEL="Text Color" MOD_MOKOJOOMHERO_TEXT_COLOR_DESC="Color of the text displayed over the hero image." -; Alignment options +; Horizontal alignment options MOD_MOKOJOOMHERO_ALIGN_LEFT="Left" MOD_MOKOJOOMHERO_ALIGN_CENTER="Center" MOD_MOKOJOOMHERO_ALIGN_RIGHT="Right" diff --git a/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.sys.ini b/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.sys.ini index 9bf627a..02f36cd 100644 --- a/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.sys.ini +++ b/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.sys.ini @@ -77,18 +77,35 @@ MOD_MOKOJOOMHERO_HERO_HEIGHT_LABEL="Hero Height" MOD_MOKOJOOMHERO_HERO_HEIGHT_DESC="Height of the hero section. Use px for fixed pixels (e.g. 400px) or vh for viewport height (e.g. 60vh for 60% of screen)." MOD_MOKOJOOMHERO_HERO_HEIGHT_HINT="e.g. 60vh or 400px" +; Hero height (mobile) +MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_LABEL="Mobile Hero Height" +MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_DESC="Height of the hero on mobile devices. Leave empty for auto height." +MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_HINT="e.g. 40vh or 300px (empty = auto)" + ; Overlay fieldset MOD_MOKOJOOMHERO_FIELDSET_OVERLAY="Overlay & Text" +MOD_MOKOJOOMHERO_OVERLAY_TYPE_LABEL="Overlay Type" +MOD_MOKOJOOMHERO_OVERLAY_TYPE_DESC="How the overlay is applied. Solid fills evenly; gradient fades from transparent to opaque in the chosen direction." +MOD_MOKOJOOMHERO_OVERLAY_SOLID="Solid" +MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_BOTTOM="Gradient (dark at bottom)" +MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_TOP="Gradient (dark at top)" +MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_LEFT="Gradient (dark at left)" +MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_RIGHT="Gradient (dark at right)" MOD_MOKOJOOMHERO_OVERLAY_COLOR_LABEL="Overlay Color" MOD_MOKOJOOMHERO_OVERLAY_COLOR_DESC="Background color of the overlay on top of the hero image." MOD_MOKOJOOMHERO_OVERLAY_OPACITY_LABEL="Overlay Opacity" MOD_MOKOJOOMHERO_OVERLAY_OPACITY_DESC="Transparency of the overlay (0 = fully transparent, 1 = fully opaque)." MOD_MOKOJOOMHERO_TEXT_ALIGN_LABEL="Text Alignment" MOD_MOKOJOOMHERO_TEXT_ALIGN_DESC="Horizontal alignment of the overlay text." +MOD_MOKOJOOMHERO_VALIGN_LABEL="Vertical Alignment" +MOD_MOKOJOOMHERO_VALIGN_DESC="Vertical position of the content within the hero." +MOD_MOKOJOOMHERO_VALIGN_TOP="Top" +MOD_MOKOJOOMHERO_VALIGN_CENTER="Center" +MOD_MOKOJOOMHERO_VALIGN_BOTTOM="Bottom" MOD_MOKOJOOMHERO_TEXT_COLOR_LABEL="Text Color" MOD_MOKOJOOMHERO_TEXT_COLOR_DESC="Color of the text displayed over the hero image." -; Alignment options +; Horizontal alignment options MOD_MOKOJOOMHERO_ALIGN_LEFT="Left" MOD_MOKOJOOMHERO_ALIGN_CENTER="Center" MOD_MOKOJOOMHERO_ALIGN_RIGHT="Right" diff --git a/src/packages/mod_mokojoomhero/media/css/mod_mokojoomhero.css b/src/packages/mod_mokojoomhero/media/css/mod_mokojoomhero.css index 7ffda51..2bdc018 100644 --- a/src/packages/mod_mokojoomhero/media/css/mod_mokojoomhero.css +++ b/src/packages/mod_mokojoomhero/media/css/mod_mokojoomhero.css @@ -121,7 +121,6 @@ iframe.mokojoomhero__video { position: relative; z-index: 1; display: flex; - align-items: center; justify-content: center; width: 100%; height: 100%; @@ -216,7 +215,7 @@ iframe.mokojoomhero__video { ============================================================ */ @media (max-width: 768px) { .mokojoomhero { - height: auto !important; + height: var(--mokojoomhero-mobile-height, auto) !important; } .mokojoomhero__video, diff --git a/src/packages/mod_mokojoomhero/mod_mokojoomhero.php b/src/packages/mod_mokojoomhero/mod_mokojoomhero.php index 4ae2aa1..b71cfe9 100644 --- a/src/packages/mod_mokojoomhero/mod_mokojoomhero.php +++ b/src/packages/mod_mokojoomhero/mod_mokojoomhero.php @@ -42,9 +42,16 @@ $heroHeight = $params->get('heroHeight', '60vh'); if (!preg_match('/^\d+(\.\d+)?(px|vh|vw|em|rem|%)$/', $heroHeight)) { $heroHeight = '60vh'; } +$heroHeightMobile = $params->get('heroHeightMobile', ''); + +if ($heroHeightMobile && !preg_match('/^\d+(\.\d+)?(px|vh|vw|em|rem|%)$/', $heroHeightMobile)) { + $heroHeightMobile = ''; +} $overlayColor = $params->get('overlayColor', '#000000'); +$overlayType = $params->get('overlayType', 'solid'); $overlayOpacity = (float) $params->get('overlayOpacity', 0.5); $textAlign = $params->get('textAlign', 'center'); +$verticalAlign = $params->get('verticalAlign', 'center'); $textColor = $params->get('textColor', '#ffffff'); $heroContent = $params->get('heroContent', ''); $showCard = (bool) $params->get('showCard', 1); diff --git a/src/packages/mod_mokojoomhero/mod_mokojoomhero.xml b/src/packages/mod_mokojoomhero/mod_mokojoomhero.xml index 3e47e1b..76d2666 100644 --- a/src/packages/mod_mokojoomhero/mod_mokojoomhero.xml +++ b/src/packages/mod_mokojoomhero/mod_mokojoomhero.xml @@ -166,6 +166,15 @@ default="60vh" filter="string" /> + + + + + + + + MOD_MOKOJOOMHERO_ALIGN_CENTER + + + + + id; $r = hexdec(substr($overlayColor, 1, 2)); $g = hexdec(substr($overlayColor, 3, 2)); $b = hexdec(substr($overlayColor, 5, 2)); -$rgba = "rgba($r, $g, $b, $overlayOpacity)"; +$rgbaOpaque = "rgba($r, $g, $b, $overlayOpacity)"; +$rgbaTransparent = "rgba($r, $g, $b, 0)"; + +// Build overlay background based on type +$overlayDirections = [ + 'gradient-bottom' => 'to bottom', + 'gradient-top' => 'to top', + 'gradient-left' => 'to left', + 'gradient-right' => 'to right', +]; + +if ($overlayType !== 'solid' && isset($overlayDirections[$overlayType])) { + $dir = $overlayDirections[$overlayType]; + $overlayBg = "background: linear-gradient($dir, $rgbaTransparent, $rgbaOpaque);"; +} else { + $overlayBg = "background-color: $rgbaOpaque;"; +} + +// Map vertical alignment to CSS align-items +$valignMap = ['top' => 'flex-start', 'center' => 'center', 'bottom' => 'flex-end']; +$valignCss = $valignMap[$verticalAlign] ?? 'center'; $heightAttr = htmlspecialchars($heroHeight, ENT_QUOTES, 'UTF-8'); ?> + + +
1) : ?> data-slides="" @@ -77,7 +103,7 @@ $heightAttr = htmlspecialchars($heroHeight, ENT_QUOTES, 'UTF-8'); -
+
showtitle) : ?> -- 2.52.0 From f5fdf6742f37d1ec0c5d48a65652b000b2db513a Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 4 Jun 2026 07:23:31 -0500 Subject: [PATCH 22/27] fix: input validation, JS error handling, and package description Add hex color validation for all color params, allowlist validation for textAlign/fadeType/overlayType, range clamp for gradientAngle, and try-catch around DirectoryIterator. Fix video.play() promise rejection and iframe.contentWindow null guards in JS. Hardcode package description in manifest. Normalize tabs throughout. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../media/js/mod_mokojoomhero.js | 13 +- .../mod_mokojoomhero/mod_mokojoomhero.php | 128 ++++++++++++------ src/pkg_mokojoomhero.xml | 2 +- 3 files changed, 98 insertions(+), 45 deletions(-) diff --git a/src/packages/mod_mokojoomhero/media/js/mod_mokojoomhero.js b/src/packages/mod_mokojoomhero/media/js/mod_mokojoomhero.js index 17dbfdc..6dfb249 100644 --- a/src/packages/mod_mokojoomhero/media/js/mod_mokojoomhero.js +++ b/src/packages/mod_mokojoomhero/media/js/mod_mokojoomhero.js @@ -86,9 +86,14 @@ document.addEventListener('DOMContentLoaded', function () { if (entry.isIntersecting) { // Resume if (video) { - video.play(); + var playPromise = video.play(); + if (playPromise !== undefined) { + playPromise.catch(function () { + // Autoplay blocked by browser policy — not actionable + }); + } } - if (iframe) { + if (iframe && iframe.contentWindow) { var src = iframe.src || ''; if (src.indexOf('youtube') !== -1) { iframe.contentWindow.postMessage('{"event":"command","func":"playVideo","args":""}', '*'); @@ -101,7 +106,7 @@ document.addEventListener('DOMContentLoaded', function () { if (video) { video.pause(); } - if (iframe) { + if (iframe && iframe.contentWindow) { var src = iframe.src || ''; if (src.indexOf('youtube') !== -1) { iframe.contentWindow.postMessage('{"event":"command","func":"pauseVideo","args":""}', '*'); @@ -130,7 +135,7 @@ document.addEventListener('DOMContentLoaded', function () { if (video) { video.muted = !muted; } - if (iframe) { + if (iframe && iframe.contentWindow) { var src = iframe.src || ''; if (src.indexOf('youtube') !== -1) { var func = muted ? 'unMute' : 'mute'; diff --git a/src/packages/mod_mokojoomhero/mod_mokojoomhero.php b/src/packages/mod_mokojoomhero/mod_mokojoomhero.php index b71cfe9..fe01842 100644 --- a/src/packages/mod_mokojoomhero/mod_mokojoomhero.php +++ b/src/packages/mod_mokojoomhero/mod_mokojoomhero.php @@ -34,19 +34,10 @@ $heroMode = $params->get('heroMode', 'images'); $imageFolder = $params->get('imageFolder', 'images/heroes'); $imageCount = (int) $params->get('imageCount', 5); $slideInterval = (int) $params->get('slideInterval', 5000); -$fadeType = $params->get('fadeType', 'crossfade'); +$fadeType = $params->get('fadeType', 'crossfade'); $videoFile = $params->get('videoFile', ''); $heroHeight = $params->get('heroHeight', '60vh'); - -// Validate heroHeight to prevent CSS injection -if (!preg_match('/^\d+(\.\d+)?(px|vh|vw|em|rem|%)$/', $heroHeight)) { - $heroHeight = '60vh'; -} $heroHeightMobile = $params->get('heroHeightMobile', ''); - -if ($heroHeightMobile && !preg_match('/^\d+(\.\d+)?(px|vh|vw|em|rem|%)$/', $heroHeightMobile)) { - $heroHeightMobile = ''; -} $overlayColor = $params->get('overlayColor', '#000000'); $overlayType = $params->get('overlayType', 'solid'); $overlayOpacity = (float) $params->get('overlayOpacity', 0.5); @@ -63,31 +54,88 @@ $gradientStart = $params->get('gradientStart', '#003366'); $gradientEnd = $params->get('gradientEnd', '#006699'); $gradientAngle = (int) $params->get('gradientAngle', 135); +// Validate CSS height values to prevent injection +if (!preg_match('/^\d+(\.\d+)?(px|vh|vw|em|rem|%)$/', $heroHeight)) { + $heroHeight = '60vh'; +} + +if ($heroHeightMobile && !preg_match('/^\d+(\.\d+)?(px|vh|vw|em|rem|%)$/', $heroHeightMobile)) { + $heroHeightMobile = ''; +} + +// Validate hex colour values +$hexColorPattern = '/^#[0-9a-fA-F]{6}$/'; + +if (!preg_match($hexColorPattern, $overlayColor)) { + $overlayColor = '#000000'; +} + +if (!preg_match($hexColorPattern, $textColor)) { + $textColor = '#ffffff'; +} + +if (!preg_match($hexColorPattern, $bgColor)) { + $bgColor = '#003366'; +} + +if (!preg_match($hexColorPattern, $gradientStart)) { + $gradientStart = '#003366'; +} + +if (!preg_match($hexColorPattern, $gradientEnd)) { + $gradientEnd = '#006699'; +} + +// Validate allowlist values +$allowedTextAlign = ['left', 'center', 'right']; + +if (!in_array($textAlign, $allowedTextAlign, true)) { + $textAlign = 'center'; +} + +$allowedFadeTypes = ['crossfade', 'slide', 'fade-black', 'zoom']; + +if (!in_array($fadeType, $allowedFadeTypes, true)) { + $fadeType = 'crossfade'; +} + +$allowedOverlayTypes = ['solid', 'gradient-bottom', 'gradient-top', 'gradient-left', 'gradient-right']; + +if (!in_array($overlayType, $allowedOverlayTypes, true)) { + $overlayType = 'solid'; +} + +$gradientAngle = max(0, min(360, $gradientAngle)); + // Collect hero images $heroImages = []; if ($heroMode === 'images') { - $folderPath = JPATH_ROOT . '/' . ltrim($imageFolder, '/'); + $folderPath = JPATH_ROOT . '/' . ltrim($imageFolder, '/'); - if (is_dir($folderPath)) { - $allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif', 'svg']; - $all = []; + if (is_dir($folderPath)) { + try { + $allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif', 'svg']; + $all = []; - foreach (new DirectoryIterator($folderPath) as $file) { - if ($file->isFile() && in_array(strtolower($file->getExtension()), $allowed, true)) { - $all[] = $file->getFilename(); - } - } + foreach (new DirectoryIterator($folderPath) as $file) { + if ($file->isFile() && in_array(strtolower($file->getExtension()), $allowed, true)) { + $all[] = $file->getFilename(); + } + } - if ($all) { - shuffle($all); - $picked = array_slice($all, 0, min($imageCount, 5)); + if ($all) { + shuffle($all); + $picked = array_slice($all, 0, min($imageCount, 5)); - foreach ($picked as $filename) { - $heroImages[] = Uri::root() . $imageFolder . '/' . $filename; - } - } - } + foreach ($picked as $filename) { + $heroImages[] = Uri::root() . $imageFolder . '/' . $filename; + } + } + } catch (\UnexpectedValueException $e) { + // Folder not readable — hero renders without background + } + } } // Build video URL — smartly detect YouTube, Vimeo, or local/direct file @@ -96,20 +144,20 @@ $youtubeId = ''; $vimeoId = ''; if ($heroMode === 'localvideo' && $localVideoFile) { - $videoUrl = Uri::root() . ltrim($localVideoFile, '/'); + $videoUrl = Uri::root() . ltrim($localVideoFile, '/'); } elseif ($heroMode === 'video' && $videoFile) { - // YouTube: watch, embed, shorts, youtu.be, with optional timestamps/params - if (preg_match('/(?:youtube\.com\/(?:watch\?.*v=|embed\/|shorts\/|v\/)|youtu\.be\/)([\w-]{11})/', $videoFile, $m)) { - $youtubeId = $m[1]; - // Vimeo: vimeo.com/123456 or player.vimeo.com/video/123456 - } elseif (preg_match('/vimeo\.com\/(?:video\/)?(\d+)/', $videoFile, $m)) { - $vimeoId = $m[1]; - } else { - // Direct URL or local file path - $videoUrl = (strpos($videoFile, '://') !== false) - ? $videoFile - : Uri::root() . ltrim($videoFile, '/'); - } + // YouTube: watch, embed, shorts, youtu.be, with optional timestamps/params + if (preg_match('/(?:youtube\.com\/(?:watch\?.*v=|embed\/|shorts\/|v\/)|youtu\.be\/)([\w-]{11})/', $videoFile, $m)) { + $youtubeId = $m[1]; + // Vimeo: vimeo.com/123456 or player.vimeo.com/video/123456 + } elseif (preg_match('/vimeo\.com\/(?:video\/)?(\d+)/', $videoFile, $m)) { + $vimeoId = $m[1]; + } else { + // Direct URL or local file path + $videoUrl = (strpos($videoFile, '://') !== false) + ? $videoFile + : Uri::root() . ltrim($videoFile, '/'); + } } require ModuleHelper::getLayoutPath('mod_mokojoomhero', $params->get('layout', 'default')); diff --git a/src/pkg_mokojoomhero.xml b/src/pkg_mokojoomhero.xml index 87a3b76..1149b1e 100644 --- a/src/pkg_mokojoomhero.xml +++ b/src/pkg_mokojoomhero.xml @@ -15,7 +15,7 @@ https://mokoconsulting.tech Copyright (C) 2026 Moko Consulting. All rights reserved. GPL-3.0-or-later - PKG_MOKOJOOMHERO_DESCRIPTION + Random hero image slideshow or background video with content overlay. Includes the hero module and system plugin for license key validation. By Moko Consulting. pkg_script.php -- 2.52.0 From 4a18f46c68bfebf4f115200465bf157d87d365b5 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 4 Jun 2026 07:30:12 -0500 Subject: [PATCH 23/27] feat: reduced motion, scroll indicator, and video poster (#49, #50, #51) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add prefers-reduced-motion support (WCAG 2.1 AA) — disables slideshow cycling, CSS transitions/animations, and Ken Burns zoom when OS setting is enabled. Add optional scroll-down chevron indicator with bounce animation and smooth-scroll click handler. Add video poster image fallback displayed while video loads. Language strings in all locales. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../language/en-GB/mod_mokojoomhero.ini | 8 +++ .../language/en-GB/mod_mokojoomhero.sys.ini | 8 +++ .../language/en-US/mod_mokojoomhero.ini | 8 +++ .../language/en-US/mod_mokojoomhero.sys.ini | 8 +++ .../media/css/mod_mokojoomhero.css | 67 +++++++++++++++++++ .../media/js/mod_mokojoomhero.js | 30 ++++++++- .../mod_mokojoomhero/mod_mokojoomhero.php | 6 +- .../mod_mokojoomhero/mod_mokojoomhero.xml | 19 ++++++ .../mod_mokojoomhero/tmpl/default.php | 19 +++++- 9 files changed, 169 insertions(+), 4 deletions(-) diff --git a/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.ini b/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.ini index 8ed7f4b..f5fdd7e 100644 --- a/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.ini +++ b/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.ini @@ -55,6 +55,14 @@ MOD_MOKOJOOMHERO_LOCAL_VIDEO_DESC="Select a video file from the Media Manager (m MOD_MOKOJOOMHERO_CARD_DELAY_LABEL="Card Fade-in Delay (ms)" MOD_MOKOJOOMHERO_CARD_DELAY_DESC="Delay in milliseconds before the content card fades in. Set to 0 for no delay." +; Video poster +MOD_MOKOJOOMHERO_VIDEO_POSTER_LABEL="Video Poster Image" +MOD_MOKOJOOMHERO_VIDEO_POSTER_DESC="Fallback image displayed while the video loads. Prevents a blank hero on slow connections." + +; Scroll indicator +MOD_MOKOJOOMHERO_SCROLL_INDICATOR_LABEL="Show Scroll Indicator" +MOD_MOKOJOOMHERO_SCROLL_INDICATOR_DESC="Show an animated down-arrow at the bottom of the hero prompting users to scroll." + ; Mute toggle MOD_MOKOJOOMHERO_MUTE_TOGGLE_LABEL="Show Mute Toggle" MOD_MOKOJOOMHERO_MUTE_TOGGLE_DESC="Show a mute/unmute button on the hero video. Videos always start muted (required for autoplay)." diff --git a/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.sys.ini b/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.sys.ini index 74d7d4d..4230603 100644 --- a/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.sys.ini +++ b/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.sys.ini @@ -56,6 +56,14 @@ MOD_MOKOJOOMHERO_LOCAL_VIDEO_DESC="Select a video file from the Media Manager (m MOD_MOKOJOOMHERO_CARD_DELAY_LABEL="Card Fade-in Delay (ms)" MOD_MOKOJOOMHERO_CARD_DELAY_DESC="Delay in milliseconds before the content card fades in. Set to 0 for no delay." +; Video poster +MOD_MOKOJOOMHERO_VIDEO_POSTER_LABEL="Video Poster Image" +MOD_MOKOJOOMHERO_VIDEO_POSTER_DESC="Fallback image displayed while the video loads." + +; Scroll indicator +MOD_MOKOJOOMHERO_SCROLL_INDICATOR_LABEL="Show Scroll Indicator" +MOD_MOKOJOOMHERO_SCROLL_INDICATOR_DESC="Show an animated down-arrow at the bottom of the hero prompting users to scroll." + ; Mute toggle MOD_MOKOJOOMHERO_MUTE_TOGGLE_LABEL="Show Mute Toggle" MOD_MOKOJOOMHERO_MUTE_TOGGLE_DESC="Show a mute/unmute button on the hero video. Videos always start muted (required for autoplay)." diff --git a/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.ini b/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.ini index 3586845..1703f17 100644 --- a/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.ini +++ b/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.ini @@ -72,6 +72,14 @@ MOD_MOKOJOOMHERO_HERO_HEIGHT_HINT="e.g. 60vh or 400px" MOD_MOKOJOOMHERO_CARD_DELAY_LABEL="Card Fade-in Delay (ms)" MOD_MOKOJOOMHERO_CARD_DELAY_DESC="Delay in milliseconds before the content card fades in. Set to 0 for no delay." +; Video poster +MOD_MOKOJOOMHERO_VIDEO_POSTER_LABEL="Video Poster Image" +MOD_MOKOJOOMHERO_VIDEO_POSTER_DESC="Fallback image displayed while the video loads. Prevents a blank hero on slow connections." + +; Scroll indicator +MOD_MOKOJOOMHERO_SCROLL_INDICATOR_LABEL="Show Scroll Indicator" +MOD_MOKOJOOMHERO_SCROLL_INDICATOR_DESC="Show an animated down-arrow at the bottom of the hero prompting users to scroll." + ; Mute toggle MOD_MOKOJOOMHERO_MUTE_TOGGLE_LABEL="Show Mute Toggle" MOD_MOKOJOOMHERO_MUTE_TOGGLE_DESC="Show a mute/unmute button on the hero video. Videos always start muted (required for autoplay)." diff --git a/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.sys.ini b/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.sys.ini index 02f36cd..06af777 100644 --- a/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.sys.ini +++ b/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.sys.ini @@ -56,6 +56,14 @@ MOD_MOKOJOOMHERO_LOCAL_VIDEO_DESC="Select a video file from the Media Manager (m MOD_MOKOJOOMHERO_CARD_DELAY_LABEL="Card Fade-in Delay (ms)" MOD_MOKOJOOMHERO_CARD_DELAY_DESC="Delay in milliseconds before the content card fades in. Set to 0 for no delay." +; Video poster +MOD_MOKOJOOMHERO_VIDEO_POSTER_LABEL="Video Poster Image" +MOD_MOKOJOOMHERO_VIDEO_POSTER_DESC="Fallback image displayed while the video loads." + +; Scroll indicator +MOD_MOKOJOOMHERO_SCROLL_INDICATOR_LABEL="Show Scroll Indicator" +MOD_MOKOJOOMHERO_SCROLL_INDICATOR_DESC="Show an animated down-arrow at the bottom of the hero prompting users to scroll." + ; Mute toggle MOD_MOKOJOOMHERO_MUTE_TOGGLE_LABEL="Show Mute Toggle" MOD_MOKOJOOMHERO_MUTE_TOGGLE_DESC="Show a mute/unmute button on the hero video. Videos always start muted (required for autoplay)." diff --git a/src/packages/mod_mokojoomhero/media/css/mod_mokojoomhero.css b/src/packages/mod_mokojoomhero/media/css/mod_mokojoomhero.css index 2bdc018..9d8f03a 100644 --- a/src/packages/mod_mokojoomhero/media/css/mod_mokojoomhero.css +++ b/src/packages/mod_mokojoomhero/media/css/mod_mokojoomhero.css @@ -210,6 +210,73 @@ iframe.mokojoomhero__video { background: rgba(0, 0, 0, 0.7); } +/* ============================================================ + Video poster image + ============================================================ */ +.mokojoomhero__poster { + position: absolute; + inset: 0; + background-size: cover; + background-position: center; + background-repeat: no-repeat; +} + +/* ============================================================ + Scroll-down indicator + ============================================================ */ +.mokojoomhero__scroll-indicator { + position: absolute; + bottom: 1.5rem; + left: 50%; + transform: translateX(-50%); + z-index: 2; + background: none; + border: none; + color: #fff; + cursor: pointer; + padding: 0; + opacity: 0.8; + transition: opacity 0.3s; + animation: mokojoomhero-bounce 2s infinite; +} + +.mokojoomhero__scroll-indicator:hover { + opacity: 1; +} + +.mokojoomhero__scroll-indicator--hidden { + display: none; +} + +@keyframes mokojoomhero-bounce { + 0%, 20%, 50%, 80%, 100% { transform: translateX(-50%) translateY(0); } + 40% { transform: translateX(-50%) translateY(-8px); } + 60% { transform: translateX(-50%) translateY(-4px); } +} + +/* ============================================================ + Reduced motion — WCAG 2.1 AA (SC 2.3.3) + ============================================================ */ +@media (prefers-reduced-motion: reduce) { + .mokojoomhero__slide { + transition: none !important; + animation: none !important; + } + + .mokojoomhero__card[data-card-delay] { + opacity: 1; + animation: none !important; + } + + .mokojoomhero__scroll-indicator { + animation: none; + } + + .mokojoomhero[data-transition="zoom"] .mokojoomhero__slide--active { + animation: none !important; + } +} + /* ============================================================ Responsive ============================================================ */ diff --git a/src/packages/mod_mokojoomhero/media/js/mod_mokojoomhero.js b/src/packages/mod_mokojoomhero/media/js/mod_mokojoomhero.js index 6dfb249..73f6ca9 100644 --- a/src/packages/mod_mokojoomhero/media/js/mod_mokojoomhero.js +++ b/src/packages/mod_mokojoomhero/media/js/mod_mokojoomhero.js @@ -20,6 +20,8 @@ document.addEventListener('DOMContentLoaded', function () { return; } + var prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + // ── Image slideshow ── document.querySelectorAll('.mokojoomhero[data-slides]').forEach(function (hero) { var slides = hero.querySelectorAll('.mokojoomhero__slide'); @@ -27,7 +29,7 @@ document.addEventListener('DOMContentLoaded', function () { var transition = hero.dataset.transition || 'crossfade'; var current = 0; - if (slides.length < 2) { + if (slides.length < 2 || prefersReducedMotion) { return; } @@ -122,6 +124,32 @@ document.addEventListener('DOMContentLoaded', function () { observer.observe(hero); }); + // ── Scroll-down indicator ── + document.querySelectorAll('.mokojoomhero__scroll-indicator').forEach(function (btn) { + var hero = btn.closest('.mokojoomhero'); + + btn.addEventListener('click', function () { + var nextEl = hero.nextElementSibling || hero.parentElement.nextElementSibling; + + if (nextEl) { + nextEl.scrollIntoView({ behavior: prefersReducedMotion ? 'auto' : 'smooth' }); + } + }); + + // Hide indicator once hero scrolls out of view + var scrollObserver = new IntersectionObserver(function (entries) { + entries.forEach(function (entry) { + if (!entry.isIntersecting) { + btn.classList.add('mokojoomhero__scroll-indicator--hidden'); + } else { + btn.classList.remove('mokojoomhero__scroll-indicator--hidden'); + } + }); + }, { threshold: 0.1 }); + + scrollObserver.observe(hero); + }); + // ── Mute/unmute toggle ── document.querySelectorAll('.mokojoomhero__mute-toggle').forEach(function (btn) { var hero = btn.closest('.mokojoomhero'); diff --git a/src/packages/mod_mokojoomhero/mod_mokojoomhero.php b/src/packages/mod_mokojoomhero/mod_mokojoomhero.php index fe01842..96d000f 100644 --- a/src/packages/mod_mokojoomhero/mod_mokojoomhero.php +++ b/src/packages/mod_mokojoomhero/mod_mokojoomhero.php @@ -47,8 +47,10 @@ $textColor = $params->get('textColor', '#ffffff'); $heroContent = $params->get('heroContent', ''); $showCard = (bool) $params->get('showCard', 1); $cardDelay = (int) $params->get('cardDelay', 0); -$showMuteToggle = (bool) $params->get('showMuteToggle', 0); -$localVideoFile = $params->get('localVideoFile', ''); +$showMuteToggle = (bool) $params->get('showMuteToggle', 0); +$videoPoster = $params->get('videoPoster', ''); +$showScrollIndicator = (bool) $params->get('showScrollIndicator', 0); +$localVideoFile = $params->get('localVideoFile', ''); $bgColor = $params->get('bgColor', '#003366'); $gradientStart = $params->get('gradientStart', '#003366'); $gradientEnd = $params->get('gradientEnd', '#006699'); diff --git a/src/packages/mod_mokojoomhero/mod_mokojoomhero.xml b/src/packages/mod_mokojoomhero/mod_mokojoomhero.xml index 76d2666..3557a15 100644 --- a/src/packages/mod_mokojoomhero/mod_mokojoomhero.xml +++ b/src/packages/mod_mokojoomhero/mod_mokojoomhero.xml @@ -175,6 +175,25 @@ default="" filter="string" /> + + + + +
+ +
+ + +
+ -