From 7a97b228b0f27296c9783ce750d6202484b25d9a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 23:43:45 +0000 Subject: [PATCH 1/3] Initial plan From 1881ced6bff557d2b306159c2c648d7185b0ad5d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 23:51:45 +0000 Subject: [PATCH 2/3] Changes before error encountered Co-authored-by: jmiller-moko <230051081+jmiller-moko@users.noreply.github.com> --- .github/workflows/release_pipeline.yml | 100 ++++++++++++------------- scripts/release/update_dates.sh | 8 -- 2 files changed, 50 insertions(+), 58 deletions(-) diff --git a/.github/workflows/release_pipeline.yml b/.github/workflows/release_pipeline.yml index f8f0b04..8ad4c2f 100644 --- a/.github/workflows/release_pipeline.yml +++ b/.github/workflows/release_pipeline.yml @@ -475,14 +475,14 @@ jobs: - name: Validate required secrets and variables env: - FTP_SERVER: ${{ secrets.FTP_SERVER }} - FTP_USER: ${{ secrets.FTP_USER }} - FTP_KEY: ${{ secrets.FTP_KEY }} - FTP_PASSWORD: ${{ secrets.FTP_PASSWORD }} - FTP_PATH: ${{ secrets.FTP_PATH }} - FTP_PROTOCOL: ${{ secrets.FTP_PROTOCOL }} - FTP_PORT: ${{ secrets.FTP_PORT }} - FTP_PATH_SUFFIX: ${{ vars.FTP_PATH_SUFFIX }} + RC_SERVER: ${{ secrets.RC_SERVER }} + RC_USER: ${{ secrets.RC_USER }} + RC_KEY: ${{ secrets.RC_KEY }} + RC_PASSWORD: ${{ secrets.RC_PASSWORD }} + RC_PATH: ${{ secrets.RC_PATH }} + RC_PROTOCOL: ${{ secrets.RC_PROTOCOL }} + RC_PORT: ${{ secrets.RC_PORT }} + RC_PATH_SUFFIX: ${{ vars.RC_PATH_SUFFIX }} CHANNEL: ${{ needs.guard.outputs.channel }} DEPLOY_DRY_RUN: ${{ vars.DEPLOY_DRY_RUN }} run: | @@ -490,22 +490,22 @@ jobs: missing=() - [ -n "${FTP_SERVER:-}" ] || missing+=("FTP_SERVER") - [ -n "${FTP_USER:-}" ] || missing+=("FTP_USER") - [ -n "${FTP_PATH:-}" ] || missing+=("FTP_PATH") + [ -n "${RC_SERVER:-}" ] || missing+=("RC_SERVER") + [ -n "${RC_USER:-}" ] || missing+=("RC_USER") + [ -n "${RC_PATH:-}" ] || missing+=("RC_PATH") - proto="${FTP_PROTOCOL:-sftp}" - if [ -n "${FTP_PROTOCOL:-}" ] && [ "${proto}" != "sftp" ]; then - missing+=("FTP_PROTOCOL_INVALID") + proto="${RC_PROTOCOL:-sftp}" + if [ -n "${RC_PROTOCOL:-}" ] && [ "${proto}" != "sftp" ]; then + missing+=("RC_PROTOCOL_INVALID") fi key_present=false - if [ -n "${FTP_KEY:-}" ]; then + if [ -n "${RC_KEY:-}" ]; then key_present=true fi pw_present=false - if [ -n "${FTP_PASSWORD:-}" ]; then + if [ -n "${RC_PASSWORD:-}" ]; then pw_present=true fi @@ -515,7 +515,7 @@ jobs: fi if [ "${auth_mode}" = "password" ] && [ "${pw_present}" != "true" ]; then - missing+=("FTP_PASSWORD_REQUIRED") + missing+=("RC_PASSWORD_REQUIRED") fi { @@ -527,7 +527,7 @@ jobs: printf '%s"%s"' "${sep}" "${m}" sep=","; done - printf '],"channel":"%s","deploy_dry_run":"%s","credential_presence":{"FTP_KEY":"%s","FTP_PASSWORD":"%s"}}\n' \ + printf '],"channel":"%s","deploy_dry_run":"%s","credential_presence":{"RC_KEY":"%s","RC_PASSWORD":"%s"}}\n' \ "${CHANNEL}" "${DEPLOY_DRY_RUN:-false}" \ "$( [ "${key_present}" = "true" ] && echo present || echo missing )" \ "$( [ "${pw_present}" = "true" ] && echo present || echo missing )" @@ -712,14 +712,14 @@ jobs: - name: Upload ZIP to SFTP (key-preferred, password-fallback, overwrite, verified) id: sftp env: - FTP_SERVER: ${{ secrets.FTP_SERVER }} - FTP_USER: ${{ secrets.FTP_USER }} - FTP_KEY: ${{ secrets.FTP_KEY }} - FTP_PASSWORD: ${{ secrets.FTP_PASSWORD }} - FTP_PATH: ${{ secrets.FTP_PATH }} - FTP_PROTOCOL: ${{ secrets.FTP_PROTOCOL }} - FTP_PORT: ${{ secrets.FTP_PORT }} - FTP_PATH_SUFFIX: ${{ vars.FTP_PATH_SUFFIX }} + RC_SERVER: ${{ secrets.RC_SERVER }} + RC_USER: ${{ secrets.RC_USER }} + RC_KEY: ${{ secrets.RC_KEY }} + RC_PASSWORD: ${{ secrets.RC_PASSWORD }} + RC_PATH: ${{ secrets.RC_PATH }} + RC_PROTOCOL: ${{ secrets.RC_PROTOCOL }} + RC_PORT: ${{ secrets.RC_PORT }} + RC_PATH_SUFFIX: ${{ vars.RC_PATH_SUFFIX }} CHANNEL: ${{ needs.guard.outputs.channel }} DEPLOY_DRY_RUN: ${{ vars.DEPLOY_DRY_RUN }} run: | @@ -728,37 +728,37 @@ jobs: ZIP="${{ steps.build.outputs.zip_name }}" DIST_DIR="${{ steps.build.outputs.dist_dir }}" - : "${FTP_SERVER:?Missing secret FTP_SERVER}" - : "${FTP_USER:?Missing secret FTP_USER}" - : "${FTP_PATH:?Missing secret FTP_PATH}" + : "${RC_SERVER:?Missing secret RC_SERVER}" + : "${RC_USER:?Missing secret RC_USER}" + : "${RC_PATH:?Missing secret RC_PATH}" - PROTOCOL="${FTP_PROTOCOL:-sftp}" + PROTOCOL="${RC_PROTOCOL:-sftp}" if [ "${PROTOCOL}" != "sftp" ]; then echo "ERROR: Only SFTP permitted" >> "${GITHUB_STEP_SUMMARY}" exit 1 fi - PORT="${FTP_PORT:-}" + PORT="${RC_PORT:-}" if [ -n "${PORT}" ]; then - HOSTPORT="${FTP_SERVER}:${PORT}" + HOSTPORT="${RC_SERVER}:${PORT}" else - HOSTPORT="${FTP_SERVER}" + HOSTPORT="${RC_SERVER}" fi - SUFFIX="${FTP_PATH_SUFFIX:-}" + SUFFIX="${RC_PATH_SUFFIX:-}" if [ -n "${SUFFIX}" ]; then - REMOTE_PATH="${FTP_PATH%/}/${SUFFIX%/}/${CHANNEL}" + REMOTE_PATH="${RC_PATH%/}/${SUFFIX%/}/${CHANNEL}" else - REMOTE_PATH="${FTP_PATH%/}/${CHANNEL}" + REMOTE_PATH="${RC_PATH%/}/${CHANNEL}" fi AUTH_MODE="password" - if [ -n "${FTP_KEY:-}" ]; then + if [ -n "${RC_KEY:-}" ]; then AUTH_MODE="key" fi - if [ "${AUTH_MODE}" = "password" ] && [ -z "${FTP_PASSWORD:-}" ]; then - echo "ERROR: FTP_PASSWORD required when FTP_KEY is not provided" >> "${GITHUB_STEP_SUMMARY}" + if [ "${AUTH_MODE}" = "password" ] && [ -z "${RC_PASSWORD:-}" ]; then + echo "ERROR: RC_PASSWORD required when RC_KEY is not provided" >> "${GITHUB_STEP_SUMMARY}" exit 1 fi @@ -773,7 +773,7 @@ jobs: printf '{' printf '"protocol":"sftp",' printf '"auth_mode":"%s",' "${AUTH_MODE}" - printf '"host":"%s",' "${FTP_SERVER}" + printf '"host":"%s",' "${RC_SERVER}" printf '"port":"%s",' "${PORT:-default}" printf '"remote_path":"%s",' "${REMOTE_PATH}" printf '"overwrite":true,' @@ -789,14 +789,14 @@ jobs: chmod 700 ~/.ssh if [ "${AUTH_MODE}" = "key" ]; then - if printf '%s' "${FTP_KEY}" | head -n 1 | grep -q '^PuTTY-User-Key-File-'; then - printf '%s' "${FTP_KEY}" > ~/.ssh/key.ppk + if printf '%s' "${RC_KEY}" | head -n 1 | grep -q '^PuTTY-User-Key-File-'; then + printf '%s' "${RC_KEY}" > ~/.ssh/key.ppk chmod 600 ~/.ssh/key.ppk if grep -Eq '^Encryption: *none[[:space:]]*$' ~/.ssh/key.ppk; then PPK_PASSPHRASE="" else - PPK_PASSPHRASE="${FTP_PASSWORD:-}" + PPK_PASSPHRASE="${RC_PASSWORD:-}" fi if [ -n "${PPK_PASSPHRASE}" ]; then @@ -808,26 +808,26 @@ jobs: rm -f ~/.ssh/key.ppk chmod 600 ~/.ssh/id_rsa else - printf '%s' "${FTP_KEY}" > ~/.ssh/id_rsa + printf '%s' "${RC_KEY}" > ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa fi fi - ssh-keyscan -H "${FTP_SERVER}" >> ~/.ssh/known_hosts + ssh-keyscan -H "${RC_SERVER}" >> ~/.ssh/known_hosts if [ "${AUTH_MODE}" = "key" ]; then CONNECT="set sftp:connect-program 'ssh -a -x -i ~/.ssh/id_rsa -o PubkeyAuthentication=yes -o PasswordAuthentication=no'" - OPEN="open -u '${FTP_USER}', sftp://${HOSTPORT}" + OPEN="open -u '${RC_USER}', sftp://${HOSTPORT}" else CONNECT="set sftp:connect-program 'ssh -a -x -o PubkeyAuthentication=no -o PasswordAuthentication=yes'" - OPEN="open -u '${FTP_USER}','${FTP_PASSWORD}', sftp://${HOSTPORT}" + OPEN="open -u '${RC_USER}','${RC_PASSWORD}', sftp://${HOSTPORT}" fi if [ "${DRY_RUN}" = "true" ]; then echo "Dry run enabled. Upload skipped." >> "${GITHUB_STEP_SUMMARY}" echo "auth_mode=${AUTH_MODE}" >> "${GITHUB_OUTPUT}" echo "remote_path=${REMOTE_PATH}" >> "${GITHUB_OUTPUT}" - echo "host=${FTP_SERVER}" >> "${GITHUB_OUTPUT}" + echo "host=${RC_SERVER}" >> "${GITHUB_OUTPUT}" echo "port=${PORT:-default}" >> "${GITHUB_OUTPUT}" exit 0 fi @@ -864,13 +864,13 @@ jobs: { echo "### SFTP upload report" echo "\`\`\`json" - echo "{\"status\":\"ok\",\"protocol\":\"sftp\",\"auth_mode\":\"${AUTH_MODE}\",\"host\":\"${FTP_SERVER}\",\"port\":\"${PORT:-default}\",\"remote_path\":\"${REMOTE_PATH}\",\"zip\":\"${ZIP}\",\"zip_bytes_local\":${ZIP_BYTES_LOCAL},\"overwrite\":true}" + echo "{\"status\":\"ok\",\"protocol\":\"sftp\",\"auth_mode\":\"${AUTH_MODE}\",\"host\":\"${RC_SERVER}\",\"port\":\"${PORT:-default}\",\"remote_path\":\"${REMOTE_PATH}\",\"zip\":\"${ZIP}\",\"zip_bytes_local\":${ZIP_BYTES_LOCAL},\"overwrite\":true}" echo "\`\`\`" } >> "${GITHUB_STEP_SUMMARY}" echo "auth_mode=${AUTH_MODE}" >> "${GITHUB_OUTPUT}" echo "remote_path=${REMOTE_PATH}" >> "${GITHUB_OUTPUT}" - echo "host=${FTP_SERVER}" >> "${GITHUB_OUTPUT}" + echo "host=${RC_SERVER}" >> "${GITHUB_OUTPUT}" echo "port=${PORT:-default}" >> "${GITHUB_OUTPUT}" - name: Create Git tag diff --git a/scripts/release/update_dates.sh b/scripts/release/update_dates.sh index 77f6031..a623911 100755 --- a/scripts/release/update_dates.sh +++ b/scripts/release/update_dates.sh @@ -66,12 +66,4 @@ else echo "⚠ Warning: src/templates/templateDetails.xml not found" fi -# Update updates.xml - replace the tag -if [ -f "updates.xml" ]; then - sed -i "s|[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}|${TODAY}|" updates.xml - echo "✓ Updated updates.xml creationDate to ${TODAY}" -else - echo "⚠ Warning: updates.xml not found" -fi - echo "Date normalization complete." From 1388e6610abccbec34956d7486d78b542b83f131 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 18 Jan 2026 00:57:21 +0000 Subject: [PATCH 3/3] Pull workflows from MokoStandards and add reusable workflows Co-authored-by: jmiller-moko <230051081+jmiller-moko@users.noreply.github.com> --- .github/workflows/ci.yml | 37 +- .github/workflows/dependency-review.yml | 4 +- .github/workflows/release_pipeline.yml | 4 +- .github/workflows/reusable-build.yml | 222 ++++++++ .github/workflows/reusable-ci-validation.yml | 534 ++++++++++++++++++ .github/workflows/reusable-deploy.yml | 312 ++++++++++ .github/workflows/reusable-joomla-testing.yml | 356 ++++++++++++ .github/workflows/reusable-php-quality.yml | 297 ++++++++++ .../workflows/reusable-project-detector.yml | 138 +++++ .github/workflows/reusable-release.yml | 397 +++++++++++++ .../workflows/reusable-script-executor.yml | 210 +++++++ .github/workflows/standards-compliance.yml | 433 ++------------ .github/workflows/version_branch.yml | 17 +- 13 files changed, 2559 insertions(+), 402 deletions(-) create mode 100644 .github/workflows/reusable-build.yml create mode 100644 .github/workflows/reusable-ci-validation.yml create mode 100644 .github/workflows/reusable-deploy.yml create mode 100644 .github/workflows/reusable-joomla-testing.yml create mode 100644 .github/workflows/reusable-php-quality.yml create mode 100644 .github/workflows/reusable-project-detector.yml create mode 100644 .github/workflows/reusable-release.yml create mode 100644 .github/workflows/reusable-script-executor.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 14bfb47..071e619 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,3 +1,31 @@ +# Copyright (C) 2026 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# FILE INFORMATION +# DEFGROUP: GitHub.Workflow +# INGROUP: MokoStandards.CI +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /.github/workflows/ci.yml +# VERSION: 02.00.00 +# BRIEF: Continuous integration workflow using local reusable workflow +# NOTE: Delegates CI execution to local reusable-ci-validation.yml for repository validation + name: Continuous Integration on: @@ -16,10 +44,13 @@ on: permissions: contents: read + pull-requests: write + checks: write jobs: - validation: - uses: mokoconsulting-tech/MokoStandards/.github/workflows/reusable-ci-validation.yml@main + ci: + name: Repository Validation Pipeline + uses: ./.github/workflows/reusable-ci-validation.yml with: - validation-scripts-path: 'scripts/validate' + profile: full secrets: inherit diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index afef9ff..e122b44 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -19,8 +19,8 @@ # # FILE INFORMATION # DEFGROUP: GitHub.Workflow -# INGROUP: Moko-Cassiopeia.Security -# REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia +# INGROUP: MokoStandards.Security +# REPO: https://github.com/mokoconsulting-tech/MokoStandards # PATH: /.github/workflows/dependency-review.yml # VERSION: 01.00.00 # BRIEF: Dependency review workflow for vulnerability scanning in pull requests diff --git a/.github/workflows/release_pipeline.yml b/.github/workflows/release_pipeline.yml index 8ad4c2f..e080033 100644 --- a/.github/workflows/release_pipeline.yml +++ b/.github/workflows/release_pipeline.yml @@ -902,7 +902,7 @@ jobs: SFTP_AUTH_MODE: ${{ steps.sftp.outputs.auth_mode }} SFTP_REMOTE_PATH: ${{ steps.sftp.outputs.remote_path }} SFTP_HOST: ${{ steps.sftp.outputs.host }} - SFTP_PORT: ${{ steps.sftp.outputs.port }} + SRC_PORT: ${{ steps.sftp.outputs.port }} run: | set -euo pipefail @@ -925,7 +925,7 @@ jobs: echo "- auth_mode: ${SFTP_AUTH_MODE:-unknown}" echo "- remote_path: ${SFTP_REMOTE_PATH:-unknown}" echo "- host: ${SFTP_HOST:-unknown}" - echo "- port: ${SFTP_PORT:-unknown}" + echo "- port: ${SRC_PORT:-unknown}" } >> RELEASE_NOTES.md - name: Create GitHub release and attach ZIP diff --git a/.github/workflows/reusable-build.yml b/.github/workflows/reusable-build.yml new file mode 100644 index 0000000..42079e5 --- /dev/null +++ b/.github/workflows/reusable-build.yml @@ -0,0 +1,222 @@ +# Copyright (C) 2026 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# FILE INFORMATION +# DEFGROUP: GitHub.Workflow +# INGROUP: MokoStandards.Reusable +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /.github/workflows/reusable-build.yml +# VERSION: 01.00.00 +# BRIEF: Reusable type-aware build workflow for Joomla, Dolibarr, and generic projects +# NOTE: Automatically detects project type and applies appropriate build steps + +name: Reusable Build + +on: + workflow_call: + inputs: + php-version: + description: 'PHP version to use for build' + required: false + type: string + default: '8.1' + node-version: + description: 'Node.js version to use for build' + required: false + type: string + default: '20.x' + working-directory: + description: 'Working directory for build' + required: false + type: string + default: '.' + upload-artifacts: + description: 'Upload build artifacts' + required: false + type: boolean + default: true + artifact-name: + description: 'Name for uploaded artifacts' + required: false + type: string + default: 'build-artifacts' + +permissions: + contents: read + +jobs: + detect: + name: Detect Project Type + uses: ./.github/workflows/reusable-project-detector.yml + with: + working-directory: ${{ inputs.working-directory }} + + build: + name: Build (${{ needs.detect.outputs.project-type }}) + runs-on: ubuntu-latest + needs: detect + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup PHP + if: needs.detect.outputs.has-php == 'true' + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ inputs.php-version }} + extensions: mbstring, xml, zip, json + tools: composer:v2 + + - name: Setup Node.js + if: needs.detect.outputs.has-node == 'true' + uses: actions/setup-node@v6 + with: + node-version: ${{ inputs.node-version }} + + - name: Get Composer cache directory + if: needs.detect.outputs.has-php == 'true' + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache Composer dependencies + if: needs.detect.outputs.has-php == 'true' + uses: actions/cache@v5 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Cache Node modules + if: needs.detect.outputs.has-node == 'true' + uses: actions/cache@v5 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: ${{ runner.os }}-node- + + - name: Install PHP dependencies + if: needs.detect.outputs.has-php == 'true' + working-directory: ${{ inputs.working-directory }} + run: | + if [ -f "composer.json" ]; then + composer install --prefer-dist --no-progress --no-interaction + echo "✅ Composer dependencies installed" >> $GITHUB_STEP_SUMMARY + fi + + - name: Install Node dependencies + if: needs.detect.outputs.has-node == 'true' + working-directory: ${{ inputs.working-directory }} + run: | + if [ -f "package.json" ]; then + npm ci + echo "✅ npm dependencies installed" >> $GITHUB_STEP_SUMMARY + fi + + - name: Build Joomla Extension + if: needs.detect.outputs.project-type == 'joomla' + working-directory: ${{ inputs.working-directory }} + run: | + echo "### 🏗️ Building Joomla Extension" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Extension Type:** ${{ needs.detect.outputs.extension-type }}" >> $GITHUB_STEP_SUMMARY + + # Run npm build if package.json has build script + if [ -f "package.json" ] && grep -q '"build"' package.json; then + echo "Running npm build..." + npm run build + echo "- ✅ npm build completed" >> $GITHUB_STEP_SUMMARY + fi + + # Run composer scripts if available + if [ -f "composer.json" ] && grep -q '"build"' composer.json; then + echo "Running composer build..." + composer run-script build + echo "- ✅ composer build completed" >> $GITHUB_STEP_SUMMARY + fi + + echo "- ✅ Joomla extension build completed" >> $GITHUB_STEP_SUMMARY + + - name: Build Dolibarr Module + if: needs.detect.outputs.project-type == 'dolibarr' + working-directory: ${{ inputs.working-directory }} + run: | + echo "### 🏗️ Building Dolibarr Module" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Run npm build if available + if [ -f "package.json" ] && grep -q '"build"' package.json; then + echo "Running npm build..." + npm run build + echo "- ✅ npm build completed" >> $GITHUB_STEP_SUMMARY + fi + + # Install Dolibarr-specific dependencies + if [ -f "composer.json" ]; then + composer install --no-dev --optimize-autoloader + echo "- ✅ Production dependencies installed" >> $GITHUB_STEP_SUMMARY + fi + + echo "- ✅ Dolibarr module build completed" >> $GITHUB_STEP_SUMMARY + + - name: Build Generic Project + if: needs.detect.outputs.project-type == 'generic' + working-directory: ${{ inputs.working-directory }} + run: | + echo "### 🏗️ Building Generic Project" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Try various build methods + if [ -f "package.json" ] && grep -q '"build"' package.json; then + npm run build + echo "- ✅ npm build completed" >> $GITHUB_STEP_SUMMARY + fi + + if [ -f "Makefile" ]; then + make build 2>/dev/null || echo "- ℹ️ Makefile build not available" >> $GITHUB_STEP_SUMMARY + fi + + echo "- ✅ Generic project build completed" >> $GITHUB_STEP_SUMMARY + + - name: Verify build output + working-directory: ${{ inputs.working-directory }} + run: | + echo "### 📦 Build Output" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Check for common build output directories + for dir in dist build public out; do + if [ -d "$dir" ]; then + echo "- ✅ Found build output: \`$dir/\`" >> $GITHUB_STEP_SUMMARY + du -sh "$dir" >> $GITHUB_STEP_SUMMARY + fi + done + + - name: Upload build artifacts + if: inputs.upload-artifacts + uses: actions/upload-artifact@v6 + with: + name: ${{ inputs.artifact-name }}-${{ needs.detect.outputs.project-type }} + path: | + ${{ inputs.working-directory }}/dist/ + ${{ inputs.working-directory }}/build/ + ${{ inputs.working-directory }}/public/ + ${{ inputs.working-directory }}/out/ + retention-days: 7 + if-no-files-found: ignore diff --git a/.github/workflows/reusable-ci-validation.yml b/.github/workflows/reusable-ci-validation.yml new file mode 100644 index 0000000..b5796f0 --- /dev/null +++ b/.github/workflows/reusable-ci-validation.yml @@ -0,0 +1,534 @@ +# Copyright (C) 2026 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# FILE INFORMATION +# DEFGROUP: GitHub.Workflow +# INGROUP: MokoStandards.Reusable +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /.github/workflows/reusable-ci-validation.yml +# VERSION: 01.00.00 +# BRIEF: Reusable CI validation workflow for repository standards enforcement +# NOTE: Supports multiple validation profiles (basic, full, strict) with configurable checks + +name: Reusable CI Validation + +on: + workflow_call: + inputs: + profile: + description: 'Validation profile (basic, full, strict)' + required: false + type: string + default: 'basic' + node-version: + description: 'Node.js version for frontend validation' + required: false + type: string + default: '20.x' + php-version: + description: 'PHP version for backend validation' + required: false + type: string + default: '8.1' + working-directory: + description: 'Working directory for validation' + required: false + type: string + default: '.' + validate-manifests: + description: 'Validate XML manifests (Joomla/Dolibarr)' + required: false + type: boolean + default: true + validate-changelogs: + description: 'Validate CHANGELOG.md format and structure' + required: false + type: boolean + default: true + validate-licenses: + description: 'Validate license headers in source files' + required: false + type: boolean + default: true + validate-security: + description: 'Check for secrets and security issues' + required: false + type: boolean + default: true + fail-on-warnings: + description: 'Fail the workflow on validation warnings' + required: false + type: boolean + default: false + +permissions: + contents: read + pull-requests: write + checks: write + +jobs: + setup: + name: Setup Validation Environment + runs-on: ubuntu-latest + outputs: + has-php: ${{ steps.detect.outputs.has-php }} + has-node: ${{ steps.detect.outputs.has-node }} + has-scripts: ${{ steps.detect.outputs.has-scripts }} + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Detect project components + id: detect + working-directory: ${{ inputs.working-directory }} + run: | + # Detect PHP files + if find . -name "*.php" -type f | head -1 | grep -q .; then + echo "has-php=true" >> $GITHUB_OUTPUT + echo "✅ PHP files detected" >> $GITHUB_STEP_SUMMARY + else + echo "has-php=false" >> $GITHUB_OUTPUT + echo "ℹ️ No PHP files detected" >> $GITHUB_STEP_SUMMARY + fi + + # Detect Node.js project + if [ -f "package.json" ]; then + echo "has-node=true" >> $GITHUB_OUTPUT + echo "✅ Node.js project detected" >> $GITHUB_STEP_SUMMARY + else + echo "has-node=false" >> $GITHUB_OUTPUT + echo "ℹ️ No Node.js project detected" >> $GITHUB_STEP_SUMMARY + fi + + # Detect validation scripts + if [ -d "scripts/validate" ] || [ -d ".github/scripts/validate" ]; then + echo "has-scripts=true" >> $GITHUB_OUTPUT + echo "✅ Validation scripts found" >> $GITHUB_STEP_SUMMARY + else + echo "has-scripts=false" >> $GITHUB_OUTPUT + echo "ℹ️ No validation scripts found" >> $GITHUB_STEP_SUMMARY + fi + + required-validations: + name: Required Validations + runs-on: ubuntu-latest + needs: setup + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Normalize line endings + run: git config --global core.autocrlf false + + - name: Setup PHP + if: needs.setup.outputs.has-php == 'true' + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ inputs.php-version }} + extensions: mbstring, xml + coverage: none + + - name: Setup Node.js + if: needs.setup.outputs.has-node == 'true' + uses: actions/setup-node@v6 + with: + node-version: ${{ inputs.node-version }} + + - name: Make scripts executable + if: needs.setup.outputs.has-scripts == 'true' + working-directory: ${{ inputs.working-directory }} + run: | + if [ -d "scripts" ]; then + find scripts -name "*.sh" -type f -exec chmod +x {} \; + fi + if [ -d ".github/scripts" ]; then + find .github/scripts -name "*.sh" -type f -exec chmod +x {} \; + fi + + - name: Validate XML manifests + if: inputs.validate-manifests + working-directory: ${{ inputs.working-directory }} + run: | + echo "### 📋 Manifest Validation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ -f "scripts/validate/manifest.sh" ]; then + echo "Running manifest validation script..." >> $GITHUB_STEP_SUMMARY + if ./scripts/validate/manifest.sh 2>&1 | tee -a $GITHUB_STEP_SUMMARY; then + echo "✅ Manifest validation passed" >> $GITHUB_STEP_SUMMARY + else + echo "❌ Manifest validation script failed" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + elif [ -f ".github/scripts/validate/manifest.sh" ]; then + echo "Running manifest validation script..." >> $GITHUB_STEP_SUMMARY + if ./.github/scripts/validate/manifest.sh 2>&1 | tee -a $GITHUB_STEP_SUMMARY; then + echo "✅ Manifest validation passed" >> $GITHUB_STEP_SUMMARY + else + echo "❌ Manifest validation script failed" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + elif command -v xmllint >/dev/null 2>&1; then + # Basic XML validation using xmllint + echo "Using xmllint for basic XML validation..." >> $GITHUB_STEP_SUMMARY + XML_FOUND=false + ERROR_FOUND=false + + while IFS= read -r file; do + XML_FOUND=true + if ! xmllint --noout "$file" 2>&1 | tee -a $GITHUB_STEP_SUMMARY; then + echo "❌ Invalid XML: $file" >> $GITHUB_STEP_SUMMARY + ERROR_FOUND=true + fi + done < <(find . -name "*.xml" -type f ! -path "*/node_modules/*" ! -path "*/.git/*") + + if [ "$ERROR_FOUND" = true ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "❌ XML validation failed" >> $GITHUB_STEP_SUMMARY + exit 1 + elif [ "$XML_FOUND" = true ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "✅ Basic XML validation passed" >> $GITHUB_STEP_SUMMARY + else + echo "ℹ️ No XML files found to validate" >> $GITHUB_STEP_SUMMARY + fi + else + echo "ℹ️ No manifest validation script or xmllint available" >> $GITHUB_STEP_SUMMARY + echo "Skipping XML validation" >> $GITHUB_STEP_SUMMARY + fi + + - name: Validate XML well-formedness + working-directory: ${{ inputs.working-directory }} + run: | + if [ -f "scripts/validate/xml_wellformed.sh" ]; then + ./scripts/validate/xml_wellformed.sh + elif [ -f ".github/scripts/validate/xml_wellformed.sh" ]; then + ./.github/scripts/validate/xml_wellformed.sh + else + echo "ℹ️ No XML well-formedness validation script found, skipping" + fi + + - name: Validate PHP syntax + if: needs.setup.outputs.has-php == 'true' + working-directory: ${{ inputs.working-directory }} + run: | + echo "### 🔍 PHP Syntax Validation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ -f "scripts/validate/php_syntax.sh" ]; then + echo "Running PHP syntax validation script..." >> $GITHUB_STEP_SUMMARY + if ./scripts/validate/php_syntax.sh 2>&1 | tee -a $GITHUB_STEP_SUMMARY; then + echo "✅ PHP syntax validation passed" >> $GITHUB_STEP_SUMMARY + else + echo "❌ PHP syntax validation script failed" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + elif [ -f ".github/scripts/validate/php_syntax.sh" ]; then + echo "Running PHP syntax validation script..." >> $GITHUB_STEP_SUMMARY + if ./.github/scripts/validate/php_syntax.sh 2>&1 | tee -a $GITHUB_STEP_SUMMARY; then + echo "✅ PHP syntax validation passed" >> $GITHUB_STEP_SUMMARY + else + echo "❌ PHP syntax validation script failed" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + else + # Basic PHP syntax check + echo "Running basic PHP syntax check..." >> $GITHUB_STEP_SUMMARY + ERROR_FOUND=false + + while IFS= read -r file; do + if ! php -l "$file" 2>&1 | tee -a $GITHUB_STEP_SUMMARY; then + echo "❌ Syntax error in: $file" >> $GITHUB_STEP_SUMMARY + ERROR_FOUND=true + fi + done < <(find . -name "*.php" -type f ! -path "*/vendor/*" ! -path "*/node_modules/*") + + if [ "$ERROR_FOUND" = true ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "❌ PHP syntax errors found" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "✅ PHP syntax validation passed" >> $GITHUB_STEP_SUMMARY + fi + fi + + optional-validations: + name: Optional Validations (${{ inputs.profile }}) + runs-on: ubuntu-latest + needs: setup + if: inputs.profile != 'basic' + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup PHP + if: needs.setup.outputs.has-php == 'true' + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ inputs.php-version }} + extensions: mbstring, xml + coverage: none + + - name: Make scripts executable + if: needs.setup.outputs.has-scripts == 'true' + working-directory: ${{ inputs.working-directory }} + run: | + if [ -d "scripts" ]; then + find scripts -name "*.sh" -type f -exec chmod +x {} \; + fi + if [ -d ".github/scripts" ]; then + find .github/scripts -name "*.sh" -type f -exec chmod +x {} \; + fi + + - name: Validate changelog + if: inputs.validate-changelogs + continue-on-error: ${{ !inputs.fail-on-warnings }} + working-directory: ${{ inputs.working-directory }} + run: | + echo "### 📝 Changelog Validation" >> $GITHUB_STEP_SUMMARY + + if [ -f "scripts/validate/changelog.sh" ]; then + ./scripts/validate/changelog.sh + echo "✅ Changelog validation passed" >> $GITHUB_STEP_SUMMARY + elif [ -f ".github/scripts/validate/changelog.sh" ]; then + ./.github/scripts/validate/changelog.sh + echo "✅ Changelog validation passed" >> $GITHUB_STEP_SUMMARY + elif [ -f "CHANGELOG.md" ]; then + # Basic changelog validation + if grep -q "## \[" CHANGELOG.md; then + echo "✅ Changelog appears well-formed" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Changelog may not follow standard format" >> $GITHUB_STEP_SUMMARY + [ "${{ inputs.fail-on-warnings }}" = "true" ] && exit 1 + fi + else + echo "⚠️ No CHANGELOG.md found" >> $GITHUB_STEP_SUMMARY + fi + + - name: Validate license headers + if: inputs.validate-licenses + continue-on-error: ${{ !inputs.fail-on-warnings }} + working-directory: ${{ inputs.working-directory }} + run: | + echo "### ⚖️ License Header Validation" >> $GITHUB_STEP_SUMMARY + + if [ -f "scripts/validate/license_headers.sh" ]; then + ./scripts/validate/license_headers.sh + echo "✅ License headers validated" >> $GITHUB_STEP_SUMMARY + elif [ -f ".github/scripts/validate/license_headers.sh" ]; then + ./.github/scripts/validate/license_headers.sh + echo "✅ License headers validated" >> $GITHUB_STEP_SUMMARY + else + # Basic license header check + COUNT_FILE=$(mktemp) + find . \( -name "*.php" -o -name "*.js" -o -name "*.py" \) -type f -exec sh -c 'if ! head -20 "$1" | grep -qi "license\|copyright\|spdx"; then echo "1"; fi' _ {} \; > "$COUNT_FILE" + FILES_WITHOUT_LICENSE=$(wc -l < "$COUNT_FILE") + rm -f "$COUNT_FILE" + if [ "$FILES_WITHOUT_LICENSE" -eq 0 ]; then + echo "✅ License headers appear present" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Some files may be missing license headers" >> $GITHUB_STEP_SUMMARY + [ "${{ inputs.fail-on-warnings }}" = "true" ] && exit 1 + fi + fi + + - name: Validate language structure + continue-on-error: true + working-directory: ${{ inputs.working-directory }} + run: | + if [ -f "scripts/validate/language_structure.sh" ]; then + ./scripts/validate/language_structure.sh + elif [ -f ".github/scripts/validate/language_structure.sh" ]; then + ./.github/scripts/validate/language_structure.sh + else + echo "ℹ️ No language structure validation script found" + fi + + - name: Validate paths + continue-on-error: true + working-directory: ${{ inputs.working-directory }} + run: | + if [ -f "scripts/validate/paths.sh" ]; then + ./scripts/validate/paths.sh + elif [ -f ".github/scripts/validate/paths.sh" ]; then + ./.github/scripts/validate/paths.sh + else + echo "ℹ️ No path validation script found" + fi + + - name: Validate tabs/whitespace + continue-on-error: true + working-directory: ${{ inputs.working-directory }} + run: | + if [ -f "scripts/validate/tabs.sh" ]; then + ./scripts/validate/tabs.sh + elif [ -f ".github/scripts/validate/tabs.sh" ]; then + ./.github/scripts/validate/tabs.sh + else + echo "ℹ️ No tabs validation script found" + fi + + - name: Validate version alignment + continue-on-error: ${{ !inputs.fail-on-warnings }} + working-directory: ${{ inputs.working-directory }} + run: | + if [ -f "scripts/validate/version_alignment.sh" ]; then + ./scripts/validate/version_alignment.sh + elif [ -f ".github/scripts/validate/version_alignment.sh" ]; then + ./.github/scripts/validate/version_alignment.sh + else + echo "ℹ️ No version alignment validation script found" + fi + + security-validations: + name: Security Validations + runs-on: ubuntu-latest + if: inputs.validate-security + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Make scripts executable + working-directory: ${{ inputs.working-directory }} + run: | + if [ -d "scripts" ]; then + find scripts -name "*.sh" -type f -exec chmod +x {} \; + fi + if [ -d ".github/scripts" ]; then + find .github/scripts -name "*.sh" -type f -exec chmod +x {} \; + fi + + - name: Check for secrets + working-directory: ${{ inputs.working-directory }} + run: | + echo "### 🔒 Security Validation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ -f "scripts/validate/no_secrets.sh" ]; then + if ./scripts/validate/no_secrets.sh 2>&1 | tee -a $GITHUB_STEP_SUMMARY; then + echo "✅ No secrets found" >> $GITHUB_STEP_SUMMARY + else + echo "❌ Secret validation script failed" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + elif [ -f ".github/scripts/validate/no_secrets.sh" ]; then + if ./.github/scripts/validate/no_secrets.sh 2>&1 | tee -a $GITHUB_STEP_SUMMARY; then + echo "✅ No secrets found" >> $GITHUB_STEP_SUMMARY + else + echo "❌ Secret validation script failed" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + else + # Basic secrets check using find to properly exclude directories + PATTERNS=( + "password\s*=\s*['\"][^'\"]+['\"]" + "api[_-]?key\s*=\s*['\"][^'\"]+['\"]" + "secret\s*=\s*['\"][^'\"]+['\"]" + "token\s*=\s*['\"][^'\"]+['\"]" + "BEGIN RSA PRIVATE KEY" + "BEGIN PRIVATE KEY" + ) + + FOUND=0 + echo "Scanning for potential secrets..." >> $GITHUB_STEP_SUMMARY + + for pattern in "${PATTERNS[@]}"; do + # Use find to exclude directories and files, then grep the results + while IFS= read -r file; do + if [ -f "$file" ]; then + if grep -HnE "$pattern" "$file" 2>/dev/null; then + FOUND=1 + echo "⚠️ Found pattern in: $file" >> $GITHUB_STEP_SUMMARY + fi + fi + done < <(find . -type f \ + ! -path "*/.git/*" \ + ! -path "*/node_modules/*" \ + ! -path "*/vendor/*" \ + ! -path "*/.github/*" \ + ! -path "*/docs/*" \ + ! -name "*.md" \ + 2>/dev/null) + done + + if [ $FOUND -eq 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "✅ Basic security check passed - no secrets detected" >> $GITHUB_STEP_SUMMARY + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "❌ Potential secrets or credentials detected" >> $GITHUB_STEP_SUMMARY + echo "Please review the findings above and ensure they are test fixtures or documentation examples" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + fi + + summary: + name: Validation Summary + runs-on: ubuntu-latest + needs: [required-validations, optional-validations, security-validations] + if: always() + + steps: + - name: Generate validation summary + run: | + echo "### 🎯 CI Validation Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Profile:** ${{ inputs.profile }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Validation Stage | Status |" >> $GITHUB_STEP_SUMMARY + echo "|-----------------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Required Validations | ${{ needs.required-validations.result == 'success' && '✅ Passed' || '❌ Failed' }} |" >> $GITHUB_STEP_SUMMARY + echo "| Optional Validations | ${{ needs.optional-validations.result == 'success' && '✅ Passed' || needs.optional-validations.result == 'skipped' && '⏭️ Skipped' || '❌ Failed' }} |" >> $GITHUB_STEP_SUMMARY + echo "| Security Validations | ${{ needs.security-validations.result == 'success' && '✅ Passed' || needs.security-validations.result == 'skipped' && '⏭️ Skipped' || '❌ Failed' }} |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Repository:** $GITHUB_REPOSITORY" >> $GITHUB_STEP_SUMMARY + echo "**Branch:** $GITHUB_REF_NAME" >> $GITHUB_STEP_SUMMARY + echo "**Commit:** $GITHUB_SHA" >> $GITHUB_STEP_SUMMARY + + - name: Check validation results + run: | + if [ "${{ needs.required-validations.result }}" == "failure" ]; then + echo "❌ Required validations failed" + exit 1 + fi + + if [ "${{ needs.security-validations.result }}" == "failure" ]; then + echo "❌ Security validations failed" + exit 1 + fi + + if [ "${{ inputs.profile }}" == "strict" ] && [ "${{ needs.optional-validations.result }}" == "failure" ]; then + echo "❌ Optional validations failed in strict mode" + exit 1 + fi + + echo "✅ CI validation completed successfully" diff --git a/.github/workflows/reusable-deploy.yml b/.github/workflows/reusable-deploy.yml new file mode 100644 index 0000000..e32e4fc --- /dev/null +++ b/.github/workflows/reusable-deploy.yml @@ -0,0 +1,312 @@ +# Copyright (C) 2026 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# FILE INFORMATION +# DEFGROUP: GitHub.Workflow +# INGROUP: MokoStandards.Reusable +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /.github/workflows/reusable-deploy.yml +# VERSION: 01.00.00 +# BRIEF: Reusable type-aware deployment workflow for staging and production +# NOTE: Supports Joomla, Dolibarr, and generic deployments with health checks + +name: Reusable Deploy + +on: + workflow_call: + inputs: + environment: + description: 'Target environment (staging, production)' + required: true + type: string + version: + description: 'Version to deploy (optional, uses latest if not specified)' + required: false + type: string + deployment-method: + description: 'Deployment method (rsync, ftp, ssh, kubernetes, custom)' + required: false + type: string + default: 'custom' + health-check-url: + description: 'URL to check after deployment' + required: false + type: string + health-check-timeout: + description: 'Health check timeout in seconds' + required: false + type: number + default: 300 + working-directory: + description: 'Working directory' + required: false + type: string + default: '.' + secrets: + DEPLOY_HOST: + description: 'Deployment host/server' + required: false + DEPLOY_USER: + description: 'Deployment user' + required: false + DEPLOY_KEY: + description: 'SSH private key or deployment credentials' + required: false + DEPLOY_PATH: + description: 'Deployment path on target server' + required: false + +permissions: + contents: read + deployments: write + +jobs: + detect: + name: Detect Project Type + uses: ./.github/workflows/reusable-project-detector.yml + with: + working-directory: ${{ inputs.working-directory }} + + prepare: + name: Prepare Deployment + runs-on: ubuntu-latest + needs: detect + outputs: + deployment-id: ${{ steps.create-deployment.outputs.deployment_id }} + version: ${{ steps.version.outputs.version }} + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Determine version + id: version + run: | + if [ -n "${{ inputs.version }}" ]; then + VERSION="${{ inputs.version }}" + else + # Use latest tag or commit SHA + VERSION=$(git describe --tags --always) + fi + echo "version=${VERSION}" >> $GITHUB_OUTPUT + echo "Deploying version: ${VERSION}" + + - name: Create deployment + id: create-deployment + uses: chrnorm/deployment-action@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + environment: ${{ inputs.environment }} + description: "Deploy ${{ needs.detect.outputs.project-type }} v${{ steps.version.outputs.version }}" + + - name: Deployment info + run: | + echo "### 🚀 Deployment Preparation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Environment:** ${{ inputs.environment }}" >> $GITHUB_STEP_SUMMARY + echo "**Version:** ${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY + echo "**Project Type:** ${{ needs.detect.outputs.project-type }}" >> $GITHUB_STEP_SUMMARY + echo "**Method:** ${{ inputs.deployment-method }}" >> $GITHUB_STEP_SUMMARY + + build: + name: Build for Deployment + needs: [detect, prepare] + uses: ./.github/workflows/reusable-build.yml + with: + working-directory: ${{ inputs.working-directory }} + upload-artifacts: true + artifact-name: deployment-package + + deploy: + name: Deploy to ${{ inputs.environment }} + runs-on: ubuntu-latest + needs: [detect, prepare, build] + environment: + name: ${{ inputs.environment }} + url: ${{ inputs.health-check-url }} + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Download build artifacts + uses: actions/download-artifact@v7 + with: + name: deployment-package-${{ needs.detect.outputs.project-type }} + path: ./dist + + - name: Setup SSH key + if: inputs.deployment-method == 'ssh' || inputs.deployment-method == 'rsync' + run: | + mkdir -p ~/.ssh + echo "${{ secrets.DEPLOY_KEY }}" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + ssh-keyscan -H "${{ secrets.DEPLOY_HOST }}" >> ~/.ssh/known_hosts + + - name: Deploy via rsync + if: inputs.deployment-method == 'rsync' + run: | + echo "Deploying via rsync to ${{ secrets.DEPLOY_HOST }}..." + + rsync -avz --delete \ + -e "ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=no" \ + ./dist/ \ + "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:${{ secrets.DEPLOY_PATH }}" + + echo "✅ rsync deployment completed" >> $GITHUB_STEP_SUMMARY + + - name: Deploy via SSH + if: inputs.deployment-method == 'ssh' + run: | + echo "Deploying via SSH to ${{ secrets.DEPLOY_HOST }}..." + + # Create deployment package + tar -czf deployment.tar.gz -C ./dist . + + # Copy to server + scp -i ~/.ssh/deploy_key deployment.tar.gz \ + "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:/tmp/" + + # Extract on server + ssh -i ~/.ssh/deploy_key "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" << 'EOF' + cd ${{ secrets.DEPLOY_PATH }} + tar -xzf /tmp/deployment.tar.gz + rm /tmp/deployment.tar.gz + EOF + + echo "✅ SSH deployment completed" >> $GITHUB_STEP_SUMMARY + + - name: Deploy Joomla Extension + if: needs.detect.outputs.project-type == 'joomla' && inputs.deployment-method == 'custom' + run: | + echo "### 🔧 Joomla Extension Deployment" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Custom Joomla deployment logic + echo "⚠️ Custom Joomla deployment logic required" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Typical steps:" >> $GITHUB_STEP_SUMMARY + echo "1. Upload extension package to Joomla server" >> $GITHUB_STEP_SUMMARY + echo "2. Install/update via Joomla Extension Manager API" >> $GITHUB_STEP_SUMMARY + echo "3. Clear Joomla cache" >> $GITHUB_STEP_SUMMARY + echo "4. Run database migrations if needed" >> $GITHUB_STEP_SUMMARY + + # Placeholder for actual deployment commands + echo "Add your Joomla-specific deployment commands here" + + - name: Deploy Dolibarr Module + if: needs.detect.outputs.project-type == 'dolibarr' && inputs.deployment-method == 'custom' + run: | + echo "### 🔧 Dolibarr Module Deployment" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Custom Dolibarr deployment logic + echo "⚠️ Custom Dolibarr deployment logic required" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Typical steps:" >> $GITHUB_STEP_SUMMARY + echo "1. Upload module to Dolibarr htdocs/custom directory" >> $GITHUB_STEP_SUMMARY + echo "2. Activate module via Dolibarr API or admin panel" >> $GITHUB_STEP_SUMMARY + echo "3. Run module setup hooks" >> $GITHUB_STEP_SUMMARY + echo "4. Clear Dolibarr cache" >> $GITHUB_STEP_SUMMARY + + # Placeholder for actual deployment commands + echo "Add your Dolibarr-specific deployment commands here" + + - name: Deploy Generic Application + if: needs.detect.outputs.project-type == 'generic' && inputs.deployment-method == 'custom' + run: | + echo "### 🔧 Generic Application Deployment" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + echo "⚠️ Custom deployment logic required" >> $GITHUB_STEP_SUMMARY + echo "Add your application-specific deployment commands" >> $GITHUB_STEP_SUMMARY + + - name: Health check + if: inputs.health-check-url != '' + run: | + echo "Running health check on ${{ inputs.health-check-url }}..." + + TIMEOUT=${{ inputs.health-check-timeout }} + ELAPSED=0 + INTERVAL=10 + + while [ $ELAPSED -lt $TIMEOUT ]; do + if curl -f -s -o /dev/null -w "%{http_code}" "${{ inputs.health-check-url }}" | grep -q "200"; then + echo "✅ Health check passed" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + echo "Health check attempt $((ELAPSED / INTERVAL + 1)) failed, retrying..." + sleep $INTERVAL + ELAPSED=$((ELAPSED + INTERVAL)) + done + + echo "❌ Health check failed after ${TIMEOUT}s" >> $GITHUB_STEP_SUMMARY + exit 1 + + - name: Update deployment status (success) + if: success() + uses: chrnorm/deployment-status@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + deployment-id: ${{ needs.prepare.outputs.deployment-id }} + state: success + environment-url: ${{ inputs.health-check-url }} + + - name: Deployment summary + if: success() + run: | + echo "### ✅ Deployment Successful" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Environment:** ${{ inputs.environment }}" >> $GITHUB_STEP_SUMMARY + echo "**Version:** ${{ needs.prepare.outputs.version }}" >> $GITHUB_STEP_SUMMARY + echo "**Project Type:** ${{ needs.detect.outputs.project-type }}" >> $GITHUB_STEP_SUMMARY + echo "**Time:** $(date -u +"%Y-%m-%d %H:%M:%S UTC")" >> $GITHUB_STEP_SUMMARY + if [ -n "${{ inputs.health-check-url }}" ]; then + echo "**URL:** ${{ inputs.health-check-url }}" >> $GITHUB_STEP_SUMMARY + fi + + rollback: + name: Rollback on Failure + runs-on: ubuntu-latest + needs: [prepare, deploy] + if: failure() + + steps: + - name: Update deployment status (failure) + uses: chrnorm/deployment-status@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + deployment-id: ${{ needs.prepare.outputs.deployment-id }} + state: failure + + - name: Rollback deployment + run: | + echo "### ❌ Deployment Failed - Initiating Rollback" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "⚠️ Rollback logic needs to be implemented" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Typical rollback steps:" >> $GITHUB_STEP_SUMMARY + echo "1. Restore previous version from backup" >> $GITHUB_STEP_SUMMARY + echo "2. Revert database migrations if applied" >> $GITHUB_STEP_SUMMARY + echo "3. Clear caches" >> $GITHUB_STEP_SUMMARY + echo "4. Verify health checks pass" >> $GITHUB_STEP_SUMMARY + + # Add your rollback commands here diff --git a/.github/workflows/reusable-joomla-testing.yml b/.github/workflows/reusable-joomla-testing.yml new file mode 100644 index 0000000..b0129ff --- /dev/null +++ b/.github/workflows/reusable-joomla-testing.yml @@ -0,0 +1,356 @@ +# Copyright (C) 2026 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# FILE INFORMATION +# DEFGROUP: GitHub.Workflow +# INGROUP: MokoStandards.Reusable +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /.github/workflows/reusable-joomla-testing.yml +# VERSION: 01.00.00 +# BRIEF: Reusable Joomla testing workflow with matrix PHP/Joomla versions +# NOTE: Supports PHPUnit, integration tests, and code coverage + +name: Reusable Joomla Testing + +on: + workflow_call: + inputs: + php-versions: + description: 'JSON array of PHP versions to test' + required: false + type: string + default: '["7.4", "8.0", "8.1", "8.2"]' + joomla-versions: + description: 'JSON array of Joomla versions to test' + required: false + type: string + default: '["4.4", "5.0", "5.1"]' + coverage: + description: 'Enable code coverage reporting' + required: false + type: boolean + default: false + coverage-php-version: + description: 'PHP version to use for coverage reporting' + required: false + type: string + default: '8.1' + coverage-joomla-version: + description: 'Joomla version to use for coverage reporting' + required: false + type: string + default: '5.0' + working-directory: + description: 'Working directory for tests' + required: false + type: string + default: '.' + run-integration-tests: + description: 'Run integration tests with Joomla installation' + required: false + type: boolean + default: true + secrets: + CODECOV_TOKEN: + description: 'Codecov token for coverage uploads' + required: false + +permissions: + contents: read + pull-requests: write + checks: write + +jobs: + unit-tests: + name: PHPUnit (PHP ${{ matrix.php-version }}, Joomla ${{ matrix.joomla-version }}) + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + php-version: ${{ fromJSON(inputs.php-versions) }} + joomla-version: ${{ fromJSON(inputs.joomla-versions) }} + exclude: + # PHP 7.4 not compatible with Joomla 5.x + - php-version: '7.4' + joomla-version: '5.0' + - php-version: '7.4' + joomla-version: '5.1' + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup PHP ${{ matrix.php-version }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: mbstring, xml, mysqli, zip, gd, intl + coverage: ${{ inputs.coverage && matrix.php-version == inputs.coverage-php-version && matrix.joomla-version == inputs.coverage-joomla-version && 'xdebug' || 'none' }} + tools: composer:v2 + + - name: Get Composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache Composer dependencies + uses: actions/cache@v5 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-php-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php-${{ matrix.php-version }}-composer- + ${{ runner.os }}-php- + + - name: Validate composer.json + working-directory: ${{ inputs.working-directory }} + run: | + if [ -f "composer.json" ]; then + composer validate --strict + else + echo "No composer.json found, skipping validation" + fi + + - name: Install dependencies + working-directory: ${{ inputs.working-directory }} + run: | + if [ -f "composer.json" ]; then + composer install --prefer-dist --no-progress --no-interaction + else + echo "No composer.json found, skipping dependency installation" + fi + + - name: Setup Joomla test environment + working-directory: ${{ inputs.working-directory }} + run: | + echo "Setting up Joomla ${{ matrix.joomla-version }} test environment" + # Add Joomla-specific environment variables + echo "JOOMLA_VERSION=${{ matrix.joomla-version }}" >> $GITHUB_ENV + + - name: Run PHPUnit tests + working-directory: ${{ inputs.working-directory }} + run: | + if [ -f "vendor/bin/phpunit" ]; then + if [ "${{ inputs.coverage && matrix.php-version == inputs.coverage-php-version && matrix.joomla-version == inputs.coverage-joomla-version }}" == "true" ]; then + vendor/bin/phpunit --coverage-text --coverage-clover=coverage.xml + else + vendor/bin/phpunit + fi + elif [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then + if [ "${{ inputs.coverage && matrix.php-version == inputs.coverage-php-version && matrix.joomla-version == inputs.coverage-joomla-version }}" == "true" ]; then + php vendor/phpunit/phpunit/phpunit --coverage-text --coverage-clover=coverage.xml + else + php vendor/phpunit/phpunit/phpunit + fi + else + echo "⚠️ No PHPUnit configuration found, skipping tests" + exit 0 + fi + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + if: inputs.coverage && matrix.php-version == inputs.coverage-php-version && matrix.joomla-version == inputs.coverage-joomla-version + with: + file: ${{ inputs.working-directory }}/coverage.xml + flags: unittests,php-${{ matrix.php-version }},joomla-${{ matrix.joomla-version }} + name: codecov-joomla-${{ matrix.php-version }}-${{ matrix.joomla-version }} + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false + + integration-tests: + name: Integration (Joomla ${{ matrix.joomla-version }}) + runs-on: ubuntu-latest + if: inputs.run-integration-tests + + services: + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: joomla_test + MYSQL_USER: joomla + MYSQL_PASSWORD: joomla + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping --silent" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + + strategy: + fail-fast: false + matrix: + joomla-version: ${{ fromJSON(inputs.joomla-versions) }} + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.1' + extensions: mbstring, xml, mysqli, zip, gd, intl, pdo_mysql + tools: composer:v2 + + - name: Get Composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache Composer dependencies + uses: actions/cache@v5 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-integration-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-integration-composer- + + - name: Install dependencies + working-directory: ${{ inputs.working-directory }} + run: | + if [ -f "composer.json" ]; then + composer install --prefer-dist --no-progress --no-interaction + fi + + - name: Download and setup Joomla ${{ matrix.joomla-version }} + run: | + echo "📦 Setting up Joomla ${{ matrix.joomla-version }} for integration testing" + + # Create Joomla directory + mkdir -p /tmp/joomla + cd /tmp/joomla + + # Determine Joomla version to download + JOOMLA_VERSION="${{ matrix.joomla-version }}" + + # Download latest patch version for the specified minor version + if [[ "$JOOMLA_VERSION" == "4.4" ]]; then + DOWNLOAD_VERSION="4.4-Stable" + elif [[ "$JOOMLA_VERSION" == "5.0" ]]; then + DOWNLOAD_VERSION="5.0-Stable" + elif [[ "$JOOMLA_VERSION" == "5.1" ]]; then + DOWNLOAD_VERSION="5.1-Stable" + else + DOWNLOAD_VERSION="${JOOMLA_VERSION}-Stable" + fi + + echo "Downloading Joomla ${DOWNLOAD_VERSION}..." + curl -L -o joomla.zip "https://downloads.joomla.org/cms/joomla${JOOMLA_VERSION%%.*}/${DOWNLOAD_VERSION}" || \ + curl -L -o joomla.zip "https://github.com/joomla/joomla-cms/releases/download/${JOOMLA_VERSION}.0/Joomla_${JOOMLA_VERSION}.0-Stable-Full_Package.zip" || \ + echo "⚠️ Could not download Joomla, integration tests may be limited" + + if [ -f joomla.zip ]; then + unzip -q joomla.zip + echo "✅ Joomla extracted successfully" + fi + + - name: Configure Joomla + run: | + echo "⚙️ Configuring Joomla for testing" + + if [ -d "/tmp/joomla" ]; then + cd /tmp/joomla + + # Create basic Joomla configuration + cat > configuration.php << 'EOF' + > $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Test Suite | Status |" >> $GITHUB_STEP_SUMMARY + echo "|------------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Unit Tests | ${{ needs.unit-tests.result == 'success' && '✅ Passed' || needs.unit-tests.result == 'skipped' && '⏭️ Skipped' || '❌ Failed' }} |" >> $GITHUB_STEP_SUMMARY + echo "| Integration Tests | ${{ needs.integration-tests.result == 'success' && '✅ Passed' || needs.integration-tests.result == 'skipped' && '⏭️ Skipped' || '❌ Failed' }} |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Repository:** $GITHUB_REPOSITORY" >> $GITHUB_STEP_SUMMARY + echo "**Branch:** $GITHUB_REF_NAME" >> $GITHUB_STEP_SUMMARY + echo "**Commit:** $GITHUB_SHA" >> $GITHUB_STEP_SUMMARY + + - name: Check test results + run: | + if [ "${{ needs.unit-tests.result }}" == "failure" ]; then + echo "❌ Unit tests failed" + exit 1 + fi + + if [ "${{ needs.integration-tests.result }}" == "failure" ]; then + echo "❌ Integration tests failed" + exit 1 + fi + + echo "✅ All test suites passed or were skipped" diff --git a/.github/workflows/reusable-php-quality.yml b/.github/workflows/reusable-php-quality.yml new file mode 100644 index 0000000..c484f21 --- /dev/null +++ b/.github/workflows/reusable-php-quality.yml @@ -0,0 +1,297 @@ +# Copyright (C) 2026 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# FILE INFORMATION +# DEFGROUP: GitHub.Workflow +# INGROUP: MokoStandards.Reusable +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /.github/workflows/reusable-php-quality.yml +# VERSION: 01.00.00 +# BRIEF: Reusable PHP code quality analysis workflow +# NOTE: Supports PHPCS, PHPStan, Psalm with configurable PHP versions and tools + +name: Reusable PHP Quality + +on: + workflow_call: + inputs: + php-versions: + description: 'JSON array of PHP versions to test' + required: false + type: string + default: '["7.4", "8.0", "8.1", "8.2"]' + tools: + description: 'JSON array of quality tools to run (phpcs, phpstan, psalm)' + required: false + type: string + default: '["phpcs", "phpstan", "psalm"]' + working-directory: + description: 'Working directory for the quality checks' + required: false + type: string + default: '.' + phpcs-standard: + description: 'PHPCS coding standard to use' + required: false + type: string + default: 'PSR12' + phpstan-level: + description: 'PHPStan analysis level (0-9)' + required: false + type: string + default: '5' + psalm-level: + description: 'Psalm error level (1-8)' + required: false + type: string + default: '4' + fail-on-error: + description: 'Fail the workflow if quality checks find issues' + required: false + type: boolean + default: true + outputs: + quality-score: + description: 'Overall quality score percentage' + value: ${{ jobs.aggregate.outputs.score }} + +permissions: + contents: read + pull-requests: write + checks: write + +jobs: + phpcs: + name: PHP_CodeSniffer (PHP ${{ matrix.php-version }}) + runs-on: ubuntu-latest + if: contains(fromJSON(inputs.tools), 'phpcs') + + strategy: + fail-fast: false + matrix: + php-version: ${{ fromJSON(inputs.php-versions) }} + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup PHP ${{ matrix.php-version }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: mbstring, xml + tools: composer:v2, phpcs + coverage: none + + - name: Get Composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache Composer dependencies + uses: actions/cache@v5 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install dependencies + working-directory: ${{ inputs.working-directory }} + run: | + if [ -f "composer.json" ]; then + composer install --prefer-dist --no-progress --no-interaction + fi + + - name: Run PHP_CodeSniffer + working-directory: ${{ inputs.working-directory }} + continue-on-error: ${{ !inputs.fail-on-error }} + run: | + if [ -f "phpcs.xml" ] || [ -f "phpcs.xml.dist" ]; then + phpcs --standard=phpcs.xml --report=summary --report-width=120 + elif [ -f "vendor/bin/phpcs" ]; then + vendor/bin/phpcs --standard=${{ inputs.phpcs-standard }} --report=summary --report-width=120 src/ + else + phpcs --standard=${{ inputs.phpcs-standard }} --report=summary --report-width=120 . || echo "No PHP files found or PHPCS configuration missing" + fi + + phpstan: + name: PHPStan (PHP ${{ matrix.php-version }}) + runs-on: ubuntu-latest + if: contains(fromJSON(inputs.tools), 'phpstan') + + strategy: + fail-fast: false + matrix: + php-version: ${{ fromJSON(inputs.php-versions) }} + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup PHP ${{ matrix.php-version }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: mbstring, xml + tools: composer:v2, phpstan + coverage: none + + - name: Get Composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache Composer dependencies + uses: actions/cache@v5 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install dependencies + working-directory: ${{ inputs.working-directory }} + run: | + if [ -f "composer.json" ]; then + composer install --prefer-dist --no-progress --no-interaction + fi + + - name: Run PHPStan + working-directory: ${{ inputs.working-directory }} + continue-on-error: ${{ !inputs.fail-on-error }} + run: | + if [ -f "phpstan.neon" ] || [ -f "phpstan.neon.dist" ]; then + phpstan analyse --no-progress --error-format=github + elif [ -f "vendor/bin/phpstan" ]; then + vendor/bin/phpstan analyse src/ --level=${{ inputs.phpstan-level }} --no-progress --error-format=github + else + phpstan analyse . --level=${{ inputs.phpstan-level }} --no-progress --error-format=github || echo "No PHP files found or PHPStan configuration missing" + fi + + psalm: + name: Psalm (PHP ${{ matrix.php-version }}) + runs-on: ubuntu-latest + if: contains(fromJSON(inputs.tools), 'psalm') + + strategy: + fail-fast: false + matrix: + php-version: ${{ fromJSON(inputs.php-versions) }} + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup PHP ${{ matrix.php-version }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: mbstring, xml + tools: composer:v2, psalm + coverage: none + + - name: Get Composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache Composer dependencies + uses: actions/cache@v5 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install dependencies + working-directory: ${{ inputs.working-directory }} + run: | + if [ -f "composer.json" ]; then + composer install --prefer-dist --no-progress --no-interaction + fi + + - name: Run Psalm + working-directory: ${{ inputs.working-directory }} + continue-on-error: ${{ !inputs.fail-on-error }} + run: | + if [ -f "psalm.xml" ] || [ -f "psalm.xml.dist" ]; then + psalm --no-progress --output-format=github --show-info=false + elif [ -f "vendor/bin/psalm" ]; then + # Initialize Psalm config if it doesn't exist + if [ ! -f "psalm.xml" ]; then + echo "Initializing Psalm configuration..." + if ! vendor/bin/psalm --init src/ ${{ inputs.psalm-level }}; then + echo "⚠️ Psalm initialization failed, proceeding with defaults" + fi + fi + vendor/bin/psalm --no-progress --output-format=github --show-info=false + else + psalm --no-progress --output-format=github --show-info=false || echo "No PHP files found or Psalm configuration missing" + fi + + aggregate: + name: Quality Check Summary + runs-on: ubuntu-latest + needs: [phpcs, phpstan, psalm] + if: always() + outputs: + score: ${{ steps.calculate.outputs.score }} + + steps: + - name: Calculate quality score + id: calculate + run: | + # Count successful jobs + SUCCESS=0 + TOTAL=0 + + if [ "${{ needs.phpcs.result }}" != "skipped" ]; then + TOTAL=$((TOTAL + 1)) + [ "${{ needs.phpcs.result }}" == "success" ] && SUCCESS=$((SUCCESS + 1)) + fi + + if [ "${{ needs.phpstan.result }}" != "skipped" ]; then + TOTAL=$((TOTAL + 1)) + [ "${{ needs.phpstan.result }}" == "success" ] && SUCCESS=$((SUCCESS + 1)) + fi + + if [ "${{ needs.psalm.result }}" != "skipped" ]; then + TOTAL=$((TOTAL + 1)) + [ "${{ needs.psalm.result }}" == "success" ] && SUCCESS=$((SUCCESS + 1)) + fi + + # Calculate percentage + if [ $TOTAL -gt 0 ]; then + SCORE=$((SUCCESS * 100 / TOTAL)) + else + SCORE=100 + fi + + echo "score=$SCORE" >> $GITHUB_OUTPUT + echo "Quality Score: $SCORE%" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- PHPCS: ${{ needs.phpcs.result }}" >> $GITHUB_STEP_SUMMARY + echo "- PHPStan: ${{ needs.phpstan.result }}" >> $GITHUB_STEP_SUMMARY + echo "- Psalm: ${{ needs.psalm.result }}" >> $GITHUB_STEP_SUMMARY + + - name: Check overall status + if: inputs.fail-on-error + run: | + if [ "${{ needs.phpcs.result }}" == "failure" ] || \ + [ "${{ needs.phpstan.result }}" == "failure" ] || \ + [ "${{ needs.psalm.result }}" == "failure" ]; then + echo "❌ Quality checks failed" + exit 1 + fi + echo "✅ All quality checks passed" diff --git a/.github/workflows/reusable-project-detector.yml b/.github/workflows/reusable-project-detector.yml new file mode 100644 index 0000000..4d96129 --- /dev/null +++ b/.github/workflows/reusable-project-detector.yml @@ -0,0 +1,138 @@ +# Copyright (C) 2026 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# FILE INFORMATION +# DEFGROUP: GitHub.Workflow +# INGROUP: MokoStandards.Reusable +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /.github/workflows/reusable-project-detector.yml +# VERSION: 01.00.00 +# BRIEF: Reusable workflow for detecting project type (Joomla, Dolibarr, Generic) +# NOTE: Provides project_type and extension_type outputs for downstream workflows + +name: Reusable Project Type Detection + +on: + workflow_call: + inputs: + working-directory: + description: 'Working directory for detection' + required: false + type: string + default: '.' + outputs: + project-type: + description: 'Detected project type (joomla, dolibarr, generic)' + value: ${{ jobs.detect.outputs.project_type }} + extension-type: + description: 'Detected extension type (component, module, plugin, etc.)' + value: ${{ jobs.detect.outputs.extension_type }} + has-php: + description: 'Whether project contains PHP files' + value: ${{ jobs.detect.outputs.has_php }} + has-node: + description: 'Whether project contains Node.js/package.json' + value: ${{ jobs.detect.outputs.has_node }} + +permissions: + contents: read + +jobs: + detect: + name: Detect Project Type + runs-on: ubuntu-latest + outputs: + project_type: ${{ steps.detect.outputs.project_type }} + extension_type: ${{ steps.detect.outputs.extension_type }} + has_php: ${{ steps.detect.outputs.has_php }} + has_node: ${{ steps.detect.outputs.has_node }} + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Detect project type and components + id: detect + working-directory: ${{ inputs.working-directory }} + run: | + echo "### 🔍 Project Detection" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Detection priority: Joomla > Dolibarr > Generic + + # Check for Joomla indicators + if [ -f "joomla.xml" ] || \ + find . -maxdepth 2 \( -name "mod_*.xml" -o -name "plg_*.xml" -o -name "com_*.xml" -o -name "pkg_*.xml" -o -name "tpl_*.xml" \) 2>/dev/null | head -1 | grep -q .; then + echo "project_type=joomla" >> $GITHUB_OUTPUT + echo "**Project Type:** Joomla" >> $GITHUB_STEP_SUMMARY + + # Detect Joomla extension type + if [ -d "administrator/components" ] || [ -d "components" ]; then + echo "extension_type=component" >> $GITHUB_OUTPUT + echo "**Extension Type:** Component" >> $GITHUB_STEP_SUMMARY + elif find . -maxdepth 1 -name "mod_*.xml" 2>/dev/null | head -1 | grep -q .; then + echo "extension_type=module" >> $GITHUB_OUTPUT + echo "**Extension Type:** Module" >> $GITHUB_STEP_SUMMARY + elif find . -maxdepth 1 -name "plg_*.xml" 2>/dev/null | head -1 | grep -q .; then + echo "extension_type=plugin" >> $GITHUB_OUTPUT + echo "**Extension Type:** Plugin" >> $GITHUB_STEP_SUMMARY + elif find . -maxdepth 1 -name "pkg_*.xml" 2>/dev/null | head -1 | grep -q .; then + echo "extension_type=package" >> $GITHUB_OUTPUT + echo "**Extension Type:** Package" >> $GITHUB_STEP_SUMMARY + elif find . -maxdepth 1 -name "tpl_*.xml" 2>/dev/null | head -1 | grep -q .; then + echo "extension_type=template" >> $GITHUB_OUTPUT + echo "**Extension Type:** Template" >> $GITHUB_STEP_SUMMARY + else + echo "extension_type=component" >> $GITHUB_OUTPUT + echo "**Extension Type:** Component (default)" >> $GITHUB_STEP_SUMMARY + fi + + # Check for Dolibarr indicators + elif [ -d "htdocs" ] || [ -d "core/modules" ] || \ + ([ -f "composer.json" ] && grep -q "dolibarr" composer.json 2>/dev/null); then + echo "project_type=dolibarr" >> $GITHUB_OUTPUT + echo "extension_type=module" >> $GITHUB_OUTPUT + echo "**Project Type:** Dolibarr" >> $GITHUB_STEP_SUMMARY + echo "**Extension Type:** Module" >> $GITHUB_STEP_SUMMARY + + # Default to Generic + else + echo "project_type=generic" >> $GITHUB_OUTPUT + echo "extension_type=application" >> $GITHUB_OUTPUT + echo "**Project Type:** Generic" >> $GITHUB_STEP_SUMMARY + echo "**Extension Type:** Application" >> $GITHUB_STEP_SUMMARY + fi + + # Detect PHP presence + if find . -name "*.php" -type f 2>/dev/null | head -1 | grep -q .; then + echo "has_php=true" >> $GITHUB_OUTPUT + echo "- ✅ PHP files detected" >> $GITHUB_STEP_SUMMARY + else + echo "has_php=false" >> $GITHUB_OUTPUT + echo "- ℹ️ No PHP files detected" >> $GITHUB_STEP_SUMMARY + fi + + # Detect Node.js presence + if [ -f "package.json" ]; then + echo "has_node=true" >> $GITHUB_OUTPUT + echo "- ✅ Node.js project detected (package.json)" >> $GITHUB_STEP_SUMMARY + else + echo "has_node=false" >> $GITHUB_OUTPUT + echo "- ℹ️ No Node.js project detected" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.github/workflows/reusable-release.yml b/.github/workflows/reusable-release.yml new file mode 100644 index 0000000..0595e97 --- /dev/null +++ b/.github/workflows/reusable-release.yml @@ -0,0 +1,397 @@ +# Copyright (C) 2026 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# FILE INFORMATION +# DEFGROUP: GitHub.Workflow +# INGROUP: MokoStandards.Reusable +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /.github/workflows/reusable-release.yml +# VERSION: 01.00.00 +# BRIEF: Reusable type-aware release workflow for Joomla, Dolibarr, and generic projects +# NOTE: Creates releases with type-specific packaging and optional marketplace publishing + +name: Reusable Release + +on: + workflow_call: + inputs: + version: + description: 'Release version (semver format)' + required: true + type: string + prerelease: + description: 'Mark as pre-release' + required: false + type: boolean + default: false + draft: + description: 'Create as draft release' + required: false + type: boolean + default: false + php-version: + description: 'PHP version for build' + required: false + type: string + default: '8.1' + create-github-release: + description: 'Create GitHub release' + required: false + type: boolean + default: true + publish-to-marketplace: + description: 'Publish to marketplace (Joomla/Dolibarr)' + required: false + type: boolean + default: false + working-directory: + description: 'Working directory' + required: false + type: string + default: '.' + secrets: + MARKETPLACE_TOKEN: + description: 'Marketplace API token (JED/Dolistore)' + required: false + +permissions: + contents: write + +jobs: + detect: + name: Detect Project Type + uses: ./.github/workflows/reusable-project-detector.yml + with: + working-directory: ${{ inputs.working-directory }} + + build-package: + name: Build Release Package + runs-on: ubuntu-latest + needs: detect + outputs: + package-name: ${{ steps.package.outputs.name }} + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup PHP + if: needs.detect.outputs.has-php == 'true' + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ inputs.php-version }} + extensions: mbstring, xml, zip + tools: composer:v2 + + - name: Setup Node.js + if: needs.detect.outputs.has-node == 'true' + uses: actions/setup-node@v6 + with: + node-version: '20.x' + + - name: Validate version format + run: | + VERSION="${{ inputs.version }}" + if ! echo "$VERSION" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$'; then + echo "❌ Invalid version format: $VERSION" + echo "Expected semver format: X.Y.Z or X.Y.Z-prerelease" + exit 1 + fi + echo "✅ Version format valid: $VERSION" + + - name: Install dependencies + working-directory: ${{ inputs.working-directory }} + run: | + if [ -f "composer.json" ]; then + composer install --no-dev --optimize-autoloader --no-interaction + echo "✅ Composer dependencies installed" >> $GITHUB_STEP_SUMMARY + fi + + if [ -f "package.json" ]; then + npm ci + if grep -q '"build"' package.json; then + npm run build + fi + echo "✅ Node dependencies installed and built" >> $GITHUB_STEP_SUMMARY + fi + + - name: Update version in files + working-directory: ${{ inputs.working-directory }} + run: | + VERSION="${{ inputs.version }}" + + # Update version in XML manifests (Joomla/Dolibarr) + if [ "${{ needs.detect.outputs.project-type }}" == "joomla" ] || \ + [ "${{ needs.detect.outputs.project-type }}" == "dolibarr" ]; then + find . -name "*.xml" -type f -not -path "*/node_modules/*" -not -path "*/vendor/*" \ + -exec sed -i "s/[^<]*<\/version>/${VERSION}<\/version>/g" {} \; + echo "- ✅ Updated version in XML manifests" >> $GITHUB_STEP_SUMMARY + fi + + # Update version in package.json + if [ -f "package.json" ]; then + sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"${VERSION}\"/g" package.json + echo "- ✅ Updated version in package.json" >> $GITHUB_STEP_SUMMARY + fi + + # Update version in composer.json + if [ -f "composer.json" ]; then + sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"${VERSION}\"/g" composer.json + echo "- ✅ Updated version in composer.json" >> $GITHUB_STEP_SUMMARY + fi + + - name: Create Joomla package + if: needs.detect.outputs.project-type == 'joomla' + working-directory: ${{ inputs.working-directory }} + run: | + mkdir -p build/package + + # Copy files excluding development artifacts + rsync -av \ + --exclude='build' \ + --exclude='tests' \ + --exclude='.git*' \ + --exclude='composer.json' \ + --exclude='composer.lock' \ + --exclude='phpunit.xml*' \ + --exclude='phpcs.xml*' \ + --exclude='phpstan.neon*' \ + --exclude='psalm.xml*' \ + --exclude='node_modules' \ + --exclude='.github' \ + --exclude='package.json' \ + --exclude='package-lock.json' \ + . build/package/ + + # Determine extension name from manifest + MANIFEST=$(find . -maxdepth 1 -name "*.xml" -not -name "phpunit.xml*" -type f | head -1) + if [ -n "$MANIFEST" ]; then + EXT_NAME=$(basename "$MANIFEST" .xml) + else + EXT_NAME=$(basename "$GITHUB_REPOSITORY" | sed 's/^joomla-//') + fi + + # Create ZIP package + cd build/package + VERSION="${{ inputs.version }}" + PACKAGE_NAME="${EXT_NAME}-${VERSION}.zip" + zip -r "../${PACKAGE_NAME}" . + cd ../.. + + echo "PACKAGE_NAME=${PACKAGE_NAME}" >> $GITHUB_ENV + echo "✅ Created Joomla package: ${PACKAGE_NAME}" >> $GITHUB_STEP_SUMMARY + + - name: Create Dolibarr package + if: needs.detect.outputs.project-type == 'dolibarr' + working-directory: ${{ inputs.working-directory }} + run: | + mkdir -p build/package + + # Copy module files + rsync -av \ + --exclude='build' \ + --exclude='tests' \ + --exclude='.git*' \ + --exclude='node_modules' \ + --exclude='.github' \ + . build/package/ + + # Determine module name + if [ -f "core/modules/modMyModule.class.php" ]; then + MODULE_NAME=$(grep -oP "class modMyModule extends DolibarrModules" core/modules/*.php | head -1 | sed 's/class mod//' | sed 's/ extends.*//') + else + MODULE_NAME=$(basename "$GITHUB_REPOSITORY" | sed 's/^dolibarr-//') + fi + + # Create ZIP package + cd build/package + VERSION="${{ inputs.version }}" + PACKAGE_NAME="${MODULE_NAME}-${VERSION}.zip" + zip -r "../${PACKAGE_NAME}" . + cd ../.. + + echo "PACKAGE_NAME=${PACKAGE_NAME}" >> $GITHUB_ENV + echo "✅ Created Dolibarr package: ${PACKAGE_NAME}" >> $GITHUB_STEP_SUMMARY + + - name: Create Generic package + if: needs.detect.outputs.project-type == 'generic' + working-directory: ${{ inputs.working-directory }} + run: | + mkdir -p build/package + + # Copy relevant build artifacts + if [ -d "dist" ]; then + cp -r dist/* build/package/ + elif [ -d "build" ]; then + cp -r build/* build/package/ + else + # Copy all files excluding development artifacts + rsync -av \ + --exclude='build' \ + --exclude='tests' \ + --exclude='.git*' \ + --exclude='node_modules' \ + . build/package/ + fi + + # Create package + REPO_NAME=$(basename "$GITHUB_REPOSITORY") + VERSION="${{ inputs.version }}" + PACKAGE_NAME="${REPO_NAME}-${VERSION}.tar.gz" + + cd build + tar -czf "${PACKAGE_NAME}" package/ + cd .. + + echo "PACKAGE_NAME=${PACKAGE_NAME}" >> $GITHUB_ENV + echo "✅ Created generic package: ${PACKAGE_NAME}" >> $GITHUB_STEP_SUMMARY + + - name: Generate checksums + working-directory: ${{ inputs.working-directory }} + run: | + cd build + PACKAGE="${PACKAGE_NAME}" + + if [ -f "$PACKAGE" ]; then + sha256sum "$PACKAGE" > "${PACKAGE}.sha256" + md5sum "$PACKAGE" > "${PACKAGE}.md5" + echo "✅ Generated checksums" >> $GITHUB_STEP_SUMMARY + fi + + - name: Output package info + id: package + run: | + echo "name=${PACKAGE_NAME}" >> $GITHUB_OUTPUT + + - name: Upload release artifacts + uses: actions/upload-artifact@v6 + with: + name: release-package + path: | + ${{ inputs.working-directory }}/build/*.zip + ${{ inputs.working-directory }}/build/*.tar.gz + ${{ inputs.working-directory }}/build/*.sha256 + ${{ inputs.working-directory }}/build/*.md5 + retention-days: 30 + + create-release: + name: Create GitHub Release + runs-on: ubuntu-latest + needs: [detect, build-package] + if: inputs.create-github-release + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Download release artifacts + uses: actions/download-artifact@v7 + with: + name: release-package + path: ./artifacts + + - name: Extract changelog + id: changelog + run: | + VERSION="${{ inputs.version }}" + + if [ -f "CHANGELOG.md" ]; then + # Extract changelog for this version + awk "/## \[${VERSION}\]/,/## \[/{if(/## \[${VERSION}\]/)next;else if(/## \[/)exit;else print}" CHANGELOG.md > release_notes.md + + if [ ! -s release_notes.md ]; then + echo "## Release ${VERSION}" > release_notes.md + echo "" >> release_notes.md + echo "No specific changelog found for this version." >> release_notes.md + echo "Please refer to the full CHANGELOG.md for details." >> release_notes.md + fi + else + echo "## Release ${VERSION}" > release_notes.md + echo "" >> release_notes.md + echo "Release created from ${{ needs.detect.outputs.project-type }} project." >> release_notes.md + fi + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ inputs.version }} + name: Release ${{ inputs.version }} + body_path: release_notes.md + draft: ${{ inputs.draft }} + prerelease: ${{ inputs.prerelease }} + files: | + artifacts/* + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Release summary + run: | + echo "### 🚀 Release Created Successfully" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Version:** ${{ inputs.version }}" >> $GITHUB_STEP_SUMMARY + echo "**Project Type:** ${{ needs.detect.outputs.project-type }}" >> $GITHUB_STEP_SUMMARY + echo "**Extension Type:** ${{ needs.detect.outputs.extension-type }}" >> $GITHUB_STEP_SUMMARY + echo "**Pre-release:** ${{ inputs.prerelease }}" >> $GITHUB_STEP_SUMMARY + echo "**Draft:** ${{ inputs.draft }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Package:** ${{ needs.build-package.outputs.package-name }}" >> $GITHUB_STEP_SUMMARY + + publish-marketplace: + name: Publish to Marketplace + runs-on: ubuntu-latest + needs: [detect, build-package, create-release] + if: inputs.publish-to-marketplace && (needs.detect.outputs.project-type == 'joomla' || needs.detect.outputs.project-type == 'dolibarr') + + steps: + - name: Download release artifacts + uses: actions/download-artifact@v7 + with: + name: release-package + path: ./artifacts + + - name: Publish to marketplace + run: | + echo "### 🌐 Marketplace Publishing" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + PROJECT_TYPE="${{ needs.detect.outputs.project-type }}" + + if [ "$PROJECT_TYPE" == "joomla" ]; then + echo "⚠️ Joomla Extensions Directory (JED) publishing requires manual submission" >> $GITHUB_STEP_SUMMARY + echo "Package ready at: artifacts/${{ needs.build-package.outputs.package-name }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "To publish to JED:" >> $GITHUB_STEP_SUMMARY + echo "1. Visit https://extensions.joomla.org/" >> $GITHUB_STEP_SUMMARY + echo "2. Login and submit the extension package" >> $GITHUB_STEP_SUMMARY + elif [ "$PROJECT_TYPE" == "dolibarr" ]; then + echo "⚠️ Dolistore publishing requires manual submission" >> $GITHUB_STEP_SUMMARY + echo "Package ready at: artifacts/${{ needs.build-package.outputs.package-name }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "To publish to Dolistore:" >> $GITHUB_STEP_SUMMARY + echo "1. Visit https://www.dolistore.com/" >> $GITHUB_STEP_SUMMARY + echo "2. Login and submit the module package" >> $GITHUB_STEP_SUMMARY + fi + + # Note: Automated marketplace publishing would require + # marketplace-specific API implementation here + # For now, we provide manual instructions diff --git a/.github/workflows/reusable-script-executor.yml b/.github/workflows/reusable-script-executor.yml new file mode 100644 index 0000000..897e9e5 --- /dev/null +++ b/.github/workflows/reusable-script-executor.yml @@ -0,0 +1,210 @@ +# Copyright (C) 2026 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# FILE INFORMATION +# DEFGROUP: GitHub.Workflows +# INGROUP: MokoStandards.Reusable +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /.github/workflows/reusable-script-executor.yml +# VERSION: 01.00.00 +# BRIEF: Reusable workflow to execute MokoStandards scripts in any repository +# NOTE: Provides unified script execution with proper environment setup + +name: Execute MokoStandards Script + +on: + workflow_call: + inputs: + script_path: + description: 'Path to script relative to scripts/ directory (e.g., validate/no_secrets.py)' + required: true + type: string + script_args: + description: 'Arguments to pass to the script' + required: false + type: string + default: '' + python_version: + description: 'Python version to use' + required: false + type: string + default: '3.11' + install_dependencies: + description: 'Install Python dependencies (pyyaml, etc.)' + required: false + type: boolean + default: true + working_directory: + description: 'Working directory for script execution' + required: false + type: string + default: '.' + create_summary: + description: 'Create GitHub step summary' + required: false + type: boolean + default: true + outputs: + exit_code: + description: 'Script exit code' + value: ${{ jobs.execute-script.outputs.exit_code }} + script_output: + description: 'Script output (truncated to 1000 chars)' + value: ${{ jobs.execute-script.outputs.script_output }} + +jobs: + execute-script: + name: Execute ${{ inputs.script_path }} + runs-on: ubuntu-latest + + outputs: + exit_code: ${{ steps.run-script.outputs.exit_code }} + script_output: ${{ steps.run-script.outputs.script_output }} + + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Python + if: endsWith(inputs.script_path, '.py') + uses: actions/setup-python@v6 + with: + python-version: ${{ inputs.python_version }} + + - name: Install Python dependencies + if: endsWith(inputs.script_path, '.py') && inputs.install_dependencies + run: | + python -m pip install --upgrade pip + pip install pyyaml + + # Install additional dependencies if requirements file exists + if [ -f "requirements.txt" ]; then + pip install -r requirements.txt + fi + + if [ "${{ inputs.create_summary }}" == "true" ]; then + echo "## 📦 Dependencies Installed" >> $GITHUB_STEP_SUMMARY + echo "- Python ${{ inputs.python_version }}" >> $GITHUB_STEP_SUMMARY + echo "- PyYAML (for configuration)" >> $GITHUB_STEP_SUMMARY + fi + + - name: Setup Bash + if: endsWith(inputs.script_path, '.sh') + run: | + bash --version + + - name: Verify script exists + id: verify + run: | + SCRIPT_PATH="scripts/${{ inputs.script_path }}" + + if [ ! -f "$SCRIPT_PATH" ]; then + echo "❌ Script not found: $SCRIPT_PATH" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Available scripts:" >> $GITHUB_STEP_SUMMARY + find scripts -name "*.py" -o -name "*.sh" | sort >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + echo "script_full_path=$SCRIPT_PATH" >> $GITHUB_OUTPUT + + if [ "${{ inputs.create_summary }}" == "true" ]; then + echo "## ✅ Script Found" >> $GITHUB_STEP_SUMMARY + echo "**Path:** \`$SCRIPT_PATH\`" >> $GITHUB_STEP_SUMMARY + echo "**Type:** $(file -b $SCRIPT_PATH)" >> $GITHUB_STEP_SUMMARY + fi + + - name: Make script executable + run: | + chmod +x ${{ steps.verify.outputs.script_full_path }} + + - name: Run script + id: run-script + working-directory: ${{ inputs.working_directory }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + SCRIPT_PATH="${{ steps.verify.outputs.script_full_path }}" + SCRIPT_ARGS="${{ inputs.script_args }}" + + echo "## 🚀 Executing Script" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Script:** \`$SCRIPT_PATH\`" >> $GITHUB_STEP_SUMMARY + echo "**Arguments:** \`$SCRIPT_ARGS\`" >> $GITHUB_STEP_SUMMARY + echo "**Working Directory:** \`${{ inputs.working_directory }}\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Output" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + + # Execute script and capture output + set +e + + if [[ "$SCRIPT_PATH" == *.py ]]; then + OUTPUT=$(python3 "$SCRIPT_PATH" $SCRIPT_ARGS 2>&1) + EXIT_CODE=$? + elif [[ "$SCRIPT_PATH" == *.sh ]]; then + OUTPUT=$(bash "$SCRIPT_PATH" $SCRIPT_ARGS 2>&1) + EXIT_CODE=$? + else + OUTPUT=$("$SCRIPT_PATH" $SCRIPT_ARGS 2>&1) + EXIT_CODE=$? + fi + + set -e + + # Save outputs + echo "exit_code=$EXIT_CODE" >> $GITHUB_OUTPUT + + # Truncate output for GitHub output (max 1000 chars) + OUTPUT_TRUNCATED="${OUTPUT:0:1000}" + echo "script_output<> $GITHUB_OUTPUT + echo "$OUTPUT_TRUNCATED" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + # Show full output in summary (with line limit) + echo "$OUTPUT" | head -n 100 >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Report exit code + if [ $EXIT_CODE -eq 0 ]; then + echo "### ✅ Script Completed Successfully" >> $GITHUB_STEP_SUMMARY + echo "**Exit Code:** $EXIT_CODE" >> $GITHUB_STEP_SUMMARY + else + echo "### ❌ Script Failed" >> $GITHUB_STEP_SUMMARY + echo "**Exit Code:** $EXIT_CODE" >> $GITHUB_STEP_SUMMARY + fi + + exit $EXIT_CODE + + - name: Upload script output + if: always() + uses: actions/upload-artifact@v6 + with: + name: script-output-${{ github.run_id }} + path: | + *.log + *.json + *.csv + retention-days: 7 + if-no-files-found: ignore diff --git a/.github/workflows/standards-compliance.yml b/.github/workflows/standards-compliance.yml index 16e2260..c18928a 100644 --- a/.github/workflows/standards-compliance.yml +++ b/.github/workflows/standards-compliance.yml @@ -19,411 +19,56 @@ # # FILE INFORMATION # DEFGROUP: GitHub.Workflow -# INGROUP: Moko-Cassiopeia.Compliance -# REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia +# INGROUP: MokoStandards.Compliance +# REPO: https://github.com/mokoconsulting-tech/MokoStandards # PATH: /.github/workflows/standards-compliance.yml # VERSION: 01.00.00 -# BRIEF: MokoStandards compliance validation workflow -# NOTE: Validates repository structure, documentation, and coding standards +# BRIEF: Standards compliance validation workflow +# NOTE: Runs manually, monthly, and on release builds name: Standards Compliance on: - workflow_dispatch: - pull_request: - branches: - - main - - dev/** - - rc/** - - version/** + # Run monthly on the 1st at 00:00 UTC schedule: - # Run monthly on the 1st at 00:00 UTC - cron: '0 0 1 * *' - workflow_run: - workflows: - - "Create version branch and bump versions" - - "Release Pipeline (dev > rc > version > main)" - types: - - completed + + # Run on release creation + release: + types: [published, created] + + # Allow manual triggering with options + workflow_dispatch: + inputs: + profile: + description: 'Validation profile' + required: false + type: choice + options: + - 'basic' + - 'full' + - 'strict' + default: 'full' + fail-on-warnings: + description: 'Fail workflow on warnings' + required: false + type: boolean + default: false permissions: contents: read pull-requests: write + checks: write jobs: - repository-structure: - name: Repository Structure Validation - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@v6 - - - name: Check Required Directories - run: | - echo "### Required Directories" >> $GITHUB_STEP_SUMMARY - - MISSING=0 - - # Check required directories - for dir in docs tests scripts .github; do - if [ -d "$dir" ]; then - echo "✅ $dir/" >> $GITHUB_STEP_SUMMARY - else - echo "❌ $dir/ (missing)" >> $GITHUB_STEP_SUMMARY - MISSING=$((MISSING + 1)) - fi - done - - if [ $MISSING -gt 0 ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "⚠️ $MISSING required directories are missing" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - - name: Check Required Files - run: | - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Required Files" >> $GITHUB_STEP_SUMMARY - - MISSING=0 - - # Check required files - for file in README.md LICENSE CONTRIBUTING.md SECURITY.md CHANGELOG.md .editorconfig; do - if [ -f "$file" ]; then - echo "✅ $file" >> $GITHUB_STEP_SUMMARY - else - echo "❌ $file (missing)" >> $GITHUB_STEP_SUMMARY - MISSING=$((MISSING + 1)) - fi - done - - if [ $MISSING -gt 0 ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "⚠️ $MISSING required files are missing" >> $GITHUB_STEP_SUMMARY - echo "See: https://github.com/mokoconsulting-tech/MokoStandards/tree/main/templates/docs/required" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - documentation-quality: - name: Documentation Quality Check - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@v6 - - - name: Validate README.md - run: | - echo "### README.md Validation" >> $GITHUB_STEP_SUMMARY - - if [ ! -f "README.md" ]; then - echo "❌ README.md not found" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - # Check minimum length - SIZE=$(wc -c < README.md) - if [ $SIZE -lt 500 ]; then - echo "⚠️ README.md is too short ($SIZE bytes, minimum 500)" >> $GITHUB_STEP_SUMMARY - else - echo "✅ README.md has adequate content ($SIZE bytes)" >> $GITHUB_STEP_SUMMARY - fi - - # Check for key sections - MISSING_SECTIONS="" - grep -qi "# \|## " README.md || MISSING_SECTIONS="${MISSING_SECTIONS}- No headings found\n" - - if [ -n "$MISSING_SECTIONS" ]; then - echo "⚠️ README.md may be missing important sections" >> $GITHUB_STEP_SUMMARY - else - echo "✅ README.md appears well-structured" >> $GITHUB_STEP_SUMMARY - fi - - - name: Validate CHANGELOG.md - run: | - echo "" >> $GITHUB_STEP_SUMMARY - echo "### CHANGELOG.md Validation" >> $GITHUB_STEP_SUMMARY - - if [ ! -f "CHANGELOG.md" ]; then - echo "❌ CHANGELOG.md not found" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - # Check for Keep a Changelog format markers - if grep -qi "## \[.*\]" CHANGELOG.md; then - echo "✅ CHANGELOG.md follows Keep a Changelog format" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ CHANGELOG.md may not follow Keep a Changelog format" >> $GITHUB_STEP_SUMMARY - echo "See: https://keepachangelog.com/" >> $GITHUB_STEP_SUMMARY - fi - - - name: Check Documentation Index - run: | - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Documentation Index" >> $GITHUB_STEP_SUMMARY - - if [ -f "docs/index.md" ] || [ -f "docs/README.md" ]; then - echo "✅ Documentation index found" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ No documentation index (docs/index.md or docs/README.md)" >> $GITHUB_STEP_SUMMARY - fi - - coding-standards: - name: Coding Standards Check - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@v6 - - - name: Check for Tab Characters - run: | - echo "### Tab Character Detection" >> $GITHUB_STEP_SUMMARY - - # Find files with tabs (excluding certain file types) - TABS_FOUND=$(find . -type f \ - ! -path "./vendor/*" \ - ! -path "./node_modules/*" \ - ! -path "./.git/*" \ - ! -name "Makefile*" \ - ! -name "*.tsv" \ - -exec grep -l $'\t' {} \; 2>/dev/null | head -10) - - if [ -n "$TABS_FOUND" ]; then - echo "⚠️ Tab characters found in files:" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "$TABS_FOUND" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "MokoStandards requires spaces over tabs (except in Makefiles)" >> $GITHUB_STEP_SUMMARY - else - echo "✅ No inappropriate tab characters found" >> $GITHUB_STEP_SUMMARY - fi - - - name: Check File Encoding - run: | - echo "" >> $GITHUB_STEP_SUMMARY - echo "### File Encoding Check" >> $GITHUB_STEP_SUMMARY - - # Check for UTF-8 encoding - NON_UTF8=$(find . -type f \( -name "*.php" -o -name "*.js" -o -name "*.md" \) \ - ! -path "./vendor/*" \ - ! -path "./node_modules/*" \ - ! -path "./.git/*" \ - -exec file {} \; | grep -v "UTF-8" | head -5) - - if [ -n "$NON_UTF8" ]; then - echo "⚠️ Non-UTF-8 files detected:" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "$NON_UTF8" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - else - echo "✅ All source files appear to be UTF-8 encoded" >> $GITHUB_STEP_SUMMARY - fi - - - name: Check Line Endings - run: | - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Line Ending Check" >> $GITHUB_STEP_SUMMARY - - # Check for CRLF line endings - CRLF_FILES=$(find . -type f \( -name "*.php" -o -name "*.js" -o -name "*.md" \) \ - ! -path "./vendor/*" \ - ! -path "./node_modules/*" \ - ! -path "./.git/*" \ - -exec file {} \; | grep "CRLF" | head -5) - - if [ -n "$CRLF_FILES" ]; then - echo "⚠️ Files with CRLF line endings found:" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "$CRLF_FILES" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "MokoStandards requires LF line endings" >> $GITHUB_STEP_SUMMARY - else - echo "✅ Line endings are consistent (LF)" >> $GITHUB_STEP_SUMMARY - fi - - license-compliance: - name: License Header Validation - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@v6 - - - name: Check SPDX Headers - run: | - echo "### SPDX License Header Check" >> $GITHUB_STEP_SUMMARY - - # Count source files with and without SPDX headers - TOTAL_PHP=0 - WITH_SPDX_PHP=0 - - if find . -name "*.php" -type f ! -path "./vendor/*" | head -1 | grep -q .; then - TOTAL_PHP=$(find . -name "*.php" -type f ! -path "./vendor/*" | wc -l) - WITH_SPDX_PHP=$(find . -name "*.php" -type f ! -path "./vendor/*" -exec grep -l "SPDX-License-Identifier" {} \; | wc -l) - fi - - if [ $TOTAL_PHP -gt 0 ]; then - PERCENT=$((WITH_SPDX_PHP * 100 / TOTAL_PHP)) - echo "- PHP files: $WITH_SPDX_PHP/$TOTAL_PHP ($PERCENT%) with SPDX headers" >> $GITHUB_STEP_SUMMARY - - if [ $PERCENT -lt 80 ]; then - echo "⚠️ Less than 80% of PHP files have SPDX headers" >> $GITHUB_STEP_SUMMARY - else - echo "✅ Good SPDX header coverage" >> $GITHUB_STEP_SUMMARY - fi - fi - - - name: Validate License File - run: | - echo "" >> $GITHUB_STEP_SUMMARY - echo "### License File Validation" >> $GITHUB_STEP_SUMMARY - - if [ ! -f "LICENSE" ]; then - echo "❌ LICENSE file not found" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - # Check license type - if grep -qi "GNU GENERAL PUBLIC LICENSE" LICENSE; then - VERSION=$(grep -i "Version 3" LICENSE || echo "") - if [ -n "$VERSION" ]; then - echo "✅ GPL-3.0-or-later license detected" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ GPL license detected but version unclear" >> $GITHUB_STEP_SUMMARY - fi - elif grep -qi "MIT License" LICENSE; then - echo "✅ MIT license detected" >> $GITHUB_STEP_SUMMARY - elif grep -qi "Apache License" LICENSE; then - echo "✅ Apache license detected" >> $GITHUB_STEP_SUMMARY - else - echo "ℹ️ License type could not be automatically detected" >> $GITHUB_STEP_SUMMARY - fi - - git-hygiene: - name: Git Repository Hygiene - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Check .gitignore - run: | - echo "### .gitignore Validation" >> $GITHUB_STEP_SUMMARY - - if [ ! -f ".gitignore" ]; then - echo "⚠️ .gitignore file not found" >> $GITHUB_STEP_SUMMARY - exit 0 - fi - - # Check for common exclusions - MISSING="" - grep -q "vendor/" .gitignore || MISSING="${MISSING}vendor/ " - grep -q "node_modules/" .gitignore || MISSING="${MISSING}node_modules/ " - - if [ -n "$MISSING" ]; then - echo "⚠️ .gitignore may be missing common exclusions: $MISSING" >> $GITHUB_STEP_SUMMARY - else - echo "✅ .gitignore appears complete" >> $GITHUB_STEP_SUMMARY - fi - - - name: Check for Large Files - run: | - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Large File Detection" >> $GITHUB_STEP_SUMMARY - - # Find files larger than 1MB - LARGE_FILES=$(find . -type f -size +1M ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" | head -5) - - if [ -n "$LARGE_FILES" ]; then - echo "⚠️ Large files detected (>1MB):" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "$LARGE_FILES" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "Consider using Git LFS for large binary files" >> $GITHUB_STEP_SUMMARY - else - echo "✅ No unusually large files detected" >> $GITHUB_STEP_SUMMARY - fi - - workflow-validation: - name: Workflow Configuration Check - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@v6 - - - name: Check Required Workflows - run: | - echo "### GitHub Actions Workflows" >> $GITHUB_STEP_SUMMARY - - WORKFLOWS_DIR=".github/workflows" - - if [ ! -d "$WORKFLOWS_DIR" ]; then - echo "❌ No workflows directory found" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - # Check for recommended workflows - if [ -f "$WORKFLOWS_DIR/ci.yml" ] || [ -f "$WORKFLOWS_DIR/build.yml" ]; then - echo "✅ CI workflow present" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ No CI workflow found (ci.yml or build.yml)" >> $GITHUB_STEP_SUMMARY - fi - - if [ -f "$WORKFLOWS_DIR/codeql-analysis.yml" ]; then - echo "✅ CodeQL security scanning present" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ CodeQL workflow not found" >> $GITHUB_STEP_SUMMARY - fi - - - name: Validate Workflow Syntax - run: | - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Workflow YAML Syntax" >> $GITHUB_STEP_SUMMARY - - INVALID=0 - shopt -s nullglob - for workflow in .github/workflows/*.yml .github/workflows/*.yaml; do - if [ -f "$workflow" ]; then - if python3 -c "import yaml; yaml.safe_load(open('$workflow'))" 2>/dev/null; then - echo "✅ $(basename $workflow)" >> $GITHUB_STEP_SUMMARY - else - echo "❌ $(basename $workflow) - invalid YAML" >> $GITHUB_STEP_SUMMARY - INVALID=$((INVALID + 1)) - fi - fi - done - - if [ $INVALID -gt 0 ]; then - exit 1 - fi - - summary: - name: Compliance Summary - runs-on: ubuntu-latest - needs: [repository-structure, documentation-quality, coding-standards, license-compliance, git-hygiene, workflow-validation] - if: always() - - steps: - - name: Generate Compliance Report - run: | - echo "# MokoStandards Compliance Report" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "All compliance checks have been executed." >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "## Validation Areas:" >> $GITHUB_STEP_SUMMARY - echo "- Repository Structure" >> $GITHUB_STEP_SUMMARY - echo "- Documentation Quality" >> $GITHUB_STEP_SUMMARY - echo "- Coding Standards" >> $GITHUB_STEP_SUMMARY - echo "- License Compliance" >> $GITHUB_STEP_SUMMARY - echo "- Git Repository Hygiene" >> $GITHUB_STEP_SUMMARY - echo "- Workflow Configuration" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "For detailed results, review individual job outputs above." >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "📚 Learn more: https://github.com/mokoconsulting-tech/MokoStandards" >> $GITHUB_STEP_SUMMARY + compliance: + name: Standards Compliance Validation + uses: ./.github/workflows/reusable-ci-validation.yml + with: + profile: ${{ inputs.profile || 'full' }} + validate-manifests: true + validate-changelogs: true + validate-licenses: true + validate-security: true + fail-on-warnings: ${{ inputs.fail-on-warnings || false }} + secrets: inherit diff --git a/.github/workflows/version_branch.yml b/.github/workflows/version_branch.yml index dd45cc2..86a2067 100644 --- a/.github/workflows/version_branch.yml +++ b/.github/workflows/version_branch.yml @@ -1,6 +1,21 @@ -name: Create version branch and bump versions +name: Version Branch and Bump on: + # Run monthly on the 1st at 03:00 UTC for automated version planning + schedule: + - cron: '0 3 1 * *' + + # Run on pull requests to version branches for validation + pull_request: + branches: + - version/** + - rc/** + + # Run on release creation for version tracking + release: + types: [published, created] + + # Allow manual triggering with full options workflow_dispatch: inputs: new_version: