From d22e05765f05ae6af6439dd9eafaedbc4cd29932 Mon Sep 17 00:00:00 2001 From: jmiller Date: Wed, 22 Apr 2026 09:05:57 +0000 Subject: [PATCH 01/16] chore: sync template with latest standards - Add .gitea/workflows mirroring .github/workflows - Add release and infrastructure standards to CLAUDE.md - Add standards to copilot-instructions.md Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitea/workflows/auto-assign.yml | 76 + .gitea/workflows/auto-dev-issue.yml | 207 ++ .gitea/workflows/auto-release.yml | 337 +++ .gitea/workflows/changelog-validation.yml | 101 + .gitea/workflows/codeql-analysis.yml | 115 + .gitea/workflows/copilot-agent.yml | 44 + .gitea/workflows/deploy-demo.yml | 734 +++++ .gitea/workflows/deploy-dev.yml | 700 +++++ .../workflows/enterprise-firewall-setup.yml | 758 +++++ .gitea/workflows/repository-cleanup.yml | 525 ++++ .gitea/workflows/standards-compliance.yml | 2614 +++++++++++++++++ .gitea/workflows/sync-version-on-merge.yml | 133 + .github/CLAUDE.md | 10 + .github/copilot-instructions.md | 7 + 14 files changed, 6361 insertions(+) create mode 100644 .gitea/workflows/auto-assign.yml create mode 100644 .gitea/workflows/auto-dev-issue.yml create mode 100644 .gitea/workflows/auto-release.yml create mode 100644 .gitea/workflows/changelog-validation.yml create mode 100644 .gitea/workflows/codeql-analysis.yml create mode 100644 .gitea/workflows/copilot-agent.yml create mode 100644 .gitea/workflows/deploy-demo.yml create mode 100644 .gitea/workflows/deploy-dev.yml create mode 100644 .gitea/workflows/enterprise-firewall-setup.yml create mode 100644 .gitea/workflows/repository-cleanup.yml create mode 100644 .gitea/workflows/standards-compliance.yml create mode 100644 .gitea/workflows/sync-version-on-merge.yml diff --git a/.gitea/workflows/auto-assign.yml b/.gitea/workflows/auto-assign.yml new file mode 100644 index 0000000..d0b70f6 --- /dev/null +++ b/.gitea/workflows/auto-assign.yml @@ -0,0 +1,76 @@ +# Copyright (C) 2026 Moko Consulting +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: GitHub.Workflow +# INGROUP: MokoStandards.Workflows.Shared +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /.github/workflows/auto-assign.yml +# VERSION: 04.06.00 +# BRIEF: Auto-assign jmiller-moko to unassigned issues and PRs every 15 minutes + +name: Auto-Assign Issues & PRs + +on: + issues: + types: [opened] + pull_request_target: + types: [opened] + schedule: + - cron: '0 */12 * * *' + workflow_dispatch: + +permissions: + issues: write + pull-requests: write + +jobs: + auto-assign: + name: Assign unassigned issues and PRs + runs-on: ubuntu-latest + + steps: + - name: Assign unassigned issues + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + REPO="${{ github.repository }}" + ASSIGNEE="jmiller-moko" + + echo "## ๐Ÿท๏ธ Auto-Assign Report" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + ASSIGNED_ISSUES=0 + ASSIGNED_PRS=0 + + # Assign unassigned open issues + ISSUES=$(gh api "repos/$REPO/issues?state=open&per_page=100&assignee=none" --jq '.[].number' 2>/dev/null || true) + for NUM in $ISSUES; do + # Skip PRs (the issues endpoint returns PRs too) + IS_PR=$(gh api "repos/$REPO/issues/$NUM" --jq '.pull_request // empty' 2>/dev/null || true) + if [ -z "$IS_PR" ]; then + gh api "repos/$REPO/issues/$NUM/assignees" -X POST -f "assignees[]=$ASSIGNEE" --silent 2>/dev/null && { + ASSIGNED_ISSUES=$((ASSIGNED_ISSUES + 1)) + echo " Assigned issue #$NUM" + } || true + fi + done + + # Assign unassigned open PRs + PRS=$(gh api "repos/$REPO/pulls?state=open&per_page=100" --jq '.[] | select(.assignees | length == 0) | .number' 2>/dev/null || true) + for NUM in $PRS; do + gh api "repos/$REPO/issues/$NUM/assignees" -X POST -f "assignees[]=$ASSIGNEE" --silent 2>/dev/null && { + ASSIGNED_PRS=$((ASSIGNED_PRS + 1)) + echo " Assigned PR #$NUM" + } || true + done + + echo "| Type | Assigned |" >> $GITHUB_STEP_SUMMARY + echo "|------|----------|" >> $GITHUB_STEP_SUMMARY + echo "| Issues | $ASSIGNED_ISSUES |" >> $GITHUB_STEP_SUMMARY + echo "| Pull Requests | $ASSIGNED_PRS |" >> $GITHUB_STEP_SUMMARY + + if [ "$ASSIGNED_ISSUES" -eq 0 ] && [ "$ASSIGNED_PRS" -eq 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "โœ… All issues and PRs already have assignees" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.gitea/workflows/auto-dev-issue.yml b/.gitea/workflows/auto-dev-issue.yml new file mode 100644 index 0000000..9b5fbe2 --- /dev/null +++ b/.gitea/workflows/auto-dev-issue.yml @@ -0,0 +1,207 @@ +# Copyright (C) 2026 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: GitHub.Workflow +# INGROUP: MokoStandards.Automation +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /templates/workflows/shared/auto-dev-issue.yml.template +# VERSION: 04.06.00 +# BRIEF: Auto-create tracking issue with sub-issues for dev/rc branch workflow +# NOTE: Synced via bulk-repo-sync to .github/workflows/auto-dev-issue.yml in all governed repos. + +name: Dev/RC Branch Issue + +on: + # Auto-create on RC branch creation + create: + # Manual trigger for dev branches + workflow_dispatch: + inputs: + branch: + description: 'Branch name (e.g., dev/my-feature or dev/04.06)' + required: true + type: string + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +permissions: + contents: read + issues: write + +jobs: + create-issue: + name: Create version tracking issue + runs-on: ubuntu-latest + if: >- + (github.event_name == 'workflow_dispatch') || + (github.event.ref_type == 'branch' && + (startsWith(github.event.ref, 'rc/') || + startsWith(github.event.ref, 'alpha/') || + startsWith(github.event.ref, 'beta/'))) + + steps: + - name: Create tracking issue and sub-issues + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + # For manual dispatch, use input; for auto, use event ref + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + BRANCH="${{ inputs.branch }}" + else + BRANCH="${{ github.event.ref }}" + fi + REPO="${{ github.repository }}" + ACTOR="${{ github.actor }}" + NOW=$(date -u '+%Y-%m-%d %H:%M UTC') + + # Determine branch type and version + if [[ "$BRANCH" == rc/* ]]; then + VERSION="${BRANCH#rc/}" + BRANCH_TYPE="Release Candidate" + LABEL_TYPE="type: release" + TITLE_PREFIX="rc" + elif [[ "$BRANCH" == beta/* ]]; then + VERSION="${BRANCH#beta/}" + BRANCH_TYPE="Beta" + LABEL_TYPE="type: release" + TITLE_PREFIX="beta" + elif [[ "$BRANCH" == alpha/* ]]; then + VERSION="${BRANCH#alpha/}" + BRANCH_TYPE="Alpha" + LABEL_TYPE="type: release" + TITLE_PREFIX="alpha" + else + VERSION="${BRANCH#dev/}" + BRANCH_TYPE="Development" + LABEL_TYPE="type: feature" + TITLE_PREFIX="feat" + fi + + TITLE="${TITLE_PREFIX}(${VERSION}): ${BRANCH_TYPE} tracking for ${BRANCH}" + + # Check for existing issue with same title prefix + EXISTING=$(gh api "repos/${REPO}/issues?state=open&per_page=10" \ + --jq ".[] | select(.title | startswith(\"${TITLE_PREFIX}(${VERSION})\")) | .number" 2>/dev/null | head -1) + + if [ -n "$EXISTING" ]; then + echo "โ„น๏ธ Issue #${EXISTING} already exists for ${VERSION}" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + # โ”€โ”€ Define sub-issues for the workflow โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + if [[ "$BRANCH" == rc/* ]]; then + SUB_ISSUES=( + "RC Testing|Verify all features work on rc branch|type: test,release-candidate" + "Regression Testing|Run full regression suite before merge|type: test,release-candidate" + "Version Bump|Bump version in README.md and all headers|type: version,release-candidate" + "Changelog Update|Update CHANGELOG.md with release notes|documentation,release-candidate" + "Merge to Version Branch|Create PR to version/XX|type: release,needs-review" + ) + elif [[ "$BRANCH" == alpha/* ]] || [[ "$BRANCH" == beta/* ]]; then + SUB_ISSUES=( + "Testing|Verify features on ${BRANCH_TYPE} branch|type: test,status: in-progress" + "Bug Fixes|Fix issues found during ${BRANCH_TYPE} testing|type: bug,status: pending" + "Promote to Next Stage|Create PR to promote to next release stage|type: release,needs-review" + ) + else + SUB_ISSUES=( + "Development|Implement feature/fix on dev branch|type: feature,status: in-progress" + "Unit Testing|Write and pass unit tests|type: test,status: pending" + "Code Review|Request and complete code review|needs-review,status: pending" + "Version Bump|Bump version in README.md and all headers|type: version,status: pending" + "Changelog Update|Update CHANGELOG.md with release notes|documentation,status: pending" + "Create RC Branch|Promote dev to rc branch for final testing|type: release,status: pending" + "Merge to Main|Create PR from rc/dev to main|type: release,needs-review,status: pending" + ) + fi + + # โ”€โ”€ Create sub-issues first โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + SUB_LIST="" + SUB_NUMBERS="" + for SUB in "${SUB_ISSUES[@]}"; do + IFS='|' read -r SUB_TITLE SUB_DESC SUB_LABELS <<< "$SUB" + SUB_FULL_TITLE="${TITLE_PREFIX}(${VERSION}): ${SUB_TITLE}" + + SUB_BODY=$(printf '### %s\n\n%s\n\n| Field | Value |\n|-------|-------|\n| **Parent Branch** | `%s` |\n| **Version** | `%s` |\n\n---\n*Sub-issue of the %s tracking issue for `%s`.*' \ + "$SUB_TITLE" "$SUB_DESC" "$BRANCH" "$VERSION" "$BRANCH_TYPE" "$BRANCH") + + SUB_URL=$(gh issue create \ + --repo "$REPO" \ + --title "$SUB_FULL_TITLE" \ + --body "$SUB_BODY" \ + --label "${SUB_LABELS}" \ + --assignee "jmiller-moko" 2>&1) + + SUB_NUM=$(echo "$SUB_URL" | grep -oE '[0-9]+$') + if [ -n "$SUB_NUM" ]; then + SUB_LIST="${SUB_LIST}\n- [ ] ${SUB_TITLE} (#${SUB_NUM})" + SUB_NUMBERS="${SUB_NUMBERS} #${SUB_NUM}" + fi + sleep 0.3 + done + + # โ”€โ”€ Create parent tracking issue โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + PARENT_BODY=$(printf '## %s Branch Created\n\n| Field | Value |\n|-------|-------|\n| **Branch** | `%s` |\n| **Version** | `%s` |\n| **Type** | %s |\n| **Created by** | @%s |\n| **Created at** | %s |\n| **Repository** | `%s` |\n\n## Workflow Sub-Issues\n\n%b\n\n---\n*Auto-created by [auto-dev-issue.yml](.github/workflows/auto-dev-issue.yml) on branch creation.*' \ + "$BRANCH_TYPE" "$BRANCH" "$VERSION" "$BRANCH_TYPE" "$ACTOR" "$NOW" "$REPO" "$SUB_LIST") + + PARENT_URL=$(gh issue create \ + --repo "$REPO" \ + --title "$TITLE" \ + --body "$PARENT_BODY" \ + --label "${LABEL_TYPE},version" \ + --assignee "jmiller-moko" 2>&1) + + PARENT_NUM=$(echo "$PARENT_URL" | grep -oE '[0-9]+$') + + # โ”€โ”€ Link sub-issues back to parent โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + if [ -n "$PARENT_NUM" ]; then + for SUB in "${SUB_ISSUES[@]}"; do + IFS='|' read -r SUB_TITLE _ _ <<< "$SUB" + SUB_FULL_TITLE="${TITLE_PREFIX}(${VERSION}): ${SUB_TITLE}" + SUB_NUM=$(gh api "repos/${REPO}/issues?state=open&per_page=20" \ + --jq ".[] | select(.title == \"${SUB_FULL_TITLE}\") | .number" 2>/dev/null | head -1) + if [ -n "$SUB_NUM" ]; then + gh api "repos/${REPO}/issues/${SUB_NUM}" -X PATCH \ + -f body="$(gh api "repos/${REPO}/issues/${SUB_NUM}" --jq '.body' 2>/dev/null) + + > **Parent Issue:** #${PARENT_NUM}" --silent 2>/dev/null || true + fi + sleep 0.2 + done + fi + + # โ”€โ”€ Create or update prerelease for alpha/beta/rc โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + if [[ "$BRANCH" == rc/* ]] || [[ "$BRANCH" == alpha/* ]] || [[ "$BRANCH" == beta/* ]]; then + case "$BRANCH_TYPE" in + Alpha) RELEASE_TAG="alpha" ;; + Beta) RELEASE_TAG="beta" ;; + "Release Candidate") RELEASE_TAG="release-candidate" ;; + esac + + EXISTING=$(gh release view "$RELEASE_TAG" --json tagName -q .tagName 2>/dev/null || true) + if [ -z "$EXISTING" ]; then + gh release create "$RELEASE_TAG" \ + --title "${RELEASE_TAG} (${VERSION})" \ + --notes "## ${BRANCH_TYPE} ${VERSION}\n\nBranch: \`${BRANCH}\`\nTracking issue: ${PARENT_URL}" \ + --prerelease \ + --target main 2>/dev/null || true + echo "${BRANCH_TYPE} release created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY + else + gh release edit "$RELEASE_TAG" \ + --title "${RELEASE_TAG} (${VERSION})" --prerelease 2>/dev/null || true + echo "${BRANCH_TYPE} release updated: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY + fi + fi + + # โ”€โ”€ Summary โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + echo "## Dev Workflow Issues Created" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Item | Issue |" >> $GITHUB_STEP_SUMMARY + echo "|------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| **Parent** | ${PARENT_URL} |" >> $GITHUB_STEP_SUMMARY + echo "| **Sub-issues** |${SUB_NUMBERS} |" >> $GITHUB_STEP_SUMMARY diff --git a/.gitea/workflows/auto-release.yml b/.gitea/workflows/auto-release.yml new file mode 100644 index 0000000..eabe619 --- /dev/null +++ b/.gitea/workflows/auto-release.yml @@ -0,0 +1,337 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: GitHub.Workflow +# INGROUP: MokoStandards.Release +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /templates/workflows/shared/auto-release.yml.template +# VERSION: 04.06.00 +# BRIEF: Generic build & release pipeline โ€” version branch, platform version, badges, tag, release +# +# +========================================================================+ +# | BUILD & RELEASE PIPELINE | +# +========================================================================+ +# | | +# | Triggers on push to main (skips bot commits + [skip ci]): | +# | | +# | Every push: | +# | 1. Read version from README.md | +# | 3. Set platform version | +# | 4. Update [VERSION: XX.YY.ZZ] badges in markdown files | +# | 6. Create git tag vXX.YY.ZZ | +# | 7a. Patch: update existing GitHub Release for this minor | +# | | +# | Every version change: archives main -> version/XX.YY branch | +# | Patch 00 = development (no release). First release = patch 01. | +# | First release only (patch == 01): | +# | 7b. Create new GitHub Release | +# | | +# +========================================================================+ + +name: Build & Release + +on: + push: + branches: + - main + - master + paths: + - 'src/**' + - 'htdocs/**' + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +permissions: + contents: write + +jobs: + release: + name: Build & Release Pipeline + runs-on: ubuntu-latest + if: >- + !contains(github.event.head_commit.message, '[skip ci]') && + github.actor != 'github-actions[bot]' + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.GH_TOKEN || github.token }} + fetch-depth: 0 + + - name: Setup MokoStandards tools + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' + run: | + git clone --depth 1 --branch version/04 --quiet \ + "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ + /tmp/mokostandards + cd /tmp/mokostandards + composer install --no-dev --no-interaction --quiet + + # -- STEP 1: Read version ----------------------------------------------- + - name: "Step 1: Read version from README.md" + id: version + run: | + VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null) + if [ -z "$VERSION" ]; then + echo "No VERSION in README.md โ€” skipping release" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + # Derive major.minor for branch naming (patches update existing branch) + MINOR=$(echo "$VERSION" | awk -F. '{printf "%s.%s", $1, $2}') + PATCH=$(echo "$VERSION" | awk -F. '{print $3}') + + MAJOR=$(echo "$VERSION" | awk -F. '{print $1}') + MINOR_NUM=$(echo "$VERSION" | awk -F. '{print $2}') + + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT" + echo "minor=$MINOR" >> "$GITHUB_OUTPUT" + echo "major=$MAJOR" >> "$GITHUB_OUTPUT" + echo "release_tag=v${MAJOR}" >> "$GITHUB_OUTPUT" + if [ "$PATCH" = "00" ]; then + echo "skip=true" >> "$GITHUB_OUTPUT" + echo "is_minor=false" >> "$GITHUB_OUTPUT" + echo "Version: $VERSION (patch 00 = development โ€” skipping release)" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + if [ "$PATCH" = "01" ]; then + echo "is_minor=true" >> "$GITHUB_OUTPUT" + echo "Version: $VERSION (first release โ€” full pipeline)" + else + echo "is_minor=false" >> "$GITHUB_OUTPUT" + echo "Version: $VERSION (patch โ€” platform version + badges only)" + fi + fi + + - name: Check if already released + if: steps.version.outputs.skip != 'true' + id: check + run: | + TAG="${{ steps.version.outputs.release_tag }}" + BRANCH="${{ steps.version.outputs.branch }}" + + TAG_EXISTS=false + BRANCH_EXISTS=false + + git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true + git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH" && BRANCH_EXISTS=true + + echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT" + echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT" + + if [ "$TAG_EXISTS" = "true" ] && [ "$BRANCH_EXISTS" = "true" ]; then + echo "already_released=true" >> "$GITHUB_OUTPUT" + else + echo "already_released=false" >> "$GITHUB_OUTPUT" + fi + + # -- SANITY CHECKS ------------------------------------------------------- + - name: "Sanity: Pre-release validation" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.version.outputs.version }}" + ERRORS=0 + + echo "## Pre-Release Sanity Checks" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # -- Version drift check (must pass before release) -------- + README_VER=$(grep -oP 'VERSION:\s*\K[\d.]+' README.md 2>/dev/null | head -1) + if [ "$README_VER" != "$VERSION" ]; then + echo "- Version drift: README says \`${README_VER}\` but releasing \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + else + echo "- Version consistent: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + + # Check CHANGELOG version matches + CL_VER=$(grep -oP 'VERSION:\s*\K[\d.]+' CHANGELOG.md 2>/dev/null | head -1) + if [ -n "$CL_VER" ] && [ "$CL_VER" != "$VERSION" ]; then + echo "- CHANGELOG drift: \`${CL_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + fi + + # Check composer.json version if present + if [ -f "composer.json" ]; then + COMP_VER=$(grep -oP '"version"\s*:\s*"\K[^"]+' composer.json 2>/dev/null | head -1) + if [ -n "$COMP_VER" ] && [ "$COMP_VER" != "$VERSION" ]; then + echo "- composer.json drift: \`${COMP_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + fi + fi + + # Common checks + if [ ! -f "LICENSE" ]; then + echo "- Missing LICENSE file" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + else + echo "- LICENSE present" >> $GITHUB_STEP_SUMMARY + fi + + if [ ! -d "src" ] && [ ! -d "htdocs" ]; then + echo "- Warning: No src/ or htdocs/ directory" >> $GITHUB_STEP_SUMMARY + else + echo "- Source directory present" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "$ERRORS" -gt 0 ]; then + echo "**${ERRORS} error(s) โ€” release may be incomplete**" >> $GITHUB_STEP_SUMMARY + else + echo "**All sanity checks passed**" >> $GITHUB_STEP_SUMMARY + fi + + # -- STEP 2: Create or update version/XX.YY archive branch --------------- + # Always runs โ€” every version change on main archives to version/XX.YY + - name: "Step 2: Version archive branch" + if: steps.check.outputs.already_released != 'true' + run: | + BRANCH="${{ steps.version.outputs.branch }}" + IS_MINOR="${{ steps.version.outputs.is_minor }}" + PATCH="${{ steps.version.outputs.version }}" + PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}') + + # Check if branch exists + if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then + git push origin HEAD:"$BRANCH" --force + echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY + else + git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH" + git push origin "$BRANCH" --force + echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY + fi + + # -- STEP 3: Set platform version ---------------------------------------- + - name: "Step 3: Set platform version" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.version.outputs.version }}" + php /tmp/mokostandards/api/cli/version_set_platform.php \ + --path . --version "$VERSION" --branch main + + # -- STEP 4: Update version badges ---------------------------------------- + - name: "Step 4: Update version badges" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.version.outputs.version }}" + find . -name "*.md" ! -path "./.git/*" ! -path "./vendor/*" | while read -r f; do + if grep -q '\[VERSION:' "$f" 2>/dev/null; then + sed -i "s/\[VERSION:[[:space:]]*[0-9]\{2\}\.[0-9]\{2\}\.[0-9]\{2\}\]/[VERSION: ${VERSION}]/" "$f" + fi + done + + # -- Commit all changes --------------------------------------------------- + - name: Commit release changes + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + if git diff --quiet && git diff --cached --quiet; then + echo "No changes to commit" + exit 0 + fi + VERSION="${{ steps.version.outputs.version }}" + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add -A + git commit -m "chore(release): build ${VERSION} [skip ci]" \ + --author="github-actions[bot] " + git push + + # -- STEP 6: Create tag --------------------------------------------------- + - name: "Step 6: Create git tag" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.tag_exists != 'true' && + steps.version.outputs.is_minor == 'true' + run: | + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + # Only create the major release tag if it doesn't exist yet + if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then + git tag "$RELEASE_TAG" + git push origin "$RELEASE_TAG" + echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY + else + echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY + fi + echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY + + # -- STEP 7: Create or update GitHub Release ------------------------------ + - name: "Step 7: GitHub Release" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.tag_exists != 'true' + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + VERSION="${{ steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + BRANCH="${{ steps.version.outputs.branch }}" + MAJOR="${{ steps.version.outputs.major }}" + + NOTES=$(php /tmp/mokostandards/api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null) + [ -z "$NOTES" ] && NOTES="Release ${VERSION}" + echo "$NOTES" > /tmp/release_notes.md + + # Check if the major release already exists + EXISTING=$(gh release view "$RELEASE_TAG" --json tagName -q .tagName 2>/dev/null || true) + + if [ -z "$EXISTING" ]; then + # First release for this major: create GitHub Release + gh release create "$RELEASE_TAG" \ + --title "v${MAJOR} (latest: ${VERSION})" \ + --notes-file /tmp/release_notes.md \ + --target "$BRANCH" + echo "Release created: ${RELEASE_TAG} (${VERSION})" >> $GITHUB_STEP_SUMMARY + else + # Update existing major release with new version info + CURRENT_NOTES=$(gh release view "$RELEASE_TAG" --json body -q .body 2>/dev/null || true) + { + echo "$CURRENT_NOTES" + echo "" + echo "---" + echo "### ${VERSION}" + echo "" + cat /tmp/release_notes.md + } > /tmp/updated_notes.md + + gh release edit "$RELEASE_TAG" \ + --title "v${MAJOR} (latest: ${VERSION})" \ + --notes-file /tmp/updated_notes.md + echo "Release updated: ${RELEASE_TAG} -> ${VERSION}" >> $GITHUB_STEP_SUMMARY + fi + + # -- Summary -------------------------------------------------------------- + - name: Pipeline Summary + if: always() + run: | + VERSION="${{ steps.version.outputs.version }}" + if [ "${{ steps.version.outputs.skip }}" = "true" ]; then + echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY + echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY + elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then + echo "## Already Released โ€” ${VERSION}" >> $GITHUB_STEP_SUMMARY + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Build & Release Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY + echo "|------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Release | [View](https://github.com/${{ github.repository }}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.gitea/workflows/changelog-validation.yml b/.gitea/workflows/changelog-validation.yml new file mode 100644 index 0000000..e2ec667 --- /dev/null +++ b/.gitea/workflows/changelog-validation.yml @@ -0,0 +1,101 @@ +# Copyright (C) 2026 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: GitHub.Workflow.Template +# INGROUP: MokoStandards.CI +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /templates/workflows/shared/changelog-validation.yml.template +# VERSION: 04.06.00 +# BRIEF: Validates CHANGELOG.md format and version consistency +# NOTE: Deployed to .github/workflows/changelog-validation.yml in governed repos. + +name: Changelog Validation + +on: + push: + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + validate-changelog: + name: Validate CHANGELOG.md + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check CHANGELOG.md exists + run: | + echo "### Changelog Validation" >> $GITHUB_STEP_SUMMARY + if [ ! -f "CHANGELOG.md" ]; then + echo "CHANGELOG.md not found in repository root." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "CHANGELOG.md exists." >> $GITHUB_STEP_SUMMARY + + - name: Check VERSION header matches README.md + run: | + # Extract version from README.md FILE INFORMATION block + README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md | head -1) + if [ -z "$README_VERSION" ]; then + echo "No VERSION found in README.md FILE INFORMATION block." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + # Check that CHANGELOG.md has a matching version header + CHANGELOG_VERSION=$(grep -oP '^\#\#\s*\[\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' CHANGELOG.md | head -1) + if [ -z "$CHANGELOG_VERSION" ]; then + echo "No version header found in CHANGELOG.md (expected \`## [XX.YY.ZZ] - YYYY-MM-DD\`)." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + if [ "$CHANGELOG_VERSION" != "$README_VERSION" ]; then + echo "CHANGELOG latest version \`${CHANGELOG_VERSION}\` does not match README VERSION \`${README_VERSION}\`." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + echo "CHANGELOG version \`${CHANGELOG_VERSION}\` matches README VERSION." >> $GITHUB_STEP_SUMMARY + + - name: Validate conventional changelog format + run: | + ERRORS=0 + + # Check that version entries follow ## [XX.YY.ZZ] - YYYY-MM-DD format + while IFS= read -r LINE; do + if ! echo "$LINE" | grep -qP '^\#\#\s*\[[0-9]{2}\.[0-9]{2}\.[0-9]{2}\]\s*-\s*[0-9]{4}-[0-9]{2}-[0-9]{2}'; then + echo "Malformed version header: \`${LINE}\`" >> $GITHUB_STEP_SUMMARY + echo " Expected format: \`## [XX.YY.ZZ] - YYYY-MM-DD\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done < <(grep -P '^\#\#\s*\[' CHANGELOG.md) + + ENTRY_COUNT=$(grep -cP '^\#\#\s*\[' CHANGELOG.md || echo "0") + if [ "$ENTRY_COUNT" -eq 0 ]; then + echo "No version entries found in CHANGELOG.md." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Found ${ENTRY_COUNT} version entr(ies) in CHANGELOG.md." >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} format issue(s) found.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Changelog format validation passed.**" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.gitea/workflows/codeql-analysis.yml b/.gitea/workflows/codeql-analysis.yml new file mode 100644 index 0000000..3abfb02 --- /dev/null +++ b/.gitea/workflows/codeql-analysis.yml @@ -0,0 +1,115 @@ +# Copyright (C) 2026 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: GitHub.Workflow.Template +# INGROUP: MokoStandards.Security +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /templates/workflows/generic/codeql-analysis.yml.template +# VERSION: 04.05.00 +# BRIEF: CodeQL security scanning workflow (generic โ€” all repo types) +# NOTE: Deployed to .github/workflows/codeql-analysis.yml in governed repos. +# CodeQL does not support PHP directly; JavaScript scans JSON/YAML/shell. +# For PHP-specific security scanning see standards-compliance.yml. + +name: CodeQL Security Scanning + +on: + push: + branches: + - main + - dev/** + - rc/** + - version/** + pull_request: + branches: + - main + - dev/** + - rc/** + schedule: + # Weekly on Monday at 06:00 UTC + - cron: '0 6 * * 1' + workflow_dispatch: + +permissions: + actions: read + contents: read + security-events: write + pull-requests: read + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ubuntu-latest + timeout-minutes: 360 + + strategy: + fail-fast: false + matrix: + # CodeQL does not support PHP. Use 'javascript' to scan JSON, YAML, + # and shell scripts. Add 'actions' to scan GitHub Actions workflows. + language: ['javascript', 'actions'] + + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + queries: security-extended,security-and-quality + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{ matrix.language }}" + upload: true + output: sarif-results + wait-for-processing: true + + - name: Upload SARIF results + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.5.0 + with: + name: codeql-results-${{ matrix.language }} + path: sarif-results + retention-days: 30 + + - name: Step summary + if: always() + run: | + echo "### ๐Ÿ” CodeQL โ€” ${{ matrix.language }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + URL="https://github.com/${{ github.repository }}/security/code-scanning" + echo "See the [Security tab]($URL) for findings." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Severity | SLA |" >> $GITHUB_STEP_SUMMARY + echo "|----------|-----|" >> $GITHUB_STEP_SUMMARY + echo "| Critical | 7 days |" >> $GITHUB_STEP_SUMMARY + echo "| High | 14 days |" >> $GITHUB_STEP_SUMMARY + echo "| Medium | 30 days |" >> $GITHUB_STEP_SUMMARY + echo "| Low | 60 days / next release |" >> $GITHUB_STEP_SUMMARY + + summary: + name: Security Scan Summary + runs-on: ubuntu-latest + needs: analyze + if: always() + + steps: + - name: Summary + run: | + echo "### ๐Ÿ›ก๏ธ CodeQL Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Trigger:** ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY + echo "**Branch:** ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY + SECURITY_URL="https://github.com/${{ github.repository }}/security" + echo "" >> $GITHUB_STEP_SUMMARY + echo "๐Ÿ“Š [View all security alerts]($SECURITY_URL)" >> $GITHUB_STEP_SUMMARY diff --git a/.gitea/workflows/copilot-agent.yml b/.gitea/workflows/copilot-agent.yml new file mode 100644 index 0000000..782945b --- /dev/null +++ b/.gitea/workflows/copilot-agent.yml @@ -0,0 +1,44 @@ +# Copyright (C) 2025 Moko Consulting +# SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later +# +# GitHub Actions workflow for Copilot coding agent +# This workflow demonstrates how to use the firewall configuration + +name: Copilot Coding Agent + +on: + pull_request: + types: [opened, synchronize, reopened] + issue_comment: + types: [created] + +permissions: + contents: write + pull-requests: write + issues: write + +jobs: + copilot-agent: + name: Run Copilot Coding Agent + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Configure Copilot Firewall + run: | + echo "Configuring firewall allowlist for enterprise-ready sites..." + bash .github/copilot/setup-firewall.sh + echo "Firewall configuration completed" + + - name: Run Copilot Agent + uses: github/copilot-swe-agent@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + issue_number: ${{ github.event.issue.number || github.event.pull_request.number }} + env: + # Environment variables are set by setup-firewall.sh + COPILOT_FIREWALL_ALLOWLIST: ${{ env.COPILOT_FIREWALL_ALLOWLIST }} diff --git a/.gitea/workflows/deploy-demo.yml b/.gitea/workflows/deploy-demo.yml new file mode 100644 index 0000000..f5fac4a --- /dev/null +++ b/.gitea/workflows/deploy-demo.yml @@ -0,0 +1,734 @@ +# Copyright (C) 2026 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# FILE INFORMATION +# DEFGROUP: GitHub.Workflow +# INGROUP: MokoStandards.Deploy +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /templates/workflows/shared/deploy-demo.yml.template +# VERSION: 04.06.00 +# BRIEF: SFTP deployment workflow for demo server โ€” synced to all governed repos +# NOTE: Synced via bulk-repo-sync to .github/workflows/deploy-demo.yml in all governed repos. +# Port is resolved in order: DEMO_FTP_PORT variable โ†’ :port suffix in DEMO_FTP_HOST โ†’ 22. + +name: Deploy to Demo Server (SFTP) + +# Deploys the contents of the src/ directory to the demo server via SFTP. +# Triggers on push/merge to main โ€” deploys the production-ready build to the demo server. +# +# Required org-level variables: DEMO_FTP_HOST, DEMO_FTP_PATH, DEMO_FTP_USERNAME +# Optional org-level variable: DEMO_FTP_PORT (auto-detected from host or defaults to 22) +# Optional org/repo variable: DEMO_FTP_SUFFIX โ€” when set, appended to DEMO_FTP_PATH to form the +# full remote destination: DEMO_FTP_PATH/DEMO_FTP_SUFFIX +# Ignore rules: Place a .ftpignore file in the src/ directory. Each non-empty, +# non-comment line is a glob pattern tested against the relative path +# of each file (e.g. "subdir/file.txt"). The .gitignore is NOT used. +# Required org-level secret: DEMO_FTP_KEY (preferred) or DEMO_FTP_PASSWORD +# +# Access control: only users with admin or maintain role on the repository may deploy. + +on: + push: + branches: + - main + - master + paths: + - 'src/**' + - 'htdocs/**' + pull_request: + types: [opened, synchronize, reopened, closed] + branches: + - main + - master + paths: + - 'src/**' + - 'htdocs/**' + workflow_dispatch: + inputs: + clear_remote: + description: 'Delete all files inside the remote destination folder before uploading' + required: false + default: false + type: boolean + +permissions: + contents: read + pull-requests: write + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + check-permission: + name: Verify Deployment Permission + runs-on: ubuntu-latest + steps: + - name: Check actor permission + env: + # Prefer the org-scoped GH_TOKEN secret (needed for the org membership + # fallback). Falls back to the built-in github.token so the collaborator + # endpoint still works even if GH_TOKEN is not configured. + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + ACTOR="${{ github.actor }}" + REPO="${{ github.repository }}" + ORG="${{ github.repository_owner }}" + + METHOD="" + AUTHORIZED="false" + + # Hardcoded authorized users โ€” always allowed to deploy + AUTHORIZED_USERS="jmiller-moko github-actions[bot]" + for user in $AUTHORIZED_USERS; do + if [ "$ACTOR" = "$user" ]; then + AUTHORIZED="true" + METHOD="hardcoded allowlist" + PERMISSION="admin" + break + fi + done + + # For other actors, check repo/org permissions via API + if [ "$AUTHORIZED" != "true" ]; then + PERMISSION=$(gh api "repos/${REPO}/collaborators/${ACTOR}/permission" \ + --jq '.permission' 2>/dev/null) + METHOD="repo collaborator API" + + if [ -z "$PERMISSION" ]; then + ORG_ROLE=$(gh api "orgs/${ORG}/memberships/${ACTOR}" \ + --jq '.role' 2>/dev/null) + METHOD="org membership API" + if [ "$ORG_ROLE" = "owner" ]; then + PERMISSION="admin" + else + PERMISSION="none" + fi + fi + + case "$PERMISSION" in + admin|maintain) AUTHORIZED="true" ;; + esac + fi + + # Write detailed summary + { + echo "## ๐Ÿ” Deploy Authorization" + echo "" + echo "| Field | Value |" + echo "|-------|-------|" + echo "| **Actor** | \`${ACTOR}\` |" + echo "| **Repository** | \`${REPO}\` |" + echo "| **Permission** | \`${PERMISSION}\` |" + echo "| **Method** | ${METHOD} |" + echo "| **Authorized** | ${AUTHORIZED} |" + echo "| **Trigger** | \`${{ github.event_name }}\` |" + echo "| **Branch** | \`${{ github.ref_name }}\` |" + echo "" + } >> "$GITHUB_STEP_SUMMARY" + + if [ "$AUTHORIZED" = "true" ]; then + echo "โœ… ${ACTOR} authorized to deploy (${METHOD})" >> "$GITHUB_STEP_SUMMARY" + else + echo "โŒ ${ACTOR} is NOT authorized to deploy." >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "Deployment requires one of:" >> "$GITHUB_STEP_SUMMARY" + echo "- Being in the hardcoded allowlist" >> "$GITHUB_STEP_SUMMARY" + echo "- Having \`admin\` or \`maintain\` role on the repository" >> "$GITHUB_STEP_SUMMARY" + exit 1 + fi + + deploy: + name: SFTP Deploy โ†’ Demo + runs-on: ubuntu-latest + needs: [check-permission] + if: >- + !startsWith(github.head_ref || github.ref_name, 'chore/') && + (github.event_name == 'workflow_dispatch' || + github.event_name == 'push' || + (github.event_name == 'pull_request' && + (github.event.action == 'opened' || + github.event.action == 'synchronize' || + github.event.action == 'reopened' || + github.event.pull_request.merged == true))) + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Resolve source directory + id: source + run: | + # Resolve source directory: src/ preferred, htdocs/ as fallback + if [ -d "src" ]; then + SRC="src" + elif [ -d "htdocs" ]; then + SRC="htdocs" + else + echo "โš ๏ธ No src/ or htdocs/ directory found โ€” skipping deployment" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + COUNT=$(find "$SRC" -type f | wc -l) + echo "โœ… Source: ${SRC}/ (${COUNT} file(s))" + echo "skip=false" >> "$GITHUB_OUTPUT" + echo "dir=${SRC}" >> "$GITHUB_OUTPUT" + + - name: Preview files to deploy + if: steps.source.outputs.skip == 'false' + env: + SOURCE_DIR: ${{ steps.source.outputs.dir }} + run: | + # โ”€โ”€ Convert a ftpignore-style glob line to an ERE pattern โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + ftpignore_to_regex() { + local line="$1" + local anchored=false + # Strip inline comments and whitespace + line=$(printf '%s' "$line" | sed 's/[[:space:]]*#.*$//' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + [ -z "$line" ] && return + # Skip negation patterns (not supported) + [[ "$line" == !* ]] && return + # Trailing slash = directory marker; strip it + line="${line%/}" + # Leading slash = anchored to root; strip it + if [[ "$line" == /* ]]; then + anchored=true + line="${line#/}" + fi + # Escape ERE special chars, then restore glob semantics + local regex + regex=$(printf '%s' "$line" \ + | sed 's/[.+^${}()|[\\]/\\&/g' \ + | sed 's/\\\*\\\*/\x01/g' \ + | sed 's/\\\*/[^\/]*/g' \ + | sed 's/\x01/.*/g' \ + | sed 's/\\\?/[^\/]/g') + if $anchored; then + printf '^%s(/|$)' "$regex" + else + printf '(^|/)%s(/|$)' "$regex" + fi + } + + # โ”€โ”€ Read .ftpignore (ftpignore-style globs) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + IGNORE_PATTERNS=() + IGNORE_SOURCES=() + if [ -f "${SOURCE_DIR}/.ftpignore" ]; then + while IFS= read -r line; do + [[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]] && continue + regex=$(ftpignore_to_regex "$line") + [ -n "$regex" ] && IGNORE_PATTERNS+=("$regex") && IGNORE_SOURCES+=("$line") + done < "${SOURCE_DIR}/.ftpignore" + fi + + # โ”€โ”€ Walk src/ and classify every file โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + WILL_UPLOAD=() + IGNORED_FILES=() + while IFS= read -r -d '' file; do + rel="${file#${SOURCE_DIR}/}" + SKIP=false + for i in "${!IGNORE_PATTERNS[@]}"; do + if echo "$rel" | grep -qE "${IGNORE_PATTERNS[$i]}" 2>/dev/null; then + IGNORED_FILES+=("$rel | .ftpignore \`${IGNORE_SOURCES[$i]}\`") + SKIP=true; break + fi + done + $SKIP && continue + WILL_UPLOAD+=("$rel") + done < <(find "$SOURCE_DIR" -type f -print0 | sort -z) + + UPLOAD_COUNT="${#WILL_UPLOAD[@]}" + IGNORE_COUNT="${#IGNORED_FILES[@]}" + + echo "โ„น๏ธ ${UPLOAD_COUNT} file(s) will be uploaded, ${IGNORE_COUNT} ignored" + + # โ”€โ”€ Write deployment preview to step summary โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + { + echo "## ๐Ÿ“‹ Deployment Preview" + echo "" + echo "| Field | Value |" + echo "|---|---|" + echo "| Source | \`${SOURCE_DIR}/\` |" + echo "| Files to upload | **${UPLOAD_COUNT}** |" + echo "| Files ignored | **${IGNORE_COUNT}** |" + echo "" + if [ "${UPLOAD_COUNT}" -gt 0 ]; then + echo "### ๐Ÿ“‚ Files that will be uploaded" + echo '```' + printf '%s\n' "${WILL_UPLOAD[@]}" + echo '```' + echo "" + fi + if [ "${IGNORE_COUNT}" -gt 0 ]; then + echo "### โญ๏ธ Files excluded" + echo "| File | Reason |" + echo "|---|---|" + for entry in "${IGNORED_FILES[@]}"; do + f="${entry% | *}"; r="${entry##* | }" + echo "| \`${f}\` | ${r} |" + done + echo "" + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Resolve SFTP host and port + if: steps.source.outputs.skip == 'false' + id: conn + env: + HOST_RAW: ${{ vars.DEMO_FTP_HOST }} + PORT_VAR: ${{ vars.DEMO_FTP_PORT }} + run: | + HOST="$HOST_RAW" + PORT="$PORT_VAR" + + if [ -z "$HOST" ]; then + echo "โญ๏ธ DEMO_FTP_HOST not configured โ€” skipping demo deployment." + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Priority 1 โ€” explicit DEMO_FTP_PORT variable + if [ -n "$PORT" ]; then + echo "โ„น๏ธ Using explicit DEMO_FTP_PORT=${PORT}" + + # Priority 2 โ€” port embedded in DEMO_FTP_HOST (host:port) + elif [[ "$HOST" == *:* ]]; then + PORT="${HOST##*:}" + HOST="${HOST%:*}" + echo "โ„น๏ธ Extracted port ${PORT} from DEMO_FTP_HOST" + + # Priority 3 โ€” SFTP default + else + PORT="22" + echo "โ„น๏ธ No port specified โ€” defaulting to SFTP port 22" + fi + + echo "host=${HOST}" >> "$GITHUB_OUTPUT" + echo "port=${PORT}" >> "$GITHUB_OUTPUT" + echo "SFTP target: ${HOST}:${PORT}" + + - name: Build remote path + if: steps.source.outputs.skip == 'false' && steps.conn.outputs.skip != 'true' + id: remote + env: + DEMO_FTP_PATH: ${{ vars.DEMO_FTP_PATH }} + DEMO_FTP_SUFFIX: ${{ vars.DEMO_FTP_SUFFIX }} + run: | + BASE="$DEMO_FTP_PATH" + + if [ -z "$BASE" ]; then + echo "โญ๏ธ DEMO_FTP_PATH not configured โ€” skipping demo deployment." + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # DEMO_FTP_SUFFIX is required โ€” it identifies the remote subdirectory for this repo. + # Without it we cannot safely determine the deployment target. + if [ -z "$DEMO_FTP_SUFFIX" ]; then + echo "โญ๏ธ DEMO_FTP_SUFFIX variable is not set โ€” skipping deployment." + echo " Set DEMO_FTP_SUFFIX as a repo or org variable to enable deploy-demo." + echo "skip=true" >> "$GITHUB_OUTPUT" + echo "path=" >> "$GITHUB_OUTPUT" + exit 0 + fi + + REMOTE="${BASE%/}/${DEMO_FTP_SUFFIX#/}" + + # โ”€โ”€ Platform-specific path safety guards โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + PLATFORM="" + MOKO_FILE=".github/.mokostandards"; [ ! -f "$MOKO_FILE" ] && MOKO_FILE=".mokostandards"; if [ -f "$MOKO_FILE" ]; then + PLATFORM=$(grep -E '^platform:' "$MOKO_FILE" | sed 's/.*:[[:space:]]*//' | tr -d '"') + fi + + if [ "$PLATFORM" = "crm-module" ]; then + # Dolibarr modules must deploy under htdocs/custom/ โ€” guard against + # accidentally overwriting server root or unrelated directories. + if [[ "$REMOTE" != *custom* ]]; then + echo "โŒ Safety check failed: Dolibarr (crm-module) remote path must contain 'custom'." + echo " Current path: ${REMOTE}" + echo " Set DEMO_FTP_SUFFIX to the module's htdocs/custom/ subdirectory." + exit 1 + fi + fi + + if [ "$PLATFORM" = "waas-component" ]; then + # Joomla extensions may only deploy to the server's tmp/ directory. + if [[ "$REMOTE" != *tmp* ]]; then + echo "โŒ Safety check failed: Joomla (waas-component) remote path must contain 'tmp'." + echo " Current path: ${REMOTE}" + echo " Set DEMO_FTP_SUFFIX to a path under the server tmp/ directory." + exit 1 + fi + fi + + echo "โ„น๏ธ Remote path: ${REMOTE}" + echo "path=${REMOTE}" >> "$GITHUB_OUTPUT" + + - name: Detect SFTP authentication method + if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true' + id: auth + env: + HAS_KEY: ${{ secrets.DEMO_FTP_KEY }} + HAS_PASSWORD: ${{ secrets.DEMO_FTP_PASSWORD }} + run: | + if [ -n "$HAS_KEY" ] && [ -n "$HAS_PASSWORD" ]; then + # Both set: key auth with password as passphrase; falls back to password-only if key fails + echo "method=key" >> "$GITHUB_OUTPUT" + echo "use_passphrase=true" >> "$GITHUB_OUTPUT" + echo "has_password=true" >> "$GITHUB_OUTPUT" + echo "โ„น๏ธ Primary: SSH key + passphrase (DEMO_FTP_KEY / DEMO_FTP_PASSWORD)" + echo "โ„น๏ธ Fallback: password-only auth if key authentication fails" + elif [ -n "$HAS_KEY" ]; then + # Key only: no passphrase, no password fallback + echo "method=key" >> "$GITHUB_OUTPUT" + echo "use_passphrase=false" >> "$GITHUB_OUTPUT" + echo "has_password=false" >> "$GITHUB_OUTPUT" + echo "โ„น๏ธ Using SSH key authentication (DEMO_FTP_KEY, no passphrase, no fallback)" + elif [ -n "$HAS_PASSWORD" ]; then + # Password only: direct SFTP password auth + echo "method=password" >> "$GITHUB_OUTPUT" + echo "use_passphrase=false" >> "$GITHUB_OUTPUT" + echo "has_password=true" >> "$GITHUB_OUTPUT" + echo "โ„น๏ธ Using password authentication (DEMO_FTP_PASSWORD)" + else + echo "โŒ No SFTP credentials configured." + echo " Set DEMO_FTP_KEY (preferred) or DEMO_FTP_PASSWORD as an org-level secret." + exit 1 + fi + + - name: Setup PHP + if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true' + uses: shivammathur/setup-php@fcafdd6392932010c2bd5094439b8e33be2a8a09 # v2.37.0 + with: + php-version: '8.1' + tools: composer + + - name: Setup MokoStandards deploy tools + if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true' + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' + run: | + git clone --depth 1 --branch version/04 --quiet \ + "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ + /tmp/mokostandards + cd /tmp/mokostandards + composer install --no-dev --no-interaction --quiet + + - name: Clear remote destination folder (manual only) + if: >- + steps.source.outputs.skip == 'false' && + steps.remote.outputs.skip != 'true' && + inputs.clear_remote == true + env: + SFTP_HOST: ${{ steps.conn.outputs.host }} + SFTP_PORT: ${{ steps.conn.outputs.port }} + SFTP_USER: ${{ vars.DEMO_FTP_USERNAME }} + SFTP_KEY: ${{ secrets.DEMO_FTP_KEY }} + SFTP_PASSWORD: ${{ secrets.DEMO_FTP_PASSWORD }} + AUTH_METHOD: ${{ steps.auth.outputs.method }} + USE_PASSPHRASE: ${{ steps.auth.outputs.use_passphrase }} + HAS_PASSWORD: ${{ steps.auth.outputs.has_password }} + REMOTE_PATH: ${{ steps.remote.outputs.path }} + run: | + cat > /tmp/moko_clear.php << 'PHPEOF' + login($username, $key)) { + if ($password !== '') { + echo "โš ๏ธ Key auth failed โ€” falling back to password\n"; + if (!$sftp->login($username, $password)) { + fwrite(STDERR, "โŒ Both key and password authentication failed\n"); + exit(1); + } + echo "โœ… Connected via password authentication (key fallback)\n"; + } else { + fwrite(STDERR, "โŒ Key authentication failed and no password fallback is available\n"); + exit(1); + } + } else { + echo "โœ… Connected via SSH key authentication\n"; + } + } else { + if (!$sftp->login($username, (string) getenv('SFTP_PASSWORD'))) { + fwrite(STDERR, "โŒ Password authentication failed\n"); + exit(1); + } + echo "โœ… Connected via password authentication\n"; + } + + // โ”€โ”€ Recursive delete โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + function rmrf(SFTP $sftp, string $path): void + { + $entries = $sftp->nlist($path); + if ($entries === false) { + return; // path does not exist โ€” nothing to clear + } + foreach ($entries as $name) { + if ($name === '.' || $name === '..') { + continue; + } + $entry = "{$path}/{$name}"; + if ($sftp->is_dir($entry)) { + rmrf($sftp, $entry); + $sftp->rmdir($entry); + echo " ๐Ÿ—‘๏ธ Removed dir: {$entry}\n"; + } else { + $sftp->delete($entry); + echo " ๐Ÿ—‘๏ธ Removed file: {$entry}\n"; + } + } + } + + // โ”€โ”€ Create remote directory tree โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + function sftpMakedirs(SFTP $sftp, string $path): void + { + $parts = array_values(array_filter(explode('/', $path), fn(string $p) => $p !== '')); + $current = str_starts_with($path, '/') ? '' : ''; + foreach ($parts as $part) { + $current .= '/' . $part; + $sftp->mkdir($current); // silently returns false if already exists + } + } + + rmrf($sftp, $remotePath); + sftpMakedirs($sftp, $remotePath); + echo "โœ… Remote folder ready: {$remotePath}\n"; + PHPEOF + php /tmp/moko_clear.php + + - name: Deploy via SFTP + if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true' + env: + SFTP_HOST: ${{ steps.conn.outputs.host }} + SFTP_PORT: ${{ steps.conn.outputs.port }} + SFTP_USER: ${{ vars.DEMO_FTP_USERNAME }} + SFTP_KEY: ${{ secrets.DEMO_FTP_KEY }} + SFTP_PASSWORD: ${{ secrets.DEMO_FTP_PASSWORD }} + AUTH_METHOD: ${{ steps.auth.outputs.method }} + USE_PASSPHRASE: ${{ steps.auth.outputs.use_passphrase }} + REMOTE_PATH: ${{ steps.remote.outputs.path }} + SOURCE_DIR: ${{ steps.source.outputs.dir }} + run: | + # โ”€โ”€ Write SSH key to temp file (key auth only) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + if [ "$AUTH_METHOD" = "key" ]; then + printf '%s' "$SFTP_KEY" > /tmp/deploy_key + chmod 600 /tmp/deploy_key + fi + + # โ”€โ”€ Generate sftp-config.json safely via jq โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + if [ "$AUTH_METHOD" = "key" ]; then + jq -n \ + --arg host "$SFTP_HOST" \ + --argjson port "${SFTP_PORT:-22}" \ + --arg user "$SFTP_USER" \ + --arg path "$REMOTE_PATH" \ + --arg key "/tmp/deploy_key" \ + '{host:$host, port:$port, user:$user, remote_path:$path, ssh_key_file:$key}' \ + > /tmp/sftp-config.json + else + jq -n \ + --arg host "$SFTP_HOST" \ + --argjson port "${SFTP_PORT:-22}" \ + --arg user "$SFTP_USER" \ + --arg path "$REMOTE_PATH" \ + --arg pass "$SFTP_PASSWORD" \ + '{host:$host, port:$port, user:$user, remote_path:$path, password:$pass}' \ + > /tmp/sftp-config.json + fi + + # โ”€โ”€ Write update files (demo = stable) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true) + VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null || echo "unknown") + REPO="${{ github.repository }}" + + if [ "$PLATFORM" = "crm-module" ]; then + printf '%s' "$VERSION" > update.txt + fi + + if [ "$PLATFORM" = "waas-component" ]; then + MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1 || true) + if [ -n "$MANIFEST" ]; then + EXT_NAME=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "${{ github.event.repository.name }}") + EXT_TYPE=$(grep -oP ']+type="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "component") + EXT_ELEMENT=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || basename "$MANIFEST" .xml) + EXT_CLIENT=$(grep -oP ']+client="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "") + EXT_FOLDER=$(grep -oP ']+group="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "") + TARGET_PLATFORM=$(grep -oP '/dev/null | head -1 || true) + [ -n "$TARGET_PLATFORM" ] && TARGET_PLATFORM="${TARGET_PLATFORM}>" + [ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '' "/") + + CLIENT_TAG="" + if [ -n "$EXT_CLIENT" ]; then CLIENT_TAG="${EXT_CLIENT}"; elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then CLIENT_TAG="site"; fi + FOLDER_TAG="" + if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then FOLDER_TAG="${EXT_FOLDER}"; fi + + DOWNLOAD_URL="https://github.com/${REPO}/releases/download/v${VERSION}/${EXT_ELEMENT}-${VERSION}.zip" + { + printf '%s\n' '' + printf '%s\n' '' + printf '%s\n' ' ' + printf '%s\n' " ${EXT_NAME}" + printf '%s\n' " ${EXT_NAME} update" + printf '%s\n' " ${EXT_ELEMENT}" + printf '%s\n' " ${EXT_TYPE}" + printf '%s\n' " ${VERSION}" + [ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}" + [ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}" + printf '%s\n' ' ' + printf '%s\n' ' stable' + printf '%s\n' ' ' + printf '%s\n' " https://github.com/${REPO}" + printf '%s\n' ' ' + printf '%s\n' " ${DOWNLOAD_URL}" + printf '%s\n' ' ' + printf '%s\n' " ${TARGET_PLATFORM}" + printf '%s\n' ' Moko Consulting' + printf '%s\n' ' https://mokoconsulting.tech' + printf '%s\n' ' ' + printf '%s\n' '' + } > updates.xml + fi + fi + + # โ”€โ”€ Run deploy-sftp.php from MokoStandards โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json) + if [ "$USE_PASSPHRASE" = "true" ]; then + DEPLOY_ARGS+=(--key-passphrase "$SFTP_PASSWORD") + fi + + PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true) + if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards/api/deploy/deploy-joomla.php" ]; then + php /tmp/mokostandards/api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}" + else + php /tmp/mokostandards/api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}" + fi + # Remove temp files that should never be left behind + rm -f /tmp/deploy_key /tmp/sftp-config.json + + - name: Create or update failure issue + if: failure() && steps.remote.outputs.skip != 'true' && steps.conn.outputs.skip != 'true' + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + REPO="${{ github.repository }}" + RUN_URL="${{ github.server_url }}/${REPO}/actions/runs/${{ github.run_id }}" + ACTOR="${{ github.actor }}" + BRANCH="${{ github.ref_name }}" + EVENT="${{ github.event_name }}" + NOW=$(date -u '+%Y-%m-%d %H:%M:%S UTC') + LABEL="deploy-failure" + + TITLE="fix: Demo deployment failed โ€” ${REPO}" + BODY="## Demo Deployment Failed + + A deployment to the demo server failed and requires attention. + + | Field | Value | + |-------|-------| + | **Repository** | \`${REPO}\` | + | **Branch** | \`${BRANCH}\` | + | **Trigger** | ${EVENT} | + | **Actor** | @${ACTOR} | + | **Failed at** | ${NOW} | + | **Run** | [View workflow run](${RUN_URL}) | + + ### Next steps + 1. Review the [workflow run log](${RUN_URL}) for the specific error. + 2. Fix the underlying issue (credentials, SFTP connectivity, permissions). + 3. Re-trigger the deployment via **Actions โ†’ Deploy to Demo Server โ†’ Run workflow**. + + --- + *Auto-created by deploy-demo.yml โ€” close this issue once the deployment is resolved.*" + + # Ensure the label exists (idempotent โ€” no-op if already present) + gh label create "$LABEL" \ + --repo "$REPO" \ + --color "CC0000" \ + --description "Automated deploy failure tracking" \ + --force 2>/dev/null || true + + # Look for an existing open deploy-failure issue + EXISTING=$(gh api "repos/${REPO}/issues?labels=${LABEL}&state=all&per_page=1&sort=created&direction=desc" \ + --jq '.[0].number' 2>/dev/null) + + if [ -n "$EXISTING" ] && [ "$EXISTING" != "null" ]; then + gh api "repos/${REPO}/issues/${EXISTING}" \ + -X PATCH \ + -f title="$TITLE" \ + -f body="$BODY" \ + -f state="open" \ + --silent + echo "๐Ÿ“‹ Failure issue #${EXISTING} updated/reopened: ${REPO}" >> "$GITHUB_STEP_SUMMARY" + else + gh issue create \ + --repo "$REPO" \ + --title "$TITLE" \ + --body "$BODY" \ + --label "$LABEL" \ + --assignee "jmiller-moko" \ + | tee -a "$GITHUB_STEP_SUMMARY" + fi + + - name: Deployment summary + if: always() + run: | + if [ "${{ steps.source.outputs.skip }}" == "true" ]; then + echo "### โญ๏ธ Deployment Skipped" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "No \`src/\` directory found in this repository." >> "$GITHUB_STEP_SUMMARY" + elif [ "${{ job.status }}" == "success" ]; then + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "### โœ… Demo Deployment Successful" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "| Field | Value |" >> "$GITHUB_STEP_SUMMARY" + echo "|-------|-------|" >> "$GITHUB_STEP_SUMMARY" + echo "| Host | \`${{ steps.conn.outputs.host }}:${{ steps.conn.outputs.port }}\` |" >> "$GITHUB_STEP_SUMMARY" + echo "| Remote path | \`${{ steps.remote.outputs.path }}\` |" >> "$GITHUB_STEP_SUMMARY" + echo "| Source | \`src/\` |" >> "$GITHUB_STEP_SUMMARY" + echo "| Trigger | ${{ github.event_name }} |" >> "$GITHUB_STEP_SUMMARY" + echo "| Auth | ${{ steps.auth.outputs.method }} |" >> "$GITHUB_STEP_SUMMARY" + echo "| Clear remote | ${{ inputs.clear_remote || 'false' }} |" >> "$GITHUB_STEP_SUMMARY" + else + echo "### โŒ Demo Deployment Failed" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "Check the job log above for error details." >> "$GITHUB_STEP_SUMMARY" + fi diff --git a/.gitea/workflows/deploy-dev.yml b/.gitea/workflows/deploy-dev.yml new file mode 100644 index 0000000..7781d00 --- /dev/null +++ b/.gitea/workflows/deploy-dev.yml @@ -0,0 +1,700 @@ +# Copyright (C) 2026 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# FILE INFORMATION +# DEFGROUP: GitHub.Workflow +# INGROUP: MokoStandards.Deploy +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /templates/workflows/shared/deploy-dev.yml.template +# VERSION: 04.06.00 +# BRIEF: SFTP deployment workflow for development server โ€” synced to all governed repos +# NOTE: Synced via bulk-repo-sync to .github/workflows/deploy-dev.yml in all governed repos. +# Port is resolved in order: DEV_FTP_PORT variable โ†’ :port suffix in DEV_FTP_HOST โ†’ 22. + +name: Deploy to Dev Server (SFTP) + +# Deploys the contents of the src/ directory to the development server via SFTP. +# Triggers on every pull_request to development branches (so the dev server always +# reflects the latest PR state) and on push/merge to main branches. +# +# Required org-level variables: DEV_FTP_HOST, DEV_FTP_PATH, DEV_FTP_USERNAME +# Optional org-level variable: DEV_FTP_PORT (auto-detected from host or defaults to 22) +# Optional org/repo variable: DEV_FTP_SUFFIX โ€” when set, appended to DEV_FTP_PATH to form the +# full remote destination: DEV_FTP_PATH/DEV_FTP_SUFFIX +# Ignore rules: Place a .ftpignore file in the src/ directory. Each non-empty, +# non-comment line is a glob pattern tested against the relative path +# of each file (e.g. "subdir/file.txt"). The .gitignore is NOT used. +# Required org-level secret: DEV_FTP_KEY (preferred) or DEV_FTP_PASSWORD +# +# Access control: only users with admin or maintain role on the repository may deploy. + +on: + push: + branches: + - 'dev/**' + - 'rc/**' + - develop + - development + paths: + - 'src/**' + - 'htdocs/**' + pull_request: + types: [opened, synchronize, reopened, closed] + branches: + - 'dev/**' + - 'rc/**' + - develop + - development + paths: + - 'src/**' + - 'htdocs/**' + workflow_dispatch: + inputs: + clear_remote: + description: 'Delete all files inside the remote destination folder before uploading' + required: false + default: false + type: boolean + +permissions: + contents: read + pull-requests: write + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + check-permission: + name: Verify Deployment Permission + runs-on: ubuntu-latest + steps: + - name: Check actor permission + env: + # Prefer the org-scoped GH_TOKEN secret (needed for the org membership + # fallback). Falls back to the built-in github.token so the collaborator + # endpoint still works even if GH_TOKEN is not configured. + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + ACTOR="${{ github.actor }}" + REPO="${{ github.repository }}" + ORG="${{ github.repository_owner }}" + + METHOD="" + AUTHORIZED="false" + + # Hardcoded authorized users โ€” always allowed to deploy + AUTHORIZED_USERS="jmiller-moko github-actions[bot]" + for user in $AUTHORIZED_USERS; do + if [ "$ACTOR" = "$user" ]; then + AUTHORIZED="true" + METHOD="hardcoded allowlist" + PERMISSION="admin" + break + fi + done + + # For other actors, check repo/org permissions via API + if [ "$AUTHORIZED" != "true" ]; then + PERMISSION=$(gh api "repos/${REPO}/collaborators/${ACTOR}/permission" \ + --jq '.permission' 2>/dev/null) + METHOD="repo collaborator API" + + if [ -z "$PERMISSION" ]; then + ORG_ROLE=$(gh api "orgs/${ORG}/memberships/${ACTOR}" \ + --jq '.role' 2>/dev/null) + METHOD="org membership API" + if [ "$ORG_ROLE" = "owner" ]; then + PERMISSION="admin" + else + PERMISSION="none" + fi + fi + + case "$PERMISSION" in + admin|maintain) AUTHORIZED="true" ;; + esac + fi + + # Write detailed summary + { + echo "## ๐Ÿ” Deploy Authorization" + echo "" + echo "| Field | Value |" + echo "|-------|-------|" + echo "| **Actor** | \`${ACTOR}\` |" + echo "| **Repository** | \`${REPO}\` |" + echo "| **Permission** | \`${PERMISSION}\` |" + echo "| **Method** | ${METHOD} |" + echo "| **Authorized** | ${AUTHORIZED} |" + echo "| **Trigger** | \`${{ github.event_name }}\` |" + echo "| **Branch** | \`${{ github.ref_name }}\` |" + echo "" + } >> "$GITHUB_STEP_SUMMARY" + + if [ "$AUTHORIZED" = "true" ]; then + echo "โœ… ${ACTOR} authorized to deploy (${METHOD})" >> "$GITHUB_STEP_SUMMARY" + else + echo "โŒ ${ACTOR} is NOT authorized to deploy." >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "Deployment requires one of:" >> "$GITHUB_STEP_SUMMARY" + echo "- Being in the hardcoded allowlist" >> "$GITHUB_STEP_SUMMARY" + echo "- Having \`admin\` or \`maintain\` role on the repository" >> "$GITHUB_STEP_SUMMARY" + exit 1 + fi + + deploy: + name: SFTP Deploy โ†’ Dev + runs-on: ubuntu-latest + needs: [check-permission] + if: >- + !startsWith(github.head_ref || github.ref_name, 'chore/') && + (github.event_name == 'workflow_dispatch' || + github.event_name == 'push' || + (github.event_name == 'pull_request' && + (github.event.action == 'opened' || + github.event.action == 'synchronize' || + github.event.action == 'reopened' || + github.event.pull_request.merged == true))) + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Resolve source directory + id: source + run: | + # Resolve source directory: src/ preferred, htdocs/ as fallback + if [ -d "src" ]; then + SRC="src" + elif [ -d "htdocs" ]; then + SRC="htdocs" + else + echo "โš ๏ธ No src/ or htdocs/ directory found โ€” skipping deployment" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + COUNT=$(find "$SRC" -type f | wc -l) + echo "โœ… Source: ${SRC}/ (${COUNT} file(s))" + echo "skip=false" >> "$GITHUB_OUTPUT" + echo "dir=${SRC}" >> "$GITHUB_OUTPUT" + + - name: Preview files to deploy + if: steps.source.outputs.skip == 'false' + env: + SOURCE_DIR: ${{ steps.source.outputs.dir }} + run: | + # โ”€โ”€ Convert a ftpignore-style glob line to an ERE pattern โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + ftpignore_to_regex() { + local line="$1" + local anchored=false + # Strip inline comments and whitespace + line=$(printf '%s' "$line" | sed 's/[[:space:]]*#.*$//' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + [ -z "$line" ] && return + # Skip negation patterns (not supported) + [[ "$line" == !* ]] && return + # Trailing slash = directory marker; strip it + line="${line%/}" + # Leading slash = anchored to root; strip it + if [[ "$line" == /* ]]; then + anchored=true + line="${line#/}" + fi + # Escape ERE special chars, then restore glob semantics + local regex + regex=$(printf '%s' "$line" \ + | sed 's/[.+^${}()|[\\]/\\&/g' \ + | sed 's/\\\*\\\*/\x01/g' \ + | sed 's/\\\*/[^\/]*/g' \ + | sed 's/\x01/.*/g' \ + | sed 's/\\\?/[^\/]/g') + if $anchored; then + printf '^%s(/|$)' "$regex" + else + printf '(^|/)%s(/|$)' "$regex" + fi + } + + # โ”€โ”€ Read .ftpignore (ftpignore-style globs) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + IGNORE_PATTERNS=() + IGNORE_SOURCES=() + if [ -f "${SOURCE_DIR}/.ftpignore" ]; then + while IFS= read -r line; do + [[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]] && continue + regex=$(ftpignore_to_regex "$line") + [ -n "$regex" ] && IGNORE_PATTERNS+=("$regex") && IGNORE_SOURCES+=("$line") + done < "${SOURCE_DIR}/.ftpignore" + fi + + # โ”€โ”€ Walk src/ and classify every file โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + WILL_UPLOAD=() + IGNORED_FILES=() + while IFS= read -r -d '' file; do + rel="${file#${SOURCE_DIR}/}" + SKIP=false + for i in "${!IGNORE_PATTERNS[@]}"; do + if echo "$rel" | grep -qE "${IGNORE_PATTERNS[$i]}" 2>/dev/null; then + IGNORED_FILES+=("$rel | .ftpignore \`${IGNORE_SOURCES[$i]}\`") + SKIP=true; break + fi + done + $SKIP && continue + WILL_UPLOAD+=("$rel") + done < <(find "$SOURCE_DIR" -type f -print0 | sort -z) + + UPLOAD_COUNT="${#WILL_UPLOAD[@]}" + IGNORE_COUNT="${#IGNORED_FILES[@]}" + + echo "โ„น๏ธ ${UPLOAD_COUNT} file(s) will be uploaded, ${IGNORE_COUNT} ignored" + + # โ”€โ”€ Write deployment preview to step summary โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + { + echo "## ๐Ÿ“‹ Deployment Preview" + echo "" + echo "| Field | Value |" + echo "|---|---|" + echo "| Source | \`${SOURCE_DIR}/\` |" + echo "| Files to upload | **${UPLOAD_COUNT}** |" + echo "| Files ignored | **${IGNORE_COUNT}** |" + echo "" + if [ "${UPLOAD_COUNT}" -gt 0 ]; then + echo "### ๐Ÿ“‚ Files that will be uploaded" + echo '```' + printf '%s\n' "${WILL_UPLOAD[@]}" + echo '```' + echo "" + fi + if [ "${IGNORE_COUNT}" -gt 0 ]; then + echo "### โญ๏ธ Files excluded" + echo "| File | Reason |" + echo "|---|---|" + for entry in "${IGNORED_FILES[@]}"; do + f="${entry% | *}"; r="${entry##* | }" + echo "| \`${f}\` | ${r} |" + done + echo "" + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Resolve SFTP host and port + if: steps.source.outputs.skip == 'false' + id: conn + env: + HOST_RAW: ${{ vars.DEV_FTP_HOST }} + PORT_VAR: ${{ vars.DEV_FTP_PORT }} + run: | + HOST="$HOST_RAW" + PORT="$PORT_VAR" + + # Priority 1 โ€” explicit DEV_FTP_PORT variable + if [ -n "$PORT" ]; then + echo "โ„น๏ธ Using explicit DEV_FTP_PORT=${PORT}" + + # Priority 2 โ€” port embedded in DEV_FTP_HOST (host:port) + elif [[ "$HOST" == *:* ]]; then + PORT="${HOST##*:}" + HOST="${HOST%:*}" + echo "โ„น๏ธ Extracted port ${PORT} from DEV_FTP_HOST" + + # Priority 3 โ€” SFTP default + else + PORT="22" + echo "โ„น๏ธ No port specified โ€” defaulting to SFTP port 22" + fi + + echo "host=${HOST}" >> "$GITHUB_OUTPUT" + echo "port=${PORT}" >> "$GITHUB_OUTPUT" + echo "SFTP target: ${HOST}:${PORT}" + + - name: Build remote path + if: steps.source.outputs.skip == 'false' + id: remote + env: + DEV_FTP_PATH: ${{ vars.DEV_FTP_PATH }} + DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }} + run: | + BASE="$DEV_FTP_PATH" + + if [ -z "$BASE" ]; then + echo "โŒ DEV_FTP_PATH is not set." + echo " Configure it as an org-level variable (Settings โ†’ Variables) and" + echo " ensure this repository has been granted access to it." + exit 1 + fi + + # DEV_FTP_SUFFIX is required โ€” it identifies the remote subdirectory for this repo. + # Without it we cannot safely determine the deployment target. + if [ -z "$DEV_FTP_SUFFIX" ]; then + echo "โญ๏ธ DEV_FTP_SUFFIX variable is not set โ€” skipping deployment." + echo " Set DEV_FTP_SUFFIX as a repo or org variable to enable deploy-dev." + echo "skip=true" >> "$GITHUB_OUTPUT" + echo "path=" >> "$GITHUB_OUTPUT" + exit 0 + fi + + REMOTE="${BASE%/}/${DEV_FTP_SUFFIX#/}" + + # โ”€โ”€ Platform-specific path safety guards โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + PLATFORM="" + MOKO_FILE=".github/.mokostandards"; [ ! -f "$MOKO_FILE" ] && MOKO_FILE=".mokostandards"; if [ -f "$MOKO_FILE" ]; then + PLATFORM=$(grep -oP '^platform:.*' "$MOKO_FILE" 2>/dev/null || true) + fi + + if [ "$PLATFORM" = "crm-module" ]; then + # Dolibarr modules must deploy under htdocs/custom/ โ€” guard against + # accidentally overwriting server root or unrelated directories. + if [[ "$REMOTE" != *custom* ]]; then + echo "โŒ Safety check failed: Dolibarr (crm-module) remote path must contain 'custom'." + echo " Current path: ${REMOTE}" + echo " Set DEV_FTP_SUFFIX to the module's htdocs/custom/ subdirectory." + exit 1 + fi + fi + + if [ "$PLATFORM" = "waas-component" ]; then + # Joomla extensions may only deploy to the server's tmp/ directory. + if [[ "$REMOTE" != *tmp* ]]; then + echo "โŒ Safety check failed: Joomla (waas-component) remote path must contain 'tmp'." + echo " Current path: ${REMOTE}" + echo " Set DEV_FTP_SUFFIX to a path under the server tmp/ directory." + exit 1 + fi + fi + + echo "โ„น๏ธ Remote path: ${REMOTE}" + echo "path=${REMOTE}" >> "$GITHUB_OUTPUT" + + - name: Detect SFTP authentication method + if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true' + id: auth + env: + HAS_KEY: ${{ secrets.DEV_FTP_KEY }} + HAS_PASSWORD: ${{ secrets.DEV_FTP_PASSWORD }} + run: | + if [ -n "$HAS_KEY" ] && [ -n "$HAS_PASSWORD" ]; then + # Both set: key auth with password as passphrase; falls back to password-only if key fails + echo "method=key" >> "$GITHUB_OUTPUT" + echo "use_passphrase=true" >> "$GITHUB_OUTPUT" + echo "has_password=true" >> "$GITHUB_OUTPUT" + echo "โ„น๏ธ Primary: SSH key + passphrase (DEV_FTP_KEY / DEV_FTP_PASSWORD)" + echo "โ„น๏ธ Fallback: password-only auth if key authentication fails" + elif [ -n "$HAS_KEY" ]; then + # Key only: no passphrase, no password fallback + echo "method=key" >> "$GITHUB_OUTPUT" + echo "use_passphrase=false" >> "$GITHUB_OUTPUT" + echo "has_password=false" >> "$GITHUB_OUTPUT" + echo "โ„น๏ธ Using SSH key authentication (DEV_FTP_KEY, no passphrase, no fallback)" + elif [ -n "$HAS_PASSWORD" ]; then + # Password only: direct SFTP password auth + echo "method=password" >> "$GITHUB_OUTPUT" + echo "use_passphrase=false" >> "$GITHUB_OUTPUT" + echo "has_password=true" >> "$GITHUB_OUTPUT" + echo "โ„น๏ธ Using password authentication (DEV_FTP_PASSWORD)" + else + echo "โŒ No SFTP credentials configured." + echo " Set DEV_FTP_KEY (preferred) or DEV_FTP_PASSWORD as an org-level secret." + exit 1 + fi + + - name: Setup PHP + if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true' + uses: shivammathur/setup-php@fcafdd6392932010c2bd5094439b8e33be2a8a09 # v2.37.0 + with: + php-version: '8.1' + tools: composer + + - name: Setup MokoStandards deploy tools + if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true' + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' + run: | + git clone --depth 1 --branch version/04 --quiet \ + "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ + /tmp/mokostandards + cd /tmp/mokostandards + composer install --no-dev --no-interaction --quiet + + - name: Clear remote destination folder (manual only) + if: >- + steps.source.outputs.skip == 'false' && + steps.remote.outputs.skip != 'true' && + inputs.clear_remote == true + env: + SFTP_HOST: ${{ steps.conn.outputs.host }} + SFTP_PORT: ${{ steps.conn.outputs.port }} + SFTP_USER: ${{ vars.DEV_FTP_USERNAME }} + SFTP_KEY: ${{ secrets.DEV_FTP_KEY }} + SFTP_PASSWORD: ${{ secrets.DEV_FTP_PASSWORD }} + AUTH_METHOD: ${{ steps.auth.outputs.method }} + USE_PASSPHRASE: ${{ steps.auth.outputs.use_passphrase }} + HAS_PASSWORD: ${{ steps.auth.outputs.has_password }} + REMOTE_PATH: ${{ steps.remote.outputs.path }} + run: | + cat > /tmp/moko_clear.php << 'PHPEOF' + login($username, $key)) { + if ($password !== '') { + echo "โš ๏ธ Key auth failed โ€” falling back to password\n"; + if (!$sftp->login($username, $password)) { + fwrite(STDERR, "โŒ Both key and password authentication failed\n"); + exit(1); + } + echo "โœ… Connected via password authentication (key fallback)\n"; + } else { + fwrite(STDERR, "โŒ Key authentication failed and no password fallback is available\n"); + exit(1); + } + } else { + echo "โœ… Connected via SSH key authentication\n"; + } + } else { + if (!$sftp->login($username, (string) getenv('SFTP_PASSWORD'))) { + fwrite(STDERR, "โŒ Password authentication failed\n"); + exit(1); + } + echo "โœ… Connected via password authentication\n"; + } + + // โ”€โ”€ Recursive delete โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + function rmrf(SFTP $sftp, string $path): void + { + $entries = $sftp->nlist($path); + if ($entries === false) { + return; // path does not exist โ€” nothing to clear + } + foreach ($entries as $name) { + if ($name === '.' || $name === '..') { + continue; + } + $entry = "{$path}/{$name}"; + if ($sftp->is_dir($entry)) { + rmrf($sftp, $entry); + $sftp->rmdir($entry); + echo " ๐Ÿ—‘๏ธ Removed dir: {$entry}\n"; + } else { + $sftp->delete($entry); + echo " ๐Ÿ—‘๏ธ Removed file: {$entry}\n"; + } + } + } + + // โ”€โ”€ Create remote directory tree โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + function sftpMakedirs(SFTP $sftp, string $path): void + { + $parts = array_values(array_filter(explode('/', $path), fn(string $p) => $p !== '')); + $current = str_starts_with($path, '/') ? '' : ''; + foreach ($parts as $part) { + $current .= '/' . $part; + $sftp->mkdir($current); // silently returns false if already exists + } + } + + rmrf($sftp, $remotePath); + sftpMakedirs($sftp, $remotePath); + echo "โœ… Remote folder ready: {$remotePath}\n"; + PHPEOF + php /tmp/moko_clear.php + + - name: Deploy via SFTP + if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true' + env: + SFTP_HOST: ${{ steps.conn.outputs.host }} + SFTP_PORT: ${{ steps.conn.outputs.port }} + SFTP_USER: ${{ vars.DEV_FTP_USERNAME }} + SFTP_KEY: ${{ secrets.DEV_FTP_KEY }} + SFTP_PASSWORD: ${{ secrets.DEV_FTP_PASSWORD }} + AUTH_METHOD: ${{ steps.auth.outputs.method }} + USE_PASSPHRASE: ${{ steps.auth.outputs.use_passphrase }} + REMOTE_PATH: ${{ steps.remote.outputs.path }} + SOURCE_DIR: ${{ steps.source.outputs.dir }} + run: | + # โ”€โ”€ Write SSH key to temp file (key auth only) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + if [ "$AUTH_METHOD" = "key" ]; then + printf '%s' "$SFTP_KEY" > /tmp/deploy_key + chmod 600 /tmp/deploy_key + fi + + # โ”€โ”€ Generate sftp-config.json safely via jq โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + if [ "$AUTH_METHOD" = "key" ]; then + jq -n \ + --arg host "$SFTP_HOST" \ + --argjson port "${SFTP_PORT:-22}" \ + --arg user "$SFTP_USER" \ + --arg path "$REMOTE_PATH" \ + --arg key "/tmp/deploy_key" \ + '{host:$host, port:$port, user:$user, remote_path:$path, ssh_key_file:$key}' \ + > /tmp/sftp-config.json + else + jq -n \ + --arg host "$SFTP_HOST" \ + --argjson port "${SFTP_PORT:-22}" \ + --arg user "$SFTP_USER" \ + --arg path "$REMOTE_PATH" \ + --arg pass "$SFTP_PASSWORD" \ + '{host:$host, port:$port, user:$user, remote_path:$path, password:$pass}' \ + > /tmp/sftp-config.json + fi + + # Dev deploys skip minified files โ€” use unminified sources for debugging + echo "*.min.js" >> "${SOURCE_DIR}/.ftpignore" + echo "*.min.css" >> "${SOURCE_DIR}/.ftpignore" + + # โ”€โ”€ Run deploy-sftp.php from MokoStandards โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json) + if [ "$USE_PASSPHRASE" = "true" ]; then + DEPLOY_ARGS+=(--key-passphrase "$SFTP_PASSWORD") + fi + + # Set platform version to "development" before deploy (Dolibarr + Joomla) + php /tmp/mokostandards/api/cli/version_set_platform.php --path . --version development + + # Write update files โ€” dev/** = development, rc/** = rc + PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true) + REPO="${{ github.repository }}" + BRANCH="${{ github.ref_name }}" + + # Determine stability tag from branch prefix + STABILITY="development" + VERSION_LABEL="development" + if [[ "$BRANCH" == rc/* ]]; then + STABILITY="rc" + VERSION_LABEL=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null || echo "${BRANCH#rc/}")-rc + fi + + if [ "$PLATFORM" = "crm-module" ]; then + printf '%s' "$VERSION_LABEL" > update.txt + fi + + if [ "$PLATFORM" = "waas-component" ]; then + MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1 || true) + if [ -n "$MANIFEST" ]; then + EXT_NAME=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "${{ github.event.repository.name }}") + EXT_TYPE=$(grep -oP ']+type="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "component") + EXT_ELEMENT=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || basename "$MANIFEST" .xml) + EXT_CLIENT=$(grep -oP ']+client="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "") + EXT_FOLDER=$(grep -oP ']+group="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "") + TARGET_PLATFORM=$(grep -oP '/dev/null | head -1 || true) + [ -n "$TARGET_PLATFORM" ] && TARGET_PLATFORM="${TARGET_PLATFORM}>" + [ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '' "/") + + CLIENT_TAG="" + if [ -n "$EXT_CLIENT" ]; then + CLIENT_TAG="${EXT_CLIENT}" + elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then + CLIENT_TAG="site" + fi + + FOLDER_TAG="" + if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then + FOLDER_TAG="${EXT_FOLDER}" + fi + + DOWNLOAD_URL="https://github.com/${REPO}/archive/refs/heads/${BRANCH}.zip" + + { + printf '%s\n' '' + printf '%s\n' '' + printf '%s\n' ' ' + printf '%s\n' " ${EXT_NAME}" + printf '%s\n' " ${EXT_NAME} ${STABILITY} build" + printf '%s\n' " ${EXT_ELEMENT}" + printf '%s\n' " ${EXT_TYPE}" + printf '%s\n' " ${VERSION_LABEL}" + [ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}" + [ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}" + printf '%s\n' ' ' + printf '%s\n' " ${STABILITY}" + printf '%s\n' ' ' + printf '%s\n' " https://github.com/${REPO}/tree/${BRANCH}" + printf '%s\n' ' ' + printf '%s\n' " ${DOWNLOAD_URL}" + printf '%s\n' ' ' + printf '%s\n' " ${TARGET_PLATFORM}" + printf '%s\n' ' Moko Consulting' + printf '%s\n' ' https://mokoconsulting.tech' + printf '%s\n' ' ' + printf '%s\n' '' + } > updates.xml + sed -i '/^[[:space:]]*$/d' updates.xml + fi + fi + + # Use Joomla-aware deploy for waas-component (routes files to correct Joomla dirs) + # Use standard SFTP deploy for everything else + PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true) + if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards/api/deploy/deploy-joomla.php" ]; then + php /tmp/mokostandards/api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}" + else + php /tmp/mokostandards/api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}" + fi + # (both scripts handle dotfile skipping and .ftpignore natively) + # Remove temp files that should never be left behind + rm -f /tmp/deploy_key /tmp/sftp-config.json + + # Dev deploys fail silently โ€” no issue creation. + # Demo and RS deploys create failure issues (production-facing). + + - name: Deployment summary + if: always() + run: | + if [ "${{ steps.source.outputs.skip }}" == "true" ]; then + echo "### โญ๏ธ Deployment Skipped" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "No \`src/\` directory found in this repository." >> "$GITHUB_STEP_SUMMARY" + elif [ "${{ job.status }}" == "success" ]; then + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "### โœ… Dev Deployment Successful" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "| Field | Value |" >> "$GITHUB_STEP_SUMMARY" + echo "|-------|-------|" >> "$GITHUB_STEP_SUMMARY" + echo "| Host | \`${{ steps.conn.outputs.host }}:${{ steps.conn.outputs.port }}\` |" >> "$GITHUB_STEP_SUMMARY" + echo "| Remote path | \`${{ steps.remote.outputs.path }}\` |" >> "$GITHUB_STEP_SUMMARY" + echo "| Source | \`src/\` |" >> "$GITHUB_STEP_SUMMARY" + echo "| Trigger | ${{ github.event_name }} |" >> "$GITHUB_STEP_SUMMARY" + echo "| Auth | ${{ steps.auth.outputs.method }} |" >> "$GITHUB_STEP_SUMMARY" + echo "| Clear remote | ${{ inputs.clear_remote || 'false' }} |" >> "$GITHUB_STEP_SUMMARY" + else + echo "### โŒ Dev Deployment Failed" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "Check the job log above for error details." >> "$GITHUB_STEP_SUMMARY" + fi diff --git a/.gitea/workflows/enterprise-firewall-setup.yml b/.gitea/workflows/enterprise-firewall-setup.yml new file mode 100644 index 0000000..1a533fb --- /dev/null +++ b/.gitea/workflows/enterprise-firewall-setup.yml @@ -0,0 +1,758 @@ +# Copyright (C) 2026 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# FILE INFORMATION +# DEFGROUP: GitHub.Workflow +# INGROUP: MokoStandards.Firewall +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /templates/workflows/shared/enterprise-firewall-setup.yml.template +# VERSION: 04.06.00 +# BRIEF: Enterprise firewall configuration โ€” generates outbound allow-rules including SFTP deployment server +# NOTE: Reads DEV_FTP_HOST / DEV_FTP_PORT variables to include SFTP egress rules alongside HTTPS rules. + +name: Enterprise Firewall Configuration + +# This workflow provides firewall configuration guidance for enterprise-ready sites +# It generates firewall rules for allowing outbound access to trusted domains +# including license providers, documentation sources, package registries, +# and the SFTP deployment server (DEV_FTP_HOST / DEV_FTP_PORT). +# +# Runs automatically when: +# - Coding agent workflows are triggered (pull requests with copilot/ prefix) +# - Manual workflow dispatch for custom configurations + +on: + workflow_dispatch: + inputs: + firewall_type: + description: 'Target firewall type' + required: true + type: choice + options: + - 'iptables' + - 'ufw' + - 'firewalld' + - 'aws-security-group' + - 'azure-nsg' + - 'gcp-firewall' + - 'cloudflare' + - 'all' + default: 'all' + output_format: + description: 'Output format' + required: true + type: choice + options: + - 'shell-script' + - 'json' + - 'yaml' + - 'markdown' + - 'all' + default: 'markdown' + + # Auto-run when coding agent creates or updates PRs + pull_request: + branches: + - 'copilot/**' + - 'agent/**' + types: [opened, synchronize, reopened] + + # Auto-run on push to coding agent branches + push: + branches: + - 'copilot/**' + - 'agent/**' + +permissions: + contents: read + actions: read + +jobs: + generate-firewall-rules: + name: Generate Firewall Rules + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.11' + + - name: Apply Firewall Rules to Runner (Auto-run only) + if: github.event_name != 'workflow_dispatch' + env: + DEV_FTP_HOST: ${{ vars.DEV_FTP_HOST }} + DEV_FTP_PORT: ${{ vars.DEV_FTP_PORT }} + run: | + echo "๐Ÿ”ฅ Applying firewall rules for coding agent environment..." + echo "" + echo "This step ensures the GitHub Actions runner can access trusted domains" + echo "including license providers, package registries, and documentation sources." + echo "" + + # Note: GitHub Actions runners are ephemeral and run in controlled environments + # This step documents what domains are being accessed during the workflow + # Actual firewall configuration is managed by GitHub + + cat > /tmp/trusted-domains.txt << 'EOF' + # Trusted domains for coding agent environment + # License Providers + www.gnu.org + opensource.org + choosealicense.com + spdx.org + creativecommons.org + apache.org + fsf.org + + # Documentation & Standards + semver.org + keepachangelog.com + conventionalcommits.org + + # GitHub & Related + github.com + api.github.com + docs.github.com + raw.githubusercontent.com + ghcr.io + + # Package Registries + npmjs.com + registry.npmjs.org + pypi.org + files.pythonhosted.org + packagist.org + repo.packagist.org + rubygems.org + + # Platform-Specific + joomla.org + downloads.joomla.org + docs.joomla.org + php.net + getcomposer.org + dolibarr.org + wiki.dolibarr.org + docs.dolibarr.org + + # Moko Consulting + mokoconsulting.tech + + # SFTP Deployment Server (DEV_FTP_HOST) + ${DEV_FTP_HOST:-} + + # Google Services + drive.google.com + docs.google.com + sheets.google.com + accounts.google.com + storage.googleapis.com + fonts.googleapis.com + fonts.gstatic.com + + # GitHub Extended + upload.github.com + objects.githubusercontent.com + user-images.githubusercontent.com + codeload.github.com + pkg.github.com + + # Developer Reference + developer.mozilla.org + stackoverflow.com + git-scm.com + + # CDN & Infrastructure + cdn.jsdelivr.net + unpkg.com + cdnjs.cloudflare.com + img.shields.io + + # Container Registries + hub.docker.com + registry-1.docker.io + + # CI & Code Quality + codecov.io + sonarcloud.io + + # Terraform & Infrastructure + registry.terraform.io + releases.hashicorp.com + checkpoint-api.hashicorp.com + EOF + + echo "โœ“ Trusted domains documented for this runner" + echo "โœ“ GitHub Actions runners have network access to these domains" + echo "" + + # Test connectivity to key domains + echo "Testing connectivity to key domains..." + for domain in "github.com" "www.gnu.org" "npmjs.com" "pypi.org"; do + if curl -s --max-time 3 -o /dev/null -w "%{http_code}" "https://$domain" | grep -q "200\|301\|302"; then + echo " โœ“ $domain is accessible" + else + echo " โš ๏ธ $domain connectivity check failed (may be expected)" + fi + done + + # Test SFTP server connectivity (TCP port check) + SFTP_HOST="${DEV_FTP_HOST:-}" + SFTP_PORT="${DEV_FTP_PORT:-22}" + if [ -n "$SFTP_HOST" ]; then + # Strip any embedded :port suffix + SFTP_HOST="${SFTP_HOST%%:*}" + echo "" + echo "Testing SFTP deployment server connectivity..." + if timeout 5 bash -c "echo >/dev/tcp/${SFTP_HOST}/${SFTP_PORT}" 2>/dev/null; then + echo " โœ“ SFTP server ${SFTP_HOST}:${SFTP_PORT} is reachable" + else + echo " โš ๏ธ SFTP server ${SFTP_HOST}:${SFTP_PORT} is not reachable from runner (firewall rule needed)" + fi + else + echo "" + echo " โ„น๏ธ DEV_FTP_HOST not configured โ€” skipping SFTP connectivity check" + fi + + - name: Generate Firewall Configuration + id: generate + env: + DEV_FTP_HOST: ${{ vars.DEV_FTP_HOST }} + DEV_FTP_PORT: ${{ vars.DEV_FTP_PORT }} + run: | + cat > generate_firewall_config.py << 'PYTHON_EOF' + #!/usr/bin/env python3 + """ + Enterprise Firewall Configuration Generator + + Generates firewall rules for enterprise-ready deployments allowing + access to trusted domains including license providers, documentation + sources, package registries, and platform-specific sites. + """ + + import json + import os + import yaml + import sys + from typing import List, Dict + + # SFTP deployment server from org variables + _sftp_host_raw = os.environ.get("DEV_FTP_HOST", "").strip() + _sftp_port = os.environ.get("DEV_FTP_PORT", "").strip() or "22" + # Strip embedded :port suffix if present + _sftp_host = _sftp_host_raw.split(":")[0] if _sftp_host_raw else "" + if ":" in _sftp_host_raw and not _sftp_port: + _sftp_port = _sftp_host_raw.split(":")[1] + + SFTP_HOST = _sftp_host + SFTP_PORT = int(_sftp_port) if _sftp_port.isdigit() else 22 + + # Trusted domains from .github/copilot.yml + TRUSTED_DOMAINS = { + "license_providers": [ + "www.gnu.org", + "opensource.org", + "choosealicense.com", + "spdx.org", + "creativecommons.org", + "apache.org", + "fsf.org", + ], + "documentation_standards": [ + "semver.org", + "keepachangelog.com", + "conventionalcommits.org", + ], + "github_related": [ + "github.com", + "api.github.com", + "docs.github.com", + "raw.githubusercontent.com", + "ghcr.io", + ], + "package_registries": [ + "npmjs.com", + "registry.npmjs.org", + "pypi.org", + "files.pythonhosted.org", + "packagist.org", + "repo.packagist.org", + "rubygems.org", + ], + "standards_organizations": [ + "json-schema.org", + "w3.org", + "ietf.org", + ], + "platform_specific": [ + "joomla.org", + "downloads.joomla.org", + "docs.joomla.org", + "php.net", + "getcomposer.org", + "dolibarr.org", + "wiki.dolibarr.org", + "docs.dolibarr.org", + ], + "moko_consulting": [ + "mokoconsulting.tech", + ], + "google_services": [ + "drive.google.com", + "docs.google.com", + "sheets.google.com", + "accounts.google.com", + "storage.googleapis.com", + "fonts.googleapis.com", + "fonts.gstatic.com", + ], + "github_extended": [ + "upload.github.com", + "objects.githubusercontent.com", + "user-images.githubusercontent.com", + "codeload.github.com", + "pkg.github.com", + ], + "developer_reference": [ + "developer.mozilla.org", + "stackoverflow.com", + "git-scm.com", + ], + "cdn_and_infrastructure": [ + "cdn.jsdelivr.net", + "unpkg.com", + "cdnjs.cloudflare.com", + "img.shields.io", + ], + "container_registries": [ + "hub.docker.com", + "registry-1.docker.io", + ], + "ci_code_quality": [ + "codecov.io", + "sonarcloud.io", + ], + "terraform_infrastructure": [ + "registry.terraform.io", + "releases.hashicorp.com", + "checkpoint-api.hashicorp.com", + ], + } + + # Inject SFTP deployment server as a separate category (port 22, not 443) + if SFTP_HOST: + TRUSTED_DOMAINS["sftp_deployment_server"] = [SFTP_HOST] + print(f"โ„น๏ธ SFTP deployment server: {SFTP_HOST}:{SFTP_PORT}") + + def generate_sftp_iptables_rules(host: str, port: int) -> str: + """Generate iptables rules specifically for SFTP egress""" + return ( + f"# Allow SFTP to deployment server {host}:{port}\n" + f"iptables -A OUTPUT -p tcp -d $(dig +short {host} | head -1)" + f" --dport {port} -j ACCEPT # SFTP deploy\n" + ) + + def generate_sftp_ufw_rules(host: str, port: int) -> str: + """Generate UFW rules for SFTP egress""" + return ( + f"# Allow SFTP to deployment server\n" + f"ufw allow out to $(dig +short {host} | head -1)" + f" port {port} proto tcp comment 'SFTP deploy to {host}'\n" + ) + + def generate_sftp_firewalld_rules(host: str, port: int) -> str: + """Generate firewalld rules for SFTP egress""" + return ( + f"# Allow SFTP to deployment server\n" + f"firewall-cmd --permanent --add-rich-rule='" + f"rule family=ipv4 destination address=$(dig +short {host} | head -1)" + f" port port={port} protocol=tcp accept' # SFTP deploy\n" + ) + + def generate_iptables_rules(domains: List[str]) -> str: + """Generate iptables firewall rules""" + rules = ["#!/bin/bash", "", "# Enterprise Firewall Rules - iptables", ""] + rules.append("# Allow outbound HTTPS to trusted domains") + rules.append("") + + for domain in domains: + rules.append(f"# Allow {domain}") + rules.append(f"iptables -A OUTPUT -p tcp -d $(dig +short {domain} | head -1) --dport 443 -j ACCEPT") + + rules.append("") + rules.append("# Allow DNS lookups") + rules.append("iptables -A OUTPUT -p udp --dport 53 -j ACCEPT") + rules.append("iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT") + + return "\n".join(rules) + + def generate_ufw_rules(domains: List[str]) -> str: + """Generate UFW firewall rules""" + rules = ["#!/bin/bash", "", "# Enterprise Firewall Rules - UFW", ""] + rules.append("# Allow outbound HTTPS to trusted domains") + rules.append("") + + for domain in domains: + rules.append(f"# Allow {domain}") + rules.append(f"ufw allow out to $(dig +short {domain} | head -1) port 443 proto tcp comment 'Allow {domain}'") + + rules.append("") + rules.append("# Allow DNS") + rules.append("ufw allow out 53/udp comment 'Allow DNS UDP'") + rules.append("ufw allow out 53/tcp comment 'Allow DNS TCP'") + + return "\n".join(rules) + + def generate_firewalld_rules(domains: List[str]) -> str: + """Generate firewalld rules""" + rules = ["#!/bin/bash", "", "# Enterprise Firewall Rules - firewalld", ""] + rules.append("# Add trusted domains to firewall") + rules.append("") + + for domain in domains: + rules.append(f"# Allow {domain}") + rules.append(f"firewall-cmd --permanent --add-rich-rule='rule family=ipv4 destination address=$(dig +short {domain} | head -1) port port=443 protocol=tcp accept'") + + rules.append("") + rules.append("# Reload firewall") + rules.append("firewall-cmd --reload") + + return "\n".join(rules) + + def generate_aws_security_group(domains: List[str]) -> Dict: + """Generate AWS Security Group rules (JSON format)""" + rules = { + "SecurityGroupRules": { + "Egress": [] + } + } + + for domain in domains: + rules["SecurityGroupRules"]["Egress"].append({ + "Description": f"Allow HTTPS to {domain}", + "IpProtocol": "tcp", + "FromPort": 443, + "ToPort": 443, + "CidrIp": "0.0.0.0/0", # In practice, resolve to specific IPs + "Tags": [{ + "Key": "Domain", + "Value": domain + }] + }) + + # Add DNS + rules["SecurityGroupRules"]["Egress"].append({ + "Description": "Allow DNS", + "IpProtocol": "udp", + "FromPort": 53, + "ToPort": 53, + "CidrIp": "0.0.0.0/0" + }) + + return rules + + def generate_markdown_documentation(domains_by_category: Dict[str, List[str]]) -> str: + """Generate markdown documentation""" + md = ["# Enterprise Firewall Configuration Guide", ""] + md.append("## Overview") + md.append("") + md.append("This document provides firewall configuration guidance for enterprise-ready deployments.") + md.append("It lists trusted domains that should be whitelisted for outbound access to ensure") + md.append("proper functionality of license validation, package management, and documentation access.") + md.append("") + + md.append("## Trusted Domains by Category") + md.append("") + + all_domains = [] + for category, domains in domains_by_category.items(): + category_name = category.replace("_", " ").title() + md.append(f"### {category_name}") + md.append("") + md.append("| Domain | Purpose |") + md.append("|--------|---------|") + + for domain in domains: + all_domains.append(domain) + purpose = get_domain_purpose(domain) + md.append(f"| `{domain}` | {purpose} |") + + md.append("") + + md.append("## Implementation Examples") + md.append("") + + md.append("### iptables Example") + md.append("") + md.append("```bash") + md.append("# Allow HTTPS to trusted domain") + md.append(f"iptables -A OUTPUT -p tcp -d $(dig +short {all_domains[0]}) --dport 443 -j ACCEPT") + md.append("```") + md.append("") + + md.append("### UFW Example") + md.append("") + md.append("```bash") + md.append("# Allow HTTPS to trusted domain") + md.append(f"ufw allow out to {all_domains[0]} port 443 proto tcp") + md.append("```") + md.append("") + + md.append("### AWS Security Group Example") + md.append("") + md.append("```json") + md.append("{") + md.append(' "IpPermissions": [{') + md.append(' "IpProtocol": "tcp",') + md.append(' "FromPort": 443,') + md.append(' "ToPort": 443,') + md.append(' "IpRanges": [{"CidrIp": "0.0.0.0/0", "Description": "HTTPS to trusted domains"}]') + md.append(" }]") + md.append("}") + md.append("```") + md.append("") + + md.append("## Ports Required") + md.append("") + md.append("| Port | Protocol | Purpose |") + md.append("|------|----------|---------|") + md.append("| 443 | TCP | HTTPS (secure web access) |") + md.append("| 80 | TCP | HTTP (redirects to HTTPS) |") + md.append("| 53 | UDP/TCP | DNS resolution |") + md.append("") + + md.append("## Security Considerations") + md.append("") + md.append("1. **DNS Resolution**: Ensure DNS queries are allowed (port 53 UDP/TCP)") + md.append("2. **Certificate Validation**: HTTPS requires ability to reach certificate authorities") + md.append("3. **Dynamic IPs**: Some domains use CDNs with dynamic IPs - consider using FQDNs in rules") + md.append("4. **Regular Updates**: Review and update whitelist as services change") + md.append("5. **Logging**: Enable logging for blocked connections to identify missing rules") + md.append("") + + md.append("## Compliance Notes") + md.append("") + md.append("- All listed domains provide read-only access to public information") + md.append("- License providers enable GPL compliance verification") + md.append("- Package registries support dependency security scanning") + md.append("- No authentication credentials are transmitted to these domains") + md.append("") + + return "\n".join(md) + + def get_domain_purpose(domain: str) -> str: + """Get human-readable purpose for a domain""" + purposes = { + "www.gnu.org": "GNU licenses and documentation", + "opensource.org": "Open Source Initiative resources", + "choosealicense.com": "GitHub license selection tool", + "spdx.org": "Software Package Data Exchange identifiers", + "creativecommons.org": "Creative Commons licenses", + "apache.org": "Apache Software Foundation licenses", + "fsf.org": "Free Software Foundation resources", + "semver.org": "Semantic versioning specification", + "keepachangelog.com": "Changelog format standards", + "conventionalcommits.org": "Commit message conventions", + "github.com": "GitHub platform access", + "api.github.com": "GitHub API access", + "docs.github.com": "GitHub documentation", + "raw.githubusercontent.com": "GitHub raw content access", + "npmjs.com": "npm package registry", + "pypi.org": "Python Package Index", + "packagist.org": "PHP Composer package registry", + "rubygems.org": "Ruby gems registry", + "joomla.org": "Joomla CMS platform", + "php.net": "PHP documentation and downloads", + "dolibarr.org": "Dolibarr ERP/CRM platform", + } + return purposes.get(domain, "Trusted resource") + + def main(): + # Use inputs if provided (manual dispatch), otherwise use defaults (auto-run) + firewall_type = "${{ github.event.inputs.firewall_type }}" or "all" + output_format = "${{ github.event.inputs.output_format }}" or "markdown" + + print(f"Running in {'manual' if '${{ github.event.inputs.firewall_type }}' else 'automatic'} mode") + print(f"Firewall type: {firewall_type}") + print(f"Output format: {output_format}") + print("") + + # Collect all domains + all_domains = [] + for domains in TRUSTED_DOMAINS.values(): + all_domains.extend(domains) + + # Remove duplicates and sort + all_domains = sorted(set(all_domains)) + + print(f"Generating firewall rules for {len(all_domains)} trusted domains...") + print("") + + # Exclude SFTP server from HTTPS rule generation (different port) + https_domains = [d for d in all_domains if d != SFTP_HOST] + + # Generate based on firewall type + if firewall_type in ["iptables", "all"]: + rules = generate_iptables_rules(https_domains) + if SFTP_HOST: + rules += "\n# โ”€โ”€ SFTP Deployment Server โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n" + rules += generate_sftp_iptables_rules(SFTP_HOST, SFTP_PORT) + with open("firewall-rules-iptables.sh", "w") as f: + f.write(rules) + print("โœ“ Generated iptables rules: firewall-rules-iptables.sh") + + if firewall_type in ["ufw", "all"]: + rules = generate_ufw_rules(https_domains) + if SFTP_HOST: + rules += "\n# โ”€โ”€ SFTP Deployment Server โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n" + rules += generate_sftp_ufw_rules(SFTP_HOST, SFTP_PORT) + with open("firewall-rules-ufw.sh", "w") as f: + f.write(rules) + print("โœ“ Generated UFW rules: firewall-rules-ufw.sh") + + if firewall_type in ["firewalld", "all"]: + rules = generate_firewalld_rules(https_domains) + if SFTP_HOST: + rules += "\n# โ”€โ”€ SFTP Deployment Server โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n" + rules += generate_sftp_firewalld_rules(SFTP_HOST, SFTP_PORT) + with open("firewall-rules-firewalld.sh", "w") as f: + f.write(rules) + print("โœ“ Generated firewalld rules: firewall-rules-firewalld.sh") + + if firewall_type in ["aws-security-group", "all"]: + rules = generate_aws_security_group(all_domains) + with open("firewall-rules-aws-sg.json", "w") as f: + json.dump(rules, f, indent=2) + print("โœ“ Generated AWS Security Group rules: firewall-rules-aws-sg.json") + + if output_format in ["yaml", "all"]: + with open("trusted-domains.yml", "w") as f: + yaml.dump(TRUSTED_DOMAINS, f, default_flow_style=False) + print("โœ“ Generated YAML domain list: trusted-domains.yml") + + if output_format in ["json", "all"]: + with open("trusted-domains.json", "w") as f: + json.dump(TRUSTED_DOMAINS, f, indent=2) + print("โœ“ Generated JSON domain list: trusted-domains.json") + + if output_format in ["markdown", "all"]: + md = generate_markdown_documentation(TRUSTED_DOMAINS) + with open("FIREWALL_CONFIGURATION.md", "w") as f: + f.write(md) + print("โœ“ Generated documentation: FIREWALL_CONFIGURATION.md") + + print("") + print("Domain Categories:") + for category, domains in TRUSTED_DOMAINS.items(): + print(f" - {category}: {len(domains)} domains") + + print("") + print("Total unique domains: ", len(all_domains)) + + if __name__ == "__main__": + main() + PYTHON_EOF + + chmod +x generate_firewall_config.py + pip install PyYAML + python3 generate_firewall_config.py + + - name: Upload Firewall Configuration Artifacts + uses: actions/upload-artifact@v6 + with: + name: firewall-configurations + path: | + firewall-rules-*.sh + firewall-rules-*.json + trusted-domains.* + FIREWALL_CONFIGURATION.md + retention-days: 90 + + - name: Display Summary + run: | + echo "## Firewall Configuration" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "**Mode**: Manual Execution" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Firewall rules have been generated for enterprise-ready deployments." >> $GITHUB_STEP_SUMMARY + else + echo "**Mode**: Automatic Execution (Coding Agent Active)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "This workflow ran automatically because a coding agent (GitHub Copilot) is active." >> $GITHUB_STEP_SUMMARY + echo "Firewall configuration has been validated for the coding agent environment." >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Files Generated" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if ls firewall-rules-* trusted-domains.* FIREWALL_CONFIGURATION.md 2>/dev/null; then + ls -lh firewall-rules-* trusted-domains.* FIREWALL_CONFIGURATION.md 2>/dev/null | awk '{print "- " $9 " (" $5 ")"}' >> $GITHUB_STEP_SUMMARY + else + echo "- Documentation generated" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "### Download Artifacts" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Download the generated firewall configurations from the workflow artifacts." >> $GITHUB_STEP_SUMMARY + else + echo "### Trusted Domains Active" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "The coding agent has access to:" >> $GITHUB_STEP_SUMMARY + echo "- License providers (GPL, OSI, SPDX, Apache, etc.)" >> $GITHUB_STEP_SUMMARY + echo "- Package registries (npm, PyPI, Packagist, RubyGems)" >> $GITHUB_STEP_SUMMARY + echo "- Documentation sources (GitHub, Joomla, Dolibarr, PHP)" >> $GITHUB_STEP_SUMMARY + echo "- Standards organizations (W3C, IETF, JSON Schema)" >> $GITHUB_STEP_SUMMARY + fi + +# Usage Instructions: +# +# This workflow runs in two modes: +# +# 1. AUTOMATIC MODE (Coding Agent): +# - Triggers when coding agent branches (copilot/**, agent/**) are pushed or PR'd +# - Validates firewall configuration for the coding agent environment +# - Documents accessible domains for compliance +# - Ensures license sources and package registries are available +# +# 2. MANUAL MODE (Enterprise Configuration): +# - Manually trigger from the Actions tab +# - Select desired firewall type and output format +# - Download generated artifacts +# - Apply firewall rules to your enterprise environment +# +# Configuration: +# - Trusted domains are sourced from .github/copilot.yml +# - Modify copilot.yml to add/remove trusted domains +# - Changes automatically propagate to firewall rules +# +# Important Notes: +# - Review generated rules before applying to production +# - Some domains may use CDNs with dynamic IPs +# - Consider using FQDN-based rules where supported +# - Test thoroughly in staging environment first +# - Monitor logs for blocked connections +# - Update rules as domains/services change diff --git a/.gitea/workflows/repository-cleanup.yml b/.gitea/workflows/repository-cleanup.yml new file mode 100644 index 0000000..ea9219d --- /dev/null +++ b/.gitea/workflows/repository-cleanup.yml @@ -0,0 +1,525 @@ +# Copyright (C) 2026 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: GitHub.Workflow +# INGROUP: MokoStandards.Maintenance +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /templates/workflows/shared/repository-cleanup.yml.template +# VERSION: 04.06.00 +# BRIEF: Recurring repository maintenance โ€” labels, branches, workflows, logs, doc indexes +# NOTE: Synced via bulk-repo-sync to .github/workflows/repository-cleanup.yml in all governed repos. +# Runs on the 1st and 15th of each month at 6:00 AM UTC, and on manual dispatch. + +name: Repository Cleanup + +on: + schedule: + - cron: '0 6 1,15 * *' + workflow_dispatch: + inputs: + reset_labels: + description: 'Delete ALL existing labels and recreate the standard set' + type: boolean + default: false + clean_branches: + description: 'Delete old chore/sync-mokostandards-* branches' + type: boolean + default: true + clean_workflows: + description: 'Delete orphaned workflow runs (cancelled, stale)' + type: boolean + default: true + clean_logs: + description: 'Delete workflow run logs older than 30 days' + type: boolean + default: true + fix_templates: + description: 'Strip copyright comment blocks from issue templates' + type: boolean + default: true + rebuild_indexes: + description: 'Rebuild docs/ index files' + type: boolean + default: true + delete_closed_issues: + description: 'Delete issues that have been closed for more than 30 days' + type: boolean + default: false + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +permissions: + contents: write + issues: write + actions: write + +jobs: + cleanup: + name: Repository Maintenance + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.GH_TOKEN || github.token }} + fetch-depth: 0 + + - name: Check actor permission + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + ACTOR="${{ github.actor }}" + # Schedule triggers use github-actions[bot] + if [ "${{ github.event_name }}" = "schedule" ]; then + echo "โœ… Scheduled run โ€” authorized" + exit 0 + fi + AUTHORIZED_USERS="jmiller-moko github-actions[bot]" + for user in $AUTHORIZED_USERS; do + if [ "$ACTOR" = "$user" ]; then + echo "โœ… ${ACTOR} authorized" + exit 0 + fi + done + PERMISSION=$(gh api "repos/${{ github.repository }}/collaborators/${ACTOR}/permission" \ + --jq '.permission' 2>/dev/null) + case "$PERMISSION" in + admin|maintain) echo "โœ… ${ACTOR} has ${PERMISSION}" ;; + *) echo "โŒ Admin or maintain required"; exit 1 ;; + esac + + # โ”€โ”€ Determine which tasks to run โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + # On schedule: run all tasks with safe defaults (labels NOT reset) + # On dispatch: use input toggles + - name: Set task flags + id: tasks + run: | + if [ "${{ github.event_name }}" = "schedule" ]; then + echo "reset_labels=false" >> $GITHUB_OUTPUT + echo "clean_branches=true" >> $GITHUB_OUTPUT + echo "clean_workflows=true" >> $GITHUB_OUTPUT + echo "clean_logs=true" >> $GITHUB_OUTPUT + echo "fix_templates=true" >> $GITHUB_OUTPUT + echo "rebuild_indexes=true" >> $GITHUB_OUTPUT + echo "delete_closed_issues=false" >> $GITHUB_OUTPUT + else + echo "reset_labels=${{ inputs.reset_labels }}" >> $GITHUB_OUTPUT + echo "clean_branches=${{ inputs.clean_branches }}" >> $GITHUB_OUTPUT + echo "clean_workflows=${{ inputs.clean_workflows }}" >> $GITHUB_OUTPUT + echo "clean_logs=${{ inputs.clean_logs }}" >> $GITHUB_OUTPUT + echo "fix_templates=${{ inputs.fix_templates }}" >> $GITHUB_OUTPUT + echo "rebuild_indexes=${{ inputs.rebuild_indexes }}" >> $GITHUB_OUTPUT + echo "delete_closed_issues=${{ inputs.delete_closed_issues }}" >> $GITHUB_OUTPUT + fi + + # โ”€โ”€ DELETE RETIRED WORKFLOWS (always runs) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + - name: Delete retired workflow files + run: | + echo "## ๐Ÿ—‘๏ธ Retired Workflow Cleanup" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + RETIRED=( + ".github/workflows/build.yml" + ".github/workflows/code-quality.yml" + ".github/workflows/release-cycle.yml" + ".github/workflows/release-pipeline.yml" + ".github/workflows/branch-cleanup.yml" + ".github/workflows/auto-update-changelog.yml" + ".github/workflows/enterprise-issue-manager.yml" + ".github/workflows/flush-actions-cache.yml" + ".github/workflows/mokostandards-script-runner.yml" + ".github/workflows/unified-ci.yml" + ".github/workflows/unified-platform-testing.yml" + ".github/workflows/reusable-build.yml" + ".github/workflows/reusable-ci-validation.yml" + ".github/workflows/reusable-deploy.yml" + ".github/workflows/reusable-php-quality.yml" + ".github/workflows/reusable-platform-testing.yml" + ".github/workflows/reusable-project-detector.yml" + ".github/workflows/reusable-release.yml" + ".github/workflows/reusable-script-executor.yml" + ".github/workflows/rebuild-docs-indexes.yml" + ".github/workflows/setup-project-v2.yml" + ".github/workflows/sync-docs-to-project.yml" + ".github/workflows/release.yml" + ".github/workflows/sync-changelogs.yml" + ".github/workflows/version_branch.yml" + "update.json" + ".github/workflows/auto-version-branch.yml" + ".github/workflows/publish-to-mokodolibarr.yml" + ".github/workflows/ci.yml" + ".github/workflows/deploy-rs.yml" + "sftp-config.json" + "sftp-config.json.template" + "scripts/sftp-config" + ) + + DELETED=0 + for wf in "${RETIRED[@]}"; do + if [ -f "$wf" ]; then + git rm "$wf" 2>/dev/null || rm -f "$wf" + echo " Deleted: \`$(basename $wf)\`" >> $GITHUB_STEP_SUMMARY + DELETED=$((DELETED+1)) + fi + done + + if [ "$DELETED" -gt 0 ]; then + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add -A + git commit -m "chore: delete ${DELETED} retired workflow file(s) [skip ci]" \ + --author="github-actions[bot] " + git push + echo "โœ… ${DELETED} retired workflow(s) deleted" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… No retired workflows found" >> $GITHUB_STEP_SUMMARY + fi + + # โ”€โ”€ LABEL RESET โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + - name: Reset labels to standard set + if: steps.tasks.outputs.reset_labels == 'true' + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + REPO="${{ github.repository }}" + echo "## ๐Ÿท๏ธ Label Reset" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + gh api "repos/${REPO}/labels?per_page=100" --paginate --jq '.[].name' | while read -r label; do + ENCODED=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$label', safe=''))") + gh api -X DELETE "repos/${REPO}/labels/${ENCODED}" --silent 2>/dev/null || true + done + + while IFS='|' read -r name color description; do + [ -z "$name" ] && continue + gh api "repos/${REPO}/labels" \ + -f name="$name" -f color="$color" -f description="$description" \ + --silent 2>/dev/null || true + done << 'LABELS' + joomla|7F52FF|Joomla extension or component + dolibarr|FF6B6B|Dolibarr module or extension + generic|808080|Generic project or library + php|4F5D95|PHP code changes + javascript|F7DF1E|JavaScript code changes + typescript|3178C6|TypeScript code changes + python|3776AB|Python code changes + css|1572B6|CSS/styling changes + html|E34F26|HTML template changes + documentation|0075CA|Documentation changes + ci-cd|000000|CI/CD pipeline changes + docker|2496ED|Docker configuration changes + tests|00FF00|Test suite changes + security|FF0000|Security-related changes + dependencies|0366D6|Dependency updates + config|F9D0C4|Configuration file changes + build|FFA500|Build system changes + automation|8B4513|Automated processes or scripts + mokostandards|B60205|MokoStandards compliance + needs-review|FBCA04|Awaiting code review + work-in-progress|D93F0B|Work in progress, not ready for merge + breaking-change|D73A4A|Breaking API or functionality change + priority: critical|B60205|Critical priority, must be addressed immediately + priority: high|D93F0B|High priority + priority: medium|FBCA04|Medium priority + priority: low|0E8A16|Low priority + type: bug|D73A4A|Something isn't working + type: feature|A2EEEF|New feature or request + type: enhancement|84B6EB|Enhancement to existing feature + type: refactor|F9D0C4|Code refactoring + type: chore|FEF2C0|Maintenance tasks + type: version|0E8A16|Version-related change + status: pending|FBCA04|Pending action or decision + status: in-progress|0E8A16|Currently being worked on + status: blocked|B60205|Blocked by another issue or dependency + status: on-hold|D4C5F9|Temporarily on hold + status: wontfix|FFFFFF|This will not be worked on + size/xs|C5DEF5|Extra small change (1-10 lines) + size/s|6FD1E2|Small change (11-30 lines) + size/m|F9DD72|Medium change (31-100 lines) + size/l|FFA07A|Large change (101-300 lines) + size/xl|FF6B6B|Extra large change (301-1000 lines) + size/xxl|B60205|Extremely large change (1000+ lines) + health: excellent|0E8A16|Health score 90-100 + health: good|FBCA04|Health score 70-89 + health: fair|FFA500|Health score 50-69 + health: poor|FF6B6B|Health score below 50 + standards-update|B60205|MokoStandards sync update + standards-drift|FBCA04|Repository drifted from MokoStandards + sync-report|0075CA|Bulk sync run report + sync-failure|D73A4A|Bulk sync failure requiring attention + push-failure|D73A4A|File push failure requiring attention + health-check|0E8A16|Repository health check results + version-drift|FFA500|Version mismatch detected + deploy-failure|CC0000|Automated deploy failure tracking + template-validation-failure|D73A4A|Template workflow validation failure + version|0E8A16|Version bump or release + LABELS + + echo "โœ… Standard labels created" >> $GITHUB_STEP_SUMMARY + + # โ”€โ”€ BRANCH CLEANUP โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + - name: Delete old sync branches + if: steps.tasks.outputs.clean_branches == 'true' + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + REPO="${{ github.repository }}" + CURRENT="chore/sync-mokostandards-v04.05" + echo "## ๐ŸŒฟ Branch Cleanup" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + FOUND=false + gh api "repos/${REPO}/branches?per_page=100" --jq '.[].name' | \ + grep "^chore/sync-mokostandards" | \ + grep -v "^${CURRENT}$" | while read -r branch; do + gh pr list --repo "$REPO" --head "$branch" --state open --json number --jq '.[].number' 2>/dev/null | while read -r pr; do + gh pr close "$pr" --repo "$REPO" --comment "Superseded by \`${CURRENT}\`" 2>/dev/null || true + echo " Closed PR #${pr}" >> $GITHUB_STEP_SUMMARY + done + gh api -X DELETE "repos/${REPO}/git/refs/heads/${branch}" --silent 2>/dev/null || true + echo " Deleted: \`${branch}\`" >> $GITHUB_STEP_SUMMARY + FOUND=true + done + + if [ "$FOUND" != "true" ]; then + echo "โœ… No old sync branches found" >> $GITHUB_STEP_SUMMARY + fi + + # โ”€โ”€ WORKFLOW RUN CLEANUP โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + - name: Clean up workflow runs + if: steps.tasks.outputs.clean_workflows == 'true' + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + REPO="${{ github.repository }}" + echo "## ๐Ÿ”„ Workflow Run Cleanup" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + DELETED=0 + # Delete cancelled and stale workflow runs + for status in cancelled stale; do + gh api "repos/${REPO}/actions/runs?status=${status}&per_page=100" \ + --jq '.workflow_runs[].id' 2>/dev/null | while read -r run_id; do + gh api -X DELETE "repos/${REPO}/actions/runs/${run_id}" --silent 2>/dev/null || true + DELETED=$((DELETED+1)) + done + done + + echo "โœ… Cleaned cancelled/stale workflow runs" >> $GITHUB_STEP_SUMMARY + + # โ”€โ”€ LOG CLEANUP โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + - name: Delete old workflow run logs + if: steps.tasks.outputs.clean_logs == 'true' + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + REPO="${{ github.repository }}" + CUTOFF=$(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-30d +%Y-%m-%dT%H:%M:%SZ) + echo "## ๐Ÿ“‹ Log Cleanup" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Deleting logs older than: ${CUTOFF}" >> $GITHUB_STEP_SUMMARY + + DELETED=0 + gh api "repos/${REPO}/actions/runs?created=<${CUTOFF}&per_page=100" \ + --jq '.workflow_runs[].id' 2>/dev/null | while read -r run_id; do + gh api -X DELETE "repos/${REPO}/actions/runs/${run_id}/logs" --silent 2>/dev/null || true + DELETED=$((DELETED+1)) + done + + echo "โœ… Cleaned old workflow run logs" >> $GITHUB_STEP_SUMMARY + + # โ”€โ”€ ISSUE TEMPLATE FIX โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + - name: Strip copyright headers from issue templates + if: steps.tasks.outputs.fix_templates == 'true' + run: | + echo "## ๐Ÿ“‹ Issue Template Cleanup" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + FIXED=0 + for f in .github/ISSUE_TEMPLATE/*.md; do + [ -f "$f" ] || continue + if grep -q '^$/d' "$f" + echo " Cleaned: \`$(basename $f)\`" >> $GITHUB_STEP_SUMMARY + FIXED=$((FIXED+1)) + fi + done + + if [ "$FIXED" -gt 0 ]; then + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add .github/ISSUE_TEMPLATE/ + git commit -m "fix: strip copyright comment blocks from issue templates [skip ci]" \ + --author="github-actions[bot] " + git push + echo "โœ… ${FIXED} template(s) cleaned and committed" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… No templates need cleaning" >> $GITHUB_STEP_SUMMARY + fi + + # โ”€โ”€ REBUILD DOC INDEXES โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + - name: Rebuild docs/ index files + if: steps.tasks.outputs.rebuild_indexes == 'true' + run: | + echo "## ๐Ÿ“š Documentation Index Rebuild" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ ! -d "docs" ]; then + echo "โญ๏ธ No docs/ directory โ€” skipping" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + UPDATED=0 + # Generate index.md for each docs/ subdirectory + find docs -type d | while read -r dir; do + INDEX="${dir}/index.md" + FILES=$(find "$dir" -maxdepth 1 -name "*.md" ! -name "index.md" -printf "- [%f](./%f)\n" 2>/dev/null | sort) + if [ -z "$FILES" ]; then + continue + fi + + cat > "$INDEX" << INDEXEOF + # $(basename "$dir") + + ## Documents + + ${FILES} + + --- + *Auto-generated by repository-cleanup workflow* + INDEXEOF + # Dedent + sed -i 's/^ //' "$INDEX" + UPDATED=$((UPDATED+1)) + done + + if [ "$UPDATED" -gt 0 ]; then + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add docs/ + if ! git diff --cached --quiet; then + git commit -m "docs: rebuild documentation indexes [skip ci]" \ + --author="github-actions[bot] " + git push + echo "โœ… ${UPDATED} index file(s) rebuilt and committed" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… All indexes already up to date" >> $GITHUB_STEP_SUMMARY + fi + else + echo "โœ… No indexes to rebuild" >> $GITHUB_STEP_SUMMARY + fi + + # โ”€โ”€ VERSION DRIFT DETECTION โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + - name: Check for version drift + run: | + echo "## ๐Ÿ“ฆ Version Drift Check" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ ! -f "README.md" ]; then + echo "โญ๏ธ No README.md โ€” skipping" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md 2>/dev/null | head -1) + if [ -z "$README_VERSION" ]; then + echo "โš ๏ธ No VERSION found in README.md FILE INFORMATION block" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + echo "**README version:** \`${README_VERSION}\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + DRIFT=0 + CHECKED=0 + + # Check all files with FILE INFORMATION blocks + while IFS= read -r -d '' file; do + FILE_VERSION=$(grep -oP '^\s*\*?\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' "$file" 2>/dev/null | head -1) + [ -z "$FILE_VERSION" ] && continue + CHECKED=$((CHECKED+1)) + if [ "$FILE_VERSION" != "$README_VERSION" ]; then + echo " โš ๏ธ \`${file}\`: \`${FILE_VERSION}\` (expected \`${README_VERSION}\`)" >> $GITHUB_STEP_SUMMARY + DRIFT=$((DRIFT+1)) + fi + done < <(find . -maxdepth 4 -type f \( -name "*.php" -o -name "*.md" -o -name "*.yml" \) ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" -print0 2>/dev/null) + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "$DRIFT" -gt 0 ]; then + echo "โš ๏ธ **${DRIFT}** file(s) out of ${CHECKED} have version drift" >> $GITHUB_STEP_SUMMARY + echo "Run \`sync-version-on-merge\` workflow or update manually" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… All ${CHECKED} file(s) match README version \`${README_VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + + # โ”€โ”€ PROTECT CUSTOM WORKFLOWS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + - name: Ensure custom workflow directory exists + run: | + echo "## ๐Ÿ”ง Custom Workflows" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ ! -d ".github/workflows/custom" ]; then + mkdir -p .github/workflows/custom + cat > .github/workflows/custom/README.md << 'CWEOF' + # Custom Workflows + + Place repo-specific workflows here. Files in this directory are: + - **Never overwritten** by MokoStandards bulk sync + - **Never deleted** by the repository-cleanup workflow + - Safe for custom CI, notifications, or repo-specific automation + + Synced workflows live in `.github/workflows/` (parent directory). + CWEOF + sed -i 's/^ //' .github/workflows/custom/README.md + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add .github/workflows/custom/ + if ! git diff --cached --quiet; then + git commit -m "chore: create .github/workflows/custom/ for repo-specific workflows [skip ci]" \ + --author="github-actions[bot] " + git push + echo "โœ… Created \`.github/workflows/custom/\` directory" >> $GITHUB_STEP_SUMMARY + fi + else + CUSTOM_COUNT=$(find .github/workflows/custom -name "*.yml" -o -name "*.yaml" 2>/dev/null | wc -l) + echo "โœ… Custom workflow directory exists (${CUSTOM_COUNT} workflow(s))" >> $GITHUB_STEP_SUMMARY + fi + + # โ”€โ”€ DELETE CLOSED ISSUES โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + - name: Delete old closed issues + if: steps.tasks.outputs.delete_closed_issues == 'true' + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + REPO="${{ github.repository }}" + CUTOFF=$(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-30d +%Y-%m-%dT%H:%M:%SZ) + echo "## ๐Ÿ—‘๏ธ Closed Issue Cleanup" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Deleting issues closed before: ${CUTOFF}" >> $GITHUB_STEP_SUMMARY + + DELETED=0 + gh api "repos/${REPO}/issues?state=closed&since=1970-01-01T00:00:00Z&per_page=100&sort=updated&direction=asc" \ + --jq ".[] | select(.closed_at < \"${CUTOFF}\") | .number" 2>/dev/null | while read -r num; do + # Lock and close with "not_planned" to mark as cleaned up + gh api "repos/${REPO}/issues/${num}/lock" -X PUT -f lock_reason="resolved" --silent 2>/dev/null || true + echo " Locked issue #${num}" >> $GITHUB_STEP_SUMMARY + DELETED=$((DELETED+1)) + done + + if [ "$DELETED" -eq 0 ] 2>/dev/null; then + echo "โœ… No old closed issues found" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… Locked ${DELETED} old closed issue(s)" >> $GITHUB_STEP_SUMMARY + fi + + - name: Summary + if: always() + run: | + echo "" >> $GITHUB_STEP_SUMMARY + echo "---" >> $GITHUB_STEP_SUMMARY + echo "*Run by @${{ github.actor }} โ€” trigger: ${{ github.event_name }}*" >> $GITHUB_STEP_SUMMARY diff --git a/.gitea/workflows/standards-compliance.yml b/.gitea/workflows/standards-compliance.yml new file mode 100644 index 0000000..79aaedd --- /dev/null +++ b/.gitea/workflows/standards-compliance.yml @@ -0,0 +1,2614 @@ +# Copyright (C) 2026 Moko Consulting +# SPDX-License-Identifier: GPL-3.0-or-later +# FILE INFORMATION +# DEFGROUP: GitHub.Workflow +# INGROUP: MokoStandards.Compliance +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /.github/workflows/standards-compliance.yml +# VERSION: 04.06.00 +# BRIEF: MokoStandards compliance validation workflow +# NOTE: Validates repository structure, documentation, and coding standards + +name: Standards Compliance + +# โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— +# โ•‘ MOKOSTANDARDS COMPLIANCE WORKFLOW โ•‘ +# โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ +# โ•‘ โ•‘ +# โ•‘ 28 checks across 4 priority tiers: โ•‘ +# โ•‘ โ•‘ +# โ•‘ TIER 1 โ€” CRITICAL (must pass) โ•‘ +# โ•‘ secret-scanning, license-compliance, repository-structure, โ•‘ +# โ•‘ coding-standards, version-consistency โ•‘ +# โ•‘ โ•‘ +# โ•‘ TIER 2 โ€” IMPORTANT (should pass) โ•‘ +# โ•‘ workflow-validation, documentation-quality, readme-completeness, โ•‘ +# โ•‘ git-hygiene, script-integrity โ•‘ +# โ•‘ โ•‘ +# โ•‘ TIER 3 โ€” QUALITY (code metrics) โ•‘ +# โ•‘ line-length, file-naming, insecure-patterns, complexity, โ•‘ +# โ•‘ duplication, dead-code โ•‘ +# โ•‘ โ•‘ +# โ•‘ TIER 4 โ€” SUPPLEMENTARY (informational) โ•‘ +# โ•‘ file-size, binary, todo, deps, links, api-docs, accessibility, โ•‘ +# โ•‘ performance, enterprise, health, terraform โ•‘ +# โ•‘ โ•‘ +# โ•‘ File size: warning >15MB, critical >20MB โ•‘ +# โ•‘ Exempt: .mmdb, .woff2, .woff, .ttf, .otf โ•‘ +# โ•‘ โ•‘ +# โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +env: + WORKFLOW_VERSION: "04.04.01" + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +# MokoStandards Policy Compliance: +# - File formatting: Enforces organizational coding standards +# - Reference: docs/policy/file-formatting.md + +# โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +# โ”‚ WORKFLOW FLOW DIAGRAM โ”‚ +# โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +# +# TRIGGER: Push/PR to main/dev/rc branches +# โ”‚ +# โ–ผ +# โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +# โ”‚ PARALLEL VALIDATION CHECKS โ”‚ +# โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +# โ”‚ +# โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +# โ–ผ โ–ผ โ–ผ โ–ผ โ–ผ +# โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +# โ”‚Repository โ”‚File Header โ”‚Code Styleโ”‚ โ”‚ Docs โ”‚ โ”‚ License โ”‚ +# โ”‚Structureโ”‚ โ”‚ Validationโ”‚ โ”‚ Check โ”‚ โ”‚ Check โ”‚ โ”‚ Check โ”‚ +# โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +# โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +# โ–ผ โ–ผ โ–ผ โ–ผ โ–ผ +# โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +# โ”‚ Check โ”‚ โ”‚ Verify โ”‚ โ”‚ Run โ”‚ โ”‚ Check โ”‚ โ”‚ Verify โ”‚ +# โ”‚Required โ”‚ โ”‚Copyright โ”‚ โ”‚ Linters โ”‚ โ”‚README โ”‚ โ”‚SPDX-ID โ”‚ +# โ”‚ Dirs โ”‚ โ”‚ Header โ”‚ โ”‚(Python, โ”‚ โ”‚ Exists โ”‚ โ”‚ Present โ”‚ +# โ”‚ โ”‚ โ”‚ Format โ”‚ โ”‚PHP,YAML) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +# โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +# โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +# โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +# โ”‚ +# โ–ผ +# โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +# โ”‚ All Checks Pass?โ”‚ +# โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +# โ”‚ โ”‚ +# YES โ”‚ โ”‚ NO +# โ–ผ โ–ผ +# โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +# โ”‚ SUCCESS โ”‚ โ”‚ CREATE ISSUE โ”‚ +# โ”‚ Summary โ”‚ โ”‚ with Failure โ”‚ +# โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ Details โ”‚ +# โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +on: + push: + branches: [main, dev/**, rc/**, version/**] + pull_request: + branches: [main, dev/**, rc/**] + workflow_dispatch: + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + # TIER 1 โ€” CRITICAL (must pass, blocks merge) + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + secret-scanning: + name: Secret Scanning + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Scan for Secrets + run: | + set -x + echo "## ๐Ÿ”’ Secret Scanning" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Scanning for hardcoded secrets and credentials." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Define secret patterns + VIOLATIONS=0 + + # Check for common secret patterns + echo "### Secret Patterns" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Helper: scan with a pattern, show results with file:line, return count + scan_pattern() { + local label="$1" icon="$2" tmpfile="$3" + local count=0 + if [ -f "$tmpfile" ]; then + count=$(wc -l < "$tmpfile") + fi + if [ "$count" -gt 0 ]; then + echo "${icon} **${label}**: ${count} finding(s)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View locations" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| File | Line | Match |" >> $GITHUB_STEP_SUMMARY + echo "|------|------|-------|" >> $GITHUB_STEP_SUMMARY + head -20 "$tmpfile" | while IFS= read -r line; do + FILE=$(echo "$line" | cut -d: -f1 | sed 's|^\./||') + LINENO=$(echo "$line" | cut -d: -f2) + MATCH=$(echo "$line" | cut -d: -f3- | head -c 80 | sed 's/|/\\|/g') + echo "| \`${FILE}\` | ${LINENO} | \`${MATCH}\` |" >> $GITHUB_STEP_SUMMARY + done + if [ "$count" -gt 20 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "*... and $((count - 20)) more*" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + VIOLATIONS=$((VIOLATIONS + count)) + fi + } + + # Pattern 1: password/secret assignments + grep -r -n -E "(password|passwd|pwd|secret|api[_-]?key|token).*=.*['\"]" . \ + --include="*.php" --include="*.py" --include="*.js" --include="*.ts" \ + --exclude-dir=".git" --exclude-dir="vendor" --exclude-dir="node_modules" 2>/dev/null | \ + grep -v -E '(test|example|sample|getenv|getString|getArgument|config\[|/\.\*/|^\s*//|^\s*\*|CREDENTIAL_PATTERNS|SecurityValidator|SECRET_PATTERN|===|!==|ApiClient|str_contains|gen_wrappers)' | \ + grep -v "= ''" | grep -v '= ""' | grep -v '\$this->config' | \ + grep -v 'type="password"' | grep -v 'type="text"' | grep -v 'name="password"' | grep -v 'name="secretkey"' | \ + grep -v '/dev/null > /tmp/secrets2.txt || true + scan_pattern "Private keys" "โŒ" /tmp/secrets2.txt + + # Pattern 3: AWS keys + grep -r -n -E "AKIA[0-9A-Z]{16}" . \ + --include="*.php" --include="*.py" --include="*.js" --include="*.txt" --include="*.env" \ + --exclude-dir=".git" --exclude-dir="vendor" --exclude-dir="node_modules" 2>/dev/null > /tmp/secrets3.txt || true + scan_pattern "AWS access keys" "โŒ" /tmp/secrets3.txt + + # Pattern 4: GitHub tokens + grep -r -n -E "gh[ps]_[a-zA-Z0-9]{36}" . \ + --include="*.php" --include="*.py" --include="*.js" --include="*.txt" --include="*.env" \ + --exclude-dir=".git" --exclude-dir="vendor" --exclude-dir="node_modules" 2>/dev/null > /tmp/secrets4.txt || true + scan_pattern "GitHub tokens" "โŒ" /tmp/secrets4.txt + + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$VIOLATIONS" -gt 0 ]; then + echo "**Total Violations**: $VIOLATIONS" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View detected secrets (file paths only)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + cat /tmp/secrets*.txt 2>/dev/null | cut -d: -f1 | sort -u >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Action Required**: Remove hardcoded secrets immediately!" >> $GITHUB_STEP_SUMMARY + echo "Use environment variables or secrets management instead." >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "โœ… No hardcoded secrets detected" >> $GITHUB_STEP_SUMMARY + fi + + license-compliance: + name: License Header Validation + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check SPDX Headers + run: | + set -x + echo "### SPDX License Header Check" >> $GITHUB_STEP_SUMMARY + + # Count source files with and without SPDX headers + TOTAL_PHP=0 + WITH_SPDX_PHP=0 + + if find . -name "*.php" -type f ! -path "./vendor/*" | head -1 | grep -q .; then + TOTAL_PHP=$(find . -name "*.php" -type f ! -path "./vendor/*" | wc -l) + WITH_SPDX_PHP=$(find . -name "*.php" -type f ! -path "./vendor/*" -exec grep -l "SPDX-License-Identifier" {} \; | wc -l) + fi + + if [ "$TOTAL_PHP" -gt 0 ]; then + PERCENT=$((WITH_SPDX_PHP * 100 / TOTAL_PHP)) + echo "- PHP files: $WITH_SPDX_PHP/$TOTAL_PHP ($PERCENT%) with SPDX headers" >> $GITHUB_STEP_SUMMARY + + if [ "$PERCENT" -lt 80 ]; then + echo "โš ๏ธ Less than 80% of PHP files have SPDX headers" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… Good SPDX header coverage" >> $GITHUB_STEP_SUMMARY + fi + fi + + - name: Validate License File + run: | + set -x + echo "" >> $GITHUB_STEP_SUMMARY + echo "### License File Validation" >> $GITHUB_STEP_SUMMARY + + if [ ! -f "LICENSE" ]; then + echo "โŒ LICENSE file not found" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### โŒ Validation Failed: LICENSE File Missing" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Error:** LICENSE file is required for all MokoStandards-compliant repositories" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Add LICENSE file with appropriate open-source license (GPL-3.0-or-later recommended)" >> $GITHUB_STEP_SUMMARY + echo "" + echo "โŒ ERROR: LICENSE file not found - This is a critical requirement" + exit 1 + fi + + # Check license type + if grep -qi "GNU GENERAL PUBLIC LICENSE" LICENSE; then + VERSION=$(grep -i "Version 3" LICENSE || echo "") + if [ -n "$VERSION" ]; then + echo "โœ… GPL-3.0-or-later license detected" >> $GITHUB_STEP_SUMMARY + else + echo "โš ๏ธ GPL license detected but version unclear" >> $GITHUB_STEP_SUMMARY + fi + elif grep -qi "MIT License" LICENSE; then + echo "โœ… MIT license detected" >> $GITHUB_STEP_SUMMARY + elif grep -qi "Apache License" LICENSE; then + echo "โœ… Apache license detected" >> $GITHUB_STEP_SUMMARY + else + echo "โ„น๏ธ License type could not be automatically detected" >> $GITHUB_STEP_SUMMARY + fi + + repository-structure: + name: Repository Structure Validation + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check Required Directories + run: | + set -x + echo "## ๐Ÿ“ Repository Structure Validation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + MISSING=0 + PRESENT=0 + TOTAL=2 + + echo "### Required Directories" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Directory | Status | Files | Size | Notes |" >> $GITHUB_STEP_SUMMARY + echo "|-----------|--------|-------|------|-------|" >> $GITHUB_STEP_SUMMARY + + # Check required directories + for dir in docs .github; do + if [ -d "$dir" ]; then + FILE_COUNT=$(find "$dir" -type f 2>/dev/null | wc -l) + DIR_SIZE=$(du -sh "$dir" 2>/dev/null | cut -f1) + echo "| $dir/ | โœ… Pass | $FILE_COUNT files | $DIR_SIZE | Complete |" >> $GITHUB_STEP_SUMMARY + PRESENT=$((PRESENT + 1)) + else + echo "| $dir/ | โŒ **Missing** | - | - | **Action Required** |" >> $GITHUB_STEP_SUMMARY + MISSING=$((MISSING + 1)) + fi + done + + echo "" >> $GITHUB_STEP_SUMMARY + PERCENT=$((PRESENT * 100 / TOTAL)) + echo "**Compliance Score:** $PERCENT% ($PRESENT/$TOTAL directories present)" >> $GITHUB_STEP_SUMMARY + + if [ "$MISSING" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ๐Ÿ”ด Critical Issues: $MISSING" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Remediation Steps:**" >> $GITHUB_STEP_SUMMARY + [ ! -d "docs" ] && echo "- Create docs directory: \`mkdir docs && echo '# Documentation' > docs/README.md\`" >> $GITHUB_STEP_SUMMARY + [ ! -d ".github" ] && echo "- Create .github directory: \`mkdir -p .github/workflows\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "๐Ÿ“š Reference: [MokoStandards Repository Structure](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/docs/policy/core-structure.md)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### โŒ Validation Failed: Required Directories Missing" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Status:** Repository structure does not meet MokoStandards requirements" >> $GITHUB_STEP_SUMMARY + echo "**Missing:** $MISSING required director(y|ies)" >> $GITHUB_STEP_SUMMARY + echo "**Compliance:** $PERCENT% ($PRESENT/$TOTAL directories present)" >> $GITHUB_STEP_SUMMARY + echo "" + echo "โŒ ERROR: Required directories missing - See job summary for remediation steps" + exit 1 + fi + + - name: Check Required Files + run: | + set -x + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Required Files" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + MISSING=0 + PRESENT=0 + TOTAL=5 + + echo "| File | Status | Size | Last Modified | Notes |" >> $GITHUB_STEP_SUMMARY + echo "|------|--------|------|---------------|-------|" >> $GITHUB_STEP_SUMMARY + + # Check required files (CHANGELOG handled separately via find -iname to support src/ChangeLog.md) + for file in README.md LICENSE CONTRIBUTING.md SECURITY.md .editorconfig; do + if [ -f "$file" ]; then + FILE_SIZE=$(wc -c < "$file" 2>/dev/null | awk '{printf "%.1f KB", $1/1024}') + LAST_MOD=$(stat -c %y "$file" 2>/dev/null | cut -d' ' -f1 || echo "Unknown") + CONTENT_CHECK="" + + # Basic content validation + case "$file" in + "README.md") + LINES=$(wc -l < "$file") + [ "$LINES" -lt 10 ] && CONTENT_CHECK="โš ๏ธ Too short" + ;; + "LICENSE") + [ $(wc -c < "$file") -lt 100 ] && CONTENT_CHECK="โš ๏ธ Incomplete?" + ;; + esac + + echo "| $file | โœ… Pass | $FILE_SIZE | $LAST_MOD | Complete $CONTENT_CHECK |" >> $GITHUB_STEP_SUMMARY + PRESENT=$((PRESENT + 1)) + else + echo "| $file | โŒ **Missing** | - | - | **Required** |" >> $GITHUB_STEP_SUMMARY + MISSING=$((MISSING + 1)) + fi + done + + echo "" >> $GITHUB_STEP_SUMMARY + PERCENT=$((PRESENT * 100 / TOTAL)) + echo "**Compliance Score:** $PERCENT% ($PRESENT/$TOTAL files present)" >> $GITHUB_STEP_SUMMARY + + if [ "$MISSING" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ๐Ÿ”ด Critical Issues: $MISSING" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Remediation Steps:**" >> $GITHUB_STEP_SUMMARY + [ ! -f "README.md" ] && echo "- Create README.md: Use [template](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/templates/docs/required/README.md)" >> $GITHUB_STEP_SUMMARY + [ ! -f "LICENSE" ] && echo "- Add LICENSE file: Choose from [OSI-approved licenses](https://opensource.org/licenses)" >> $GITHUB_STEP_SUMMARY + [ ! -f "CONTRIBUTING.md" ] && echo "- Create CONTRIBUTING.md: Use [template](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/templates/docs/required/CONTRIBUTING.md)" >> $GITHUB_STEP_SUMMARY + [ ! -f "SECURITY.md" ] && echo "- Create SECURITY.md: Use [template](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/templates/docs/required/SECURITY.md)" >> $GITHUB_STEP_SUMMARY + [ ! -f ".editorconfig" ] && echo "- Add .editorconfig: Use [template](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/templates/.editorconfig)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "๐Ÿ“š Reference: [MokoStandards File Requirements](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/docs/policy/file-header-standards.md)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### โŒ Validation Failed: Required Files Missing" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Status:** Repository files do not meet MokoStandards requirements" >> $GITHUB_STEP_SUMMARY + echo "**Missing:** $MISSING required file(s)" >> $GITHUB_STEP_SUMMARY + echo "**Compliance:** $PERCENT% ($PRESENT/$TOTAL files present)" >> $GITHUB_STEP_SUMMARY + echo "" + echo "โŒ ERROR: Required files missing - See job summary for remediation steps" + exit 1 + fi + + coding-standards: + name: Coding Standards Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check for Tab Characters + run: | + set -x + echo "### Tab Character Detection" >> $GITHUB_STEP_SUMMARY + + # Policy: Tabs are DEFAULT. Only check for tabs in files that REQUIRE spaces. + # Languages requiring spaces: YAML, Python, Haskell, F#, CoffeeScript, Nim, JSON, RST + TABS_IN_SPACES_FILES=$(find . -type f \ + \( -name "*.yml" -o -name "*.yaml" \ + -o -name "*.py" \ + -o -name "*.hs" -o -name "*.lhs" \ + -o -name "*.fs" -o -name "*.fsx" -o -name "*.fsi" \ + -o -name "*.coffee" -o -name "*.litcoffee" \ + -o -name "*.nim" -o -name "*.nims" -o -name "*.nimble" \ + -o -name "*.json" \ + -o -name "*.rst" \) \ + ! -path "./vendor/*" \ + ! -path "./node_modules/*" \ + ! -path "./.git/*" \ + -exec grep -l $'\t' {} \; 2>/dev/null | head -10) + + if [ -n "$TABS_IN_SPACES_FILES" ]; then + echo "โš ๏ธ Tab characters found in files that require spaces:" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "$TABS_IN_SPACES_FILES" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "These languages require spaces (tabs will break): YAML, Python, Haskell, F#, CoffeeScript, Nim, JSON, RST" >> $GITHUB_STEP_SUMMARY + echo "All other files (including .md, .ps1, LICENSE, etc.) may use tabs per MokoStandards policy" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… No tabs found in files requiring spaces" >> $GITHUB_STEP_SUMMARY + echo "Note: Tabs are allowed in most files (policy default). Only checked files requiring spaces." >> $GITHUB_STEP_SUMMARY + fi + + - name: Check File Encoding + run: | + set -x + echo "" >> $GITHUB_STEP_SUMMARY + echo "### File Encoding Check" >> $GITHUB_STEP_SUMMARY + + # Check for UTF-8 encoding (ASCII is a subset of UTF-8 and is acceptable) + NON_UTF8=$(find . -type f \( -name "*.php" -o -name "*.js" -o -name "*.md" \) \ + ! -path "./vendor/*" \ + ! -path "./node_modules/*" \ + ! -path "./.git/*" \ + -exec file {} \; | grep -v "UTF-8" | grep -v "ASCII" | head -5) + + if [ -n "$NON_UTF8" ]; then + echo "โš ๏ธ Non-UTF-8 files detected:" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "$NON_UTF8" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… All source files appear to be UTF-8 encoded" >> $GITHUB_STEP_SUMMARY + fi + + - name: Check Line Endings + run: | + set -x + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Line Ending Check" >> $GITHUB_STEP_SUMMARY + + # Check for CRLF line endings + CRLF_FILES=$(find . -type f \( -name "*.php" -o -name "*.js" -o -name "*.md" \) \ + ! -path "./vendor/*" \ + ! -path "./node_modules/*" \ + ! -path "./.git/*" \ + -exec file {} \; | grep "CRLF" | head -5) + + if [ -n "$CRLF_FILES" ]; then + echo "โš ๏ธ Files with CRLF line endings found:" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "$CRLF_FILES" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "MokoStandards requires LF line endings" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… Line endings are consistent (LF)" >> $GITHUB_STEP_SUMMARY + fi + + version-consistency: + name: Version Consistency Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Set up PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: '8.1' + extensions: json + tools: composer + coverage: none + + - name: Setup MokoStandards tools + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' + run: | + git clone --depth 1 --branch version/04 --quiet \ + "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ + /tmp/mokostandards 2>/dev/null || true + if [ -d "/tmp/mokostandards" ] && [ -f "/tmp/mokostandards/composer.json" ]; then + cd /tmp/mokostandards + composer install --no-dev --no-interaction --quiet 2>/dev/null || true + fi + + - name: Run Version Consistency Check + id: version_check + run: | + set -x + echo "## ๐Ÿ”ข Version Consistency Validation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Use MokoStandards tools (no Composer needed on the governed repo) + if [ -f "/tmp/mokostandards/api/validate/check_version_consistency.php" ]; then + php /tmp/mokostandards/api/validate/check_version_consistency.php --path . --verbose 2>&1 | tee /tmp/version-check.log + EXIT_CODE=${PIPESTATUS[0]} + elif [ -f "api/validate/check_version_consistency.php" ]; then + php api/validate/check_version_consistency.php --path . --verbose 2>&1 | tee /tmp/version-check.log + EXIT_CODE=${PIPESTATUS[0]} + else + echo "โญ๏ธ MokoStandards tools not available โ€” skipping version check" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + echo '```' >> $GITHUB_STEP_SUMMARY + cat /tmp/version-check.log >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + + if [ "$EXIT_CODE" -eq 0 ]; then + echo "โœ… All version numbers are consistent" >> $GITHUB_STEP_SUMMARY + else + echo "โŒ Version drift detected" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + # TIER 2 โ€” IMPORTANT (should pass) + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + workflow-validation: + name: Workflow Configuration Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check Required Workflows + run: | + set -x + echo "### GitHub Actions Workflows" >> $GITHUB_STEP_SUMMARY + + WORKFLOWS_DIR=".github/workflows" + + if [ ! -d "$WORKFLOWS_DIR" ]; then + echo "โŒ No workflows directory found" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### โŒ Validation Failed: Workflows Directory Missing" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Error:** .github/workflows directory is required for CI/CD automation" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Create .github/workflows directory and add GitHub Actions workflows" >> $GITHUB_STEP_SUMMARY + echo "" + echo "โŒ ERROR: .github/workflows directory not found" + exit 1 + fi + + # Check for recommended workflows + CI_FOUND=false + for wf in ci.yml build.yml ci-dolibarr.yml ci-joomla.yml; do + if [ -f "$WORKFLOWS_DIR/$wf" ]; then + echo "โœ… CI workflow present ($wf)" >> $GITHUB_STEP_SUMMARY + CI_FOUND=true + break + fi + done + if [ "$CI_FOUND" = "false" ]; then + echo "โš ๏ธ No CI workflow found (ci.yml, build.yml, ci-dolibarr.yml, or ci-joomla.yml)" >> $GITHUB_STEP_SUMMARY + fi + + if [ -f "$WORKFLOWS_DIR/codeql-analysis.yml" ]; then + echo "โœ… CodeQL security scanning present" >> $GITHUB_STEP_SUMMARY + else + echo "โš ๏ธ CodeQL workflow not found" >> $GITHUB_STEP_SUMMARY + fi + + # Check for MokoStandards-synced workflows + for wf in deploy-dev.yml deploy-demo.yml deploy-rs.yml sync-version-on-merge.yml auto-release.yml standards-compliance.yml enterprise-firewall-setup.yml; do + if [ -f "$WORKFLOWS_DIR/$wf" ]; then + echo "โœ… ${wf}" >> $GITHUB_STEP_SUMMARY + else + echo "โš ๏ธ ${wf} not found (synced from MokoStandards)" >> $GITHUB_STEP_SUMMARY + fi + done + + - name: Validate Workflow Syntax + run: | + set -x + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Workflow YAML Syntax" >> $GITHUB_STEP_SUMMARY + + INVALID=0 + for workflow in $(find .github/workflows -maxdepth 1 -type f \( -name "*.yml" -o -name "*.yaml" \) 2>/dev/null); do + if [ -f "$workflow" ]; then + if python3 -c "import yaml, sys; yaml.safe_load(open(sys.argv[1]))" "$workflow" 2>/dev/null; then + echo "โœ… $(basename $workflow)" >> $GITHUB_STEP_SUMMARY + else + echo "โŒ $(basename $workflow) - invalid YAML" >> $GITHUB_STEP_SUMMARY + INVALID=$((INVALID + 1)) + fi + fi + done + + if [ "$INVALID" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "### โŒ Validation Failed: Invalid Workflow YAML Syntax" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Error:** $INVALID workflow file(s) have invalid YAML syntax" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Fix YAML syntax errors in the marked workflow files" >> $GITHUB_STEP_SUMMARY + echo "**Tool:** Run \`python3 -c \"import yaml; yaml.safe_load(open('.github/workflows/FILE.yml'))\"\` locally" >> $GITHUB_STEP_SUMMARY + echo "" + echo "โŒ ERROR: $INVALID workflow file(s) with invalid YAML syntax" + exit 1 + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "### โœ… All Workflow Files Have Valid YAML Syntax" >> $GITHUB_STEP_SUMMARY + echo "" + echo "โœ… SUCCESS: All workflow files passed YAML validation" + + - name: Validate CodeQL Configuration + if: hashFiles('.github/workflows/codeql-analysis.yml') != '' + run: | + set -e + echo "" >> $GITHUB_STEP_SUMMARY + echo "### CodeQL Language Configuration" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Inline validation (rewritten from Python to bash for PHP-only architecture) + CODEQL_FILE=".github/workflows/codeql-analysis.yml" + + if [ ! -f "$CODEQL_FILE" ]; then + echo "โš ๏ธ CodeQL workflow file not found" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### โš ๏ธ CodeQL Workflow Not Found" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Status:** CodeQL workflow file not present - skipping language validation" >> $GITHUB_STEP_SUMMARY + echo "" + echo "โš ๏ธ INFO: CodeQL workflow not found - Skipping validation" + exit 0 + fi + + echo "**CodeQL Configuration Analysis**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Extract configured languages from workflow + LANGUAGES=$(grep -A5 "language:" "$CODEQL_FILE" | grep -oP "(?<=')[^']+(?=')" | tr '\n' ' ' || echo "") + + # Check if this is a configuration-only scan (no languages specified) + if grep -q "category.*language:config" "$CODEQL_FILE"; then + echo "**Scan Type:** Configuration-only (no language matrix)" >> $GITHUB_STEP_SUMMARY + echo "**Status:** โœ… Valid configuration for PHP-only repository" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "This CodeQL workflow scans YAML, JSON, shell scripts for security issues." >> $GITHUB_STEP_SUMMARY + echo "PHP security is handled by SecurityValidator enterprise library." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "โœ… SUCCESS: CodeQL configuration-only scan properly configured" + exit 0 + fi + + if [ -z "$LANGUAGES" ]; then + echo "โŒ No languages configured in CodeQL workflow" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### โŒ Validation Failed: CodeQL Languages Not Configured" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Error:** CodeQL workflow exists but has no languages configured" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Configure appropriate languages in codeql-analysis.yml" >> $GITHUB_STEP_SUMMARY + echo "" + echo "โŒ ERROR: No languages configured in CodeQL workflow" + exit 1 + fi + + echo "**Configured Languages:** $LANGUAGES" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Validate language presence in repository + INVALID_LANGS="" + VALID_LANGS="" + + for LANG in $LANGUAGES; do + case "$LANG" in + python) + # Check for Python files (should be none in v04.00.04) + if find . -name "*.py" -type f ! -path "./.git/*" | grep -q .; then + VALID_LANGS="$VALID_LANGS python" + echo "โœ… Python: Found Python files" >> $GITHUB_STEP_SUMMARY + else + INVALID_LANGS="$INVALID_LANGS python" + echo "โŒ Python: No Python files found (PHP-only repository)" >> $GITHUB_STEP_SUMMARY + fi + ;; + javascript|typescript) + # Check for JS/TS files + if find . \( -name "*.js" -o -name "*.ts" -o -name "*.json" \) -type f ! -path "./.git/*" ! -path "./node_modules/*" | grep -q .; then + VALID_LANGS="$VALID_LANGS $LANG" + echo "โœ… $LANG: Found JavaScript/TypeScript/JSON files" >> $GITHUB_STEP_SUMMARY + else + INVALID_LANGS="$INVALID_LANGS $LANG" + echo "โš ๏ธ $LANG: No JavaScript/TypeScript files found" >> $GITHUB_STEP_SUMMARY + fi + ;; + java) + if find . -name "*.java" -type f ! -path "./.git/*" | grep -q .; then + VALID_LANGS="$VALID_LANGS java" + echo "โœ… Java: Found Java files" >> $GITHUB_STEP_SUMMARY + else + INVALID_LANGS="$INVALID_LANGS java" + echo "โš ๏ธ Java: No Java files found" >> $GITHUB_STEP_SUMMARY + fi + ;; + go) + if find . -name "*.go" -type f ! -path "./.git/*" | grep -q .; then + VALID_LANGS="$VALID_LANGS go" + echo "โœ… Go: Found Go files" >> $GITHUB_STEP_SUMMARY + else + INVALID_LANGS="$INVALID_LANGS go" + echo "โš ๏ธ Go: No Go files found" >> $GITHUB_STEP_SUMMARY + fi + ;; + cpp|c) + if find . \( -name "*.cpp" -o -name "*.c" -o -name "*.h" \) -type f ! -path "./.git/*" | grep -q .; then + VALID_LANGS="$VALID_LANGS $LANG" + echo "โœ… $LANG: Found C/C++ files" >> $GITHUB_STEP_SUMMARY + else + INVALID_LANGS="$INVALID_LANGS $LANG" + echo "โš ๏ธ $LANG: No C/C++ files found" >> $GITHUB_STEP_SUMMARY + fi + ;; + ruby) + if find . -name "*.rb" -type f ! -path "./.git/*" | grep -q .; then + VALID_LANGS="$VALID_LANGS ruby" + echo "โœ… Ruby: Found Ruby files" >> $GITHUB_STEP_SUMMARY + else + INVALID_LANGS="$INVALID_LANGS ruby" + echo "โš ๏ธ Ruby: No Ruby files found" >> $GITHUB_STEP_SUMMARY + fi + ;; + *) + echo "โš ๏ธ $LANG: Unknown language, skipping validation" >> $GITHUB_STEP_SUMMARY + ;; + esac + done + + echo "" >> $GITHUB_STEP_SUMMARY + + # Report results + if [ -n "$INVALID_LANGS" ]; then + echo "**โš ๏ธ Warning:** Some configured languages may not have corresponding files:" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "Invalid languages: $INVALID_LANGS" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Note:** This is informational. CodeQL will skip languages without source files." >> $GITHUB_STEP_SUMMARY + echo "For PHP repository (v04.00.04), JavaScript language covers JSON/YAML/shell scripts." >> $GITHUB_STEP_SUMMARY + else + echo "โœ… **All configured CodeQL languages have corresponding source files**" >> $GITHUB_STEP_SUMMARY + fi + + # Always succeed - this is informational only + echo "" >> $GITHUB_STEP_SUMMARY + echo "### โœ… CodeQL Configuration Validation Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Status:** CodeQL language configuration reviewed successfully" >> $GITHUB_STEP_SUMMARY + echo "" + echo "โœ… SUCCESS: CodeQL validation complete" + exit 0 + + documentation-quality: + name: Documentation Quality Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Validate README.md + run: | + set -x + echo "## ๐Ÿ“š Documentation Quality Check" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### README.md Analysis" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ ! -f "README.md" ]; then + echo "โŒ **Critical:** README.md not found" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### โŒ Validation Failed: README.md Missing" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Error:** README.md is required for all MokoStandards-compliant repositories" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Create README.md with project description, setup instructions, and usage examples" >> $GITHUB_STEP_SUMMARY + echo "" + echo "โŒ ERROR: README.md not found - This is a critical requirement" + exit 1 + fi + + # Detailed content analysis + SIZE=$(wc -c < README.md) + LINES=$(wc -l < README.md) + WORDS=$(wc -w < README.md) + HEADINGS=$(grep -c "^#" README.md || echo 0) + LINKS=$(grep -c "\[.*\](.*)" README.md || echo 0) + CODE_BLOCKS=$(grep -c '```' README.md || echo 0) + + echo "| Metric | Value | Status | Recommendation |" >> $GITHUB_STEP_SUMMARY + echo "|--------|-------|--------|----------------|" >> $GITHUB_STEP_SUMMARY + + # Size check + SIZE_STATUS="โœ… Good" + SIZE_REC="Adequate length" + if [ "$SIZE" -lt 500 ]; then + SIZE_STATUS="โš ๏ธ Warning" + SIZE_REC="Add more content (min 500 bytes)" + elif [ "$SIZE" -gt 50000 ]; then + SIZE_STATUS="โš ๏ธ Warning" + SIZE_REC="Consider splitting into multiple docs" + fi + echo "| Size | $SIZE bytes | $SIZE_STATUS | $SIZE_REC |" >> $GITHUB_STEP_SUMMARY + + # Line count + LINES_STATUS="โœ… Good" + LINES_REC="Good size" + if [ "$LINES" -lt 20 ]; then + LINES_STATUS="โš ๏ธ Warning" + LINES_REC="Add more sections (min 20 lines)" + fi + echo "| Lines | $LINES | $LINES_STATUS | $LINES_REC |" >> $GITHUB_STEP_SUMMARY + + # Word count + WORDS_STATUS="โœ… Good" + WORDS_REC="Good detail" + if [ "$WORDS" -lt 100 ]; then + WORDS_STATUS="โš ๏ธ Warning" + WORDS_REC="Add more description (min 100 words)" + fi + echo "| Words | $WORDS | $WORDS_STATUS | $WORDS_REC |" >> $GITHUB_STEP_SUMMARY + + # Headings + HEADINGS_STATUS="โœ… Good" + HEADINGS_REC="Well structured" + if [ "$HEADINGS" -lt 3 ]; then + HEADINGS_STATUS="โš ๏ธ Warning" + HEADINGS_REC="Add more sections (min 3 headings)" + fi + echo "| Headings | $HEADINGS | $HEADINGS_STATUS | $HEADINGS_REC |" >> $GITHUB_STEP_SUMMARY + + # Links + LINKS_STATUS="โœ… Good" + LINKS_REC="Includes references" + if [ "$LINKS" -lt 1 ]; then + LINKS_STATUS="โ„น๏ธ Info" + LINKS_REC="Consider adding useful links" + fi + echo "| Links | $LINKS | $LINKS_STATUS | $LINKS_REC |" >> $GITHUB_STEP_SUMMARY + + # Code blocks + CODE_STATUS="โœ… Good" + CODE_REC="Includes examples" + if [ "$CODE_BLOCKS" -eq 0 ]; then + CODE_STATUS="โ„น๏ธ Info" + CODE_REC="Consider adding code examples" + fi + echo "| Code blocks | $CODE_BLOCKS | $CODE_STATUS | $CODE_REC |" >> $GITHUB_STEP_SUMMARY + + echo "" >> $GITHUB_STEP_SUMMARY + + # Check for key sections + echo "**Section Coverage:**" >> $GITHUB_STEP_SUMMARY + MISSING_COUNT=0 + grep -qi "install\|setup\|getting started" README.md && echo "- โœ… Installation/Setup instructions" >> $GITHUB_STEP_SUMMARY || { echo "- โš ๏ธ Missing: Installation/Setup" >> $GITHUB_STEP_SUMMARY; MISSING_COUNT=$((MISSING_COUNT + 1)); } + grep -qi "usage\|example\|how to" README.md && echo "- โœ… Usage examples" >> $GITHUB_STEP_SUMMARY || { echo "- โš ๏ธ Missing: Usage examples" >> $GITHUB_STEP_SUMMARY; MISSING_COUNT=$((MISSING_COUNT + 1)); } + grep -qi "license" README.md && echo "- โœ… License information" >> $GITHUB_STEP_SUMMARY || { echo "- โš ๏ธ Missing: License information" >> $GITHUB_STEP_SUMMARY; MISSING_COUNT=$((MISSING_COUNT + 1)); } + grep -qi "contribut" README.md && echo "- โœ… Contributing guidelines" >> $GITHUB_STEP_SUMMARY || echo "- โ„น๏ธ Optional: Contributing section" >> $GITHUB_STEP_SUMMARY + + if [ "$MISSING_COUNT" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**โš ๏ธ $MISSING_COUNT important sections missing**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Validate CHANGELOG.md + run: | + set -x + echo "" >> $GITHUB_STEP_SUMMARY + echo "### CHANGELOG.md Analysis" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Locate changelog case-insensitively; accepted at root, src/, or docs/ + CHANGELOG_PATH=$(find . -maxdepth 3 \( -path ./.git -o -path ./node_modules \) -prune \ + -o -iname "changelog.md" -print | head -1 | sed 's|^\./||') + + if [ -z "$CHANGELOG_PATH" ]; then + echo "โŒ **Critical:** CHANGELOG.md not found (checked root, src/, docs/)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### โŒ Validation Failed: CHANGELOG.md Missing" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Error:** CHANGELOG.md is required for all MokoStandards-compliant repositories" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Create CHANGELOG.md following [Keep a Changelog](https://keepachangelog.com/) format" >> $GITHUB_STEP_SUMMARY + echo "" + echo "โŒ ERROR: CHANGELOG.md not found - This is a critical requirement" + exit 1 + fi + + echo "๐Ÿ“„ Found: $CHANGELOG_PATH" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Analyze changelog structure + VERSIONS=$(grep -c "## \[" "$CHANGELOG_PATH" || echo 0) + UNRELEASED=$(grep -c "## \[Unreleased\]" "$CHANGELOG_PATH" || echo 0) + DATES=$(grep -c "[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}" "$CHANGELOG_PATH" || echo 0) + SIZE=$(wc -c < "$CHANGELOG_PATH") + + echo "| Metric | Value | Status | Notes |" >> $GITHUB_STEP_SUMMARY + echo "|--------|-------|--------|-------|" >> $GITHUB_STEP_SUMMARY + + # Check format + if grep -qi "## \[.*\]" "$CHANGELOG_PATH"; then + echo "| Format | Keep a Changelog | โœ… Pass | Standard format |" >> $GITHUB_STEP_SUMMARY + else + echo "| Format | Custom | โš ๏ธ Warning | Consider [Keep a Changelog](https://keepachangelog.com/) |" >> $GITHUB_STEP_SUMMARY + fi + + # Version count + VERSIONS_STATUS="โœ… Good" + VERSIONS_NOTE="Well maintained" + if [ "$VERSIONS" -lt 1 ]; then + VERSIONS_STATUS="โš ๏ธ Warning" + VERSIONS_NOTE="Add version entries" + fi + echo "| Versions | $VERSIONS | $VERSIONS_STATUS | $VERSIONS_NOTE |" >> $GITHUB_STEP_SUMMARY + + # Unreleased section + if [ "$UNRELEASED" -gt 0 ]; then + echo "| Unreleased | Yes | โœ… Good | Active development tracked |" >> $GITHUB_STEP_SUMMARY + else + echo "| Unreleased | No | โ„น๏ธ Info | Consider adding [Unreleased] section |" >> $GITHUB_STEP_SUMMARY + fi + + # Dates + DATES_STATUS="โœ… Good" + if [ "$DATES" -lt 1 ]; then + DATES_STATUS="โš ๏ธ Warning" + DATES_NOTE="Add release dates" + else + DATES_NOTE="Dates present" + fi + echo "| Release dates | $DATES | $DATES_STATUS | $DATES_NOTE |" >> $GITHUB_STEP_SUMMARY + + # Check for standard sections + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Changelog Sections:**" >> $GITHUB_STEP_SUMMARY + grep -qi "### Added" "$CHANGELOG_PATH" && echo "- โœ… Added section" >> $GITHUB_STEP_SUMMARY || echo "- โ„น๏ธ Added section (optional)" >> $GITHUB_STEP_SUMMARY + grep -qi "### Changed" "$CHANGELOG_PATH" && echo "- โœ… Changed section" >> $GITHUB_STEP_SUMMARY || echo "- โ„น๏ธ Changed section (optional)" >> $GITHUB_STEP_SUMMARY + grep -qi "### Fixed" "$CHANGELOG_PATH" && echo "- โœ… Fixed section" >> $GITHUB_STEP_SUMMARY || echo "- โ„น๏ธ Fixed section (optional)" >> $GITHUB_STEP_SUMMARY + + echo "" >> $GITHUB_STEP_SUMMARY + echo "๐Ÿ“š Reference: [Keep a Changelog](https://keepachangelog.com/)" >> $GITHUB_STEP_SUMMARY + + - name: Check Documentation Index + run: | + set -x + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Documentation Index" >> $GITHUB_STEP_SUMMARY + + if [ -f "docs/index.md" ] || [ -f "docs/README.md" ]; then + echo "โœ… Documentation index found" >> $GITHUB_STEP_SUMMARY + else + echo "โš ๏ธ No documentation index (docs/index.md or docs/README.md)" >> $GITHUB_STEP_SUMMARY + fi + + readme-completeness: + name: README Completeness Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check README Sections + run: | + set -x + echo "## ๐Ÿ“„ README Completeness Check" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ ! -f "README.md" ]; then + echo "โŒ README.md not found" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + # Required sections + REQUIRED_SECTIONS=("Installation" "Usage" "Contributing" "License") + MISSING=0 + PRESENT=0 + + echo "### Required Sections" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + for section in "${REQUIRED_SECTIONS[@]}"; do + if grep -qi "##.*$section" README.md; then + echo "โœ… $section" >> $GITHUB_STEP_SUMMARY + PRESENT=$((PRESENT + 1)) + else + echo "โŒ $section" >> $GITHUB_STEP_SUMMARY + MISSING=$((MISSING + 1)) + fi + done + + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Completeness**: $PRESENT/${#REQUIRED_SECTIONS[@]} required sections present" >> $GITHUB_STEP_SUMMARY + + if [ "$MISSING" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Action Required**: Add missing sections to README.md" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + # ============================================================================ + # PHASE 3: Future Enhancements + # ============================================================================ + + git-hygiene: + name: Git Repository Hygiene + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + + - name: Check .gitignore + run: | + set -x + echo "### .gitignore Validation" >> $GITHUB_STEP_SUMMARY + + if [ ! -f ".gitignore" ]; then + echo "โš ๏ธ .gitignore file not found" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### โš ๏ธ Warning: .gitignore Not Found" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Status:** .gitignore file is recommended but not required" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation:** Add .gitignore to exclude build artifacts, dependencies, and temporary files" >> $GITHUB_STEP_SUMMARY + echo "" + echo "โš ๏ธ WARNING: .gitignore file not found - Continuing validation" + exit 0 + fi + + # Check for common exclusions + MISSING="" + grep -q "vendor/" .gitignore || MISSING="${MISSING}vendor/ " + grep -q "node_modules/" .gitignore || MISSING="${MISSING}node_modules/ " + + if [ -n "$MISSING" ]; then + echo "โš ๏ธ .gitignore may be missing common exclusions: $MISSING" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… .gitignore appears complete" >> $GITHUB_STEP_SUMMARY + fi + + - name: Check for Large Files + run: | + set -x + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Large File Detection" >> $GITHUB_STEP_SUMMARY + + # Find files larger than 1MB + LARGE_FILES=$(find . -type f -size +1M ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" | head -5) + + if [ -n "$LARGE_FILES" ]; then + echo "โš ๏ธ Large files detected (>1MB):" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "$LARGE_FILES" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "Consider using Git LFS for large binary files" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… No unusually large files detected" >> $GITHUB_STEP_SUMMARY + fi + + script-integrity: + name: Script Integrity Validation + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.x' + + - name: Validate Script Integrity + id: script_check + run: | + set -x + echo "## ๐Ÿ” Script Integrity Validation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ -f "api/.script-registry.json" ]; then + echo "### Critical Scripts" >> $GITHUB_STEP_SUMMARY + php api/maintenance/update_sha_hashes.php \ + --dry-run --verbose | tee /tmp/script-validation.log + + EXIT_CODE=$? + + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + cat /tmp/script-validation.log >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + + if [ "$EXIT_CODE" -eq 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "โœ… All critical scripts validated successfully!" >> $GITHUB_STEP_SUMMARY + exit 0 + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "โŒ Script integrity violations detected" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Review validation report and update registry" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + else + echo "โ„น๏ธ Script registry not found - skipping integrity check" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + # TIER 3 โ€” QUALITY (code quality metrics) + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + line-length-validation: + name: Line Length Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check Line Lengths + run: | + set -x + echo "## ๐Ÿ“ Line Length Validation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Line length standards: + # - General source code: 120 characters (hard limit) + # - YAML workflows: 180 characters (exception for GitHub Actions) + # - Markdown files: No limit (content-focused) + + echo "### Line Length Standards" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| File Type | Soft Limit | Hard Limit |" >> $GITHUB_STEP_SUMMARY + echo "|-----------|------------|------------|" >> $GITHUB_STEP_SUMMARY + echo "| General source code | 80 chars | 120 chars |" >> $GITHUB_STEP_SUMMARY + echo "| YAML workflows | 80 chars | 180 chars |" >> $GITHUB_STEP_SUMMARY + echo "| Markdown files | N/A | No limit |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Check YAML files (using yamllint which is already configured) + echo "### YAML Files (180 char limit)" >> $GITHUB_STEP_SUMMARY + + YAML_VIOLATIONS=0 + if command -v yamllint >/dev/null 2>&1; then + # Install yamllint if not present + : + else + pip install yamllint >/dev/null 2>&1 + fi + + # Run yamllint and count line-length warnings + YAML_OUTPUT=$(yamllint .github/workflows/*.yml 2>&1 | grep "line too long" || true) + if [ -n "$YAML_OUTPUT" ]; then + YAML_VIOLATIONS=$(echo "$YAML_OUTPUT" | wc -l) + echo "โš ๏ธ Found $YAML_VIOLATIONS lines exceeding 180 characters in YAML files" >> $GITHUB_STEP_SUMMARY + echo "
View warnings (informational only)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "$YAML_OUTPUT" | head -20 >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… All YAML files comply with 180 character limit" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # Check source code files (PHP, Python, JavaScript, etc.) for 120 char limit + echo "### Source Code Files (120 char limit)" >> $GITHUB_STEP_SUMMARY + + LONG_LINES=$(find . -type f \ + \( -name "*.php" -o -name "*.py" -o -name "*.js" -o -name "*.ts" \ + -o -name "*.go" -o -name "*.rs" -o -name "*.java" -o -name "*.c" \ + -o -name "*.cpp" -o -name "*.h" -o -name "*.sh" \) \ + ! -path "./vendor/*" \ + ! -path "./node_modules/*" \ + ! -path "./.git/*" \ + ! -path "./build/*" \ + ! -path "./dist/*" \ + -exec awk 'length > 120 { print FILENAME ":" NR ": " length " chars" }' {} \; 2>/dev/null | head -20) + + if [ -n "$LONG_LINES" ]; then + LINE_COUNT=$(echo "$LONG_LINES" | wc -l) + echo "โš ๏ธ Found $LINE_COUNT source code lines exceeding 120 characters" >> $GITHUB_STEP_SUMMARY + echo "
View violations (informational)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "$LONG_LINES" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… All source code files comply with 120 character limit" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # Confirm Markdown files are not checked + echo "### Markdown Files" >> $GITHUB_STEP_SUMMARY + echo "โœ… Markdown files have no line length limit per coding standards" >> $GITHUB_STEP_SUMMARY + echo "Rationale: Content-focused format, URLs, tables, and natural prose flow" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Summary + echo "### Summary" >> $GITHUB_STEP_SUMMARY + echo "This check is **informational only** and does not block merges." >> $GITHUB_STEP_SUMMARY + echo "Line length standards help maintain code readability." >> $GITHUB_STEP_SUMMARY + echo "Exceptions documented in: \`docs/policy/coding-style-guide.md\`" >> $GITHUB_STEP_SUMMARY + + file-naming-standards: + name: File Naming Standards + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check File Naming + run: | + set -x + echo "## ๐Ÿ“ File Naming Standards" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + VIOLATIONS=0 + + # Check PHP files (should be PascalCase for classes) + INVALID_PHP=$(find . -name "*.php" ! -path "./vendor/*" ! -path "./.git/*" ! -regex ".*/[A-Z][a-zA-Z0-9]*\.php" ! -name "index.php" ! -name "functions.php" | wc -l || echo 0) + + # Check config files (should be kebab-case) + INVALID_CONFIG=$(find . -name "*.yml" -o -name "*.yaml" -o -name "*.json" ! -path "./vendor/*" ! -path "./.git/*" ! -path "./node_modules/*" | grep -E "[A-Z_]" | wc -l || echo 0) + + echo "### Naming Violations" >> $GITHUB_STEP_SUMMARY + echo "- **PHP files not PascalCase**: $INVALID_PHP" >> $GITHUB_STEP_SUMMARY + echo "- **Config files not kebab-case**: $INVALID_CONFIG" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + VIOLATIONS=$((INVALID_PHP + INVALID_CONFIG)) + + if [ "$VIOLATIONS" -gt 0 ]; then + echo "โš ๏ธ Found $VIOLATIONS naming convention violation(s)" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Follow naming conventions for consistency" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… File naming conventions followed" >> $GITHUB_STEP_SUMMARY + fi + + insecure-patterns: + name: Insecure Code Pattern Detection + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Scan for Insecure Patterns + run: | + set -x + echo "## ๐Ÿ”’ Insecure Code Pattern Detection" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + VIOLATIONS=0 + + # PHP: SQL injection patterns + if grep -r -n "\\$_\(GET\|POST\|REQUEST\).*mysql_query\|mysqli_query" . --include="*.php" ! -path "./vendor/*" 2>/dev/null > /tmp/sql_inject.txt; then + COUNT=$(wc -l < /tmp/sql_inject.txt) + echo "โš ๏ธ Found $COUNT potential SQL injection pattern(s)" >> $GITHUB_STEP_SUMMARY + VIOLATIONS=$((VIOLATIONS + COUNT)) + fi + + # PHP: eval/exec usage + if grep -r -n "eval\|exec\|system\|passthru\|shell_exec" . --include="*.php" ! -path "./vendor/*" 2>/dev/null > /tmp/exec.txt; then + COUNT=$(wc -l < /tmp/exec.txt) + echo "โš ๏ธ Found $COUNT dangerous function call(s)" >> $GITHUB_STEP_SUMMARY + VIOLATIONS=$((VIOLATIONS + COUNT)) + fi + + # Python: eval usage + if grep -r -n "eval(" . --include="*.py" 2>/dev/null > /tmp/py_eval.txt; then + COUNT=$(wc -l < /tmp/py_eval.txt) + echo "โš ๏ธ Found $COUNT Python eval() usage(s)" >> $GITHUB_STEP_SUMMARY + VIOLATIONS=$((VIOLATIONS + COUNT)) + fi + + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$VIOLATIONS" -gt 0 ]; then + echo "**Total Violations**: $VIOLATIONS" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Review and secure flagged patterns" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… No insecure patterns detected" >> $GITHUB_STEP_SUMMARY + fi + + code-complexity: + name: Code Complexity Analysis + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: '8.1' + + - name: Analyze Complexity + run: | + set -x + echo "## ๐Ÿ“Š Code Complexity Analysis" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + PHP_COUNT=$(find . -name "*.php" ! -path "./vendor/*" ! -path "./.git/*" | wc -l) + + if [ "$PHP_COUNT" -gt 0 ]; then + # Install phploc + wget https://phar.phpunit.de/phploc.phar 2>/dev/null + chmod +x phploc.phar + + echo "### PHP Code Metrics" >> $GITHUB_STEP_SUMMARY + if ./phploc.phar --exclude vendor --exclude .git . 2>&1 | tee /tmp/phploc.txt; then + COMPLEXITY=$(grep "Cyclomatic Complexity" /tmp/phploc.txt | grep "Average" | awk '{print $NF}' || echo "N/A") + echo "**Average Cyclomatic Complexity**: $COMPLEXITY" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$COMPLEXITY" != "N/A" ] && [ $(echo "$COMPLEXITY > 10" | bc -l) -eq 1 ]; then + echo "โš ๏ธ Average complexity exceeds recommended threshold (10)" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Refactor complex functions" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… Code complexity within acceptable limits" >> $GITHUB_STEP_SUMMARY + fi + fi + else + echo "โ„น๏ธ No PHP files found for complexity analysis" >> $GITHUB_STEP_SUMMARY + fi + + code-duplication: + name: Code Duplication Detection + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: '8.1' + + - name: Detect Duplicates + run: | + set -x + echo "## ๐Ÿ” Code Duplication Detection" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Check if PHP files exist + PHP_COUNT=$(find . -name "*.php" ! -path "./vendor/*" ! -path "./.git/*" | wc -l) + + if [ "$PHP_COUNT" -gt 0 ]; then + echo "### PHP Code Duplication" >> $GITHUB_STEP_SUMMARY + + # Install phpcpd + wget https://phar.phpunit.de/phpcpd.phar 2>/dev/null + chmod +x phpcpd.phar + + # Run duplication detection + if ./phpcpd.phar --exclude vendor --exclude .git . 2>&1 | tee /tmp/phpcpd.txt; then + DUPLICATION=$(grep "Found" /tmp/phpcpd.txt | grep -oE "[0-9]+\.[0-9]+%" | head -1 || echo "0.00%") + echo "๐Ÿ“Š **Duplication Rate**: $DUPLICATION" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + DUPLICATION_NUM=$(echo "$DUPLICATION" | sed 's/%//') + if [ $(echo "$DUPLICATION_NUM > 5.0" | bc -l) -eq 1 ]; then + echo "โš ๏ธ Code duplication exceeds 5% threshold" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View duplication details" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + cat /tmp/phpcpd.txt >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… Code duplication within acceptable limits (<5%)" >> $GITHUB_STEP_SUMMARY + fi + else + echo "โœ… No significant code duplication detected" >> $GITHUB_STEP_SUMMARY + fi + else + echo "โ„น๏ธ No PHP files found for duplication analysis" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Note**: This is an informational check to encourage DRY principles." >> $GITHUB_STEP_SUMMARY + + dead-code-detection: + name: Dead Code Detection + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.x' + + - name: Detect Dead Code + run: | + set -x + echo "## ๐Ÿ—‘๏ธ Dead Code Detection" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + PY_COUNT=$(find . -name "*.py" ! -path "./vendor/*" ! -path "./.git/*" ! -path "./venv/*" | wc -l) + + if [ "$PY_COUNT" -gt 0 ]; then + pip install vulture 2>/dev/null + echo "### Python Dead Code" >> $GITHUB_STEP_SUMMARY + + if vulture . --exclude vendor,venv,.git 2>&1 | tee /tmp/vulture.txt; then + DEAD_COUNT=$(wc -l < /tmp/vulture.txt || echo 0) + if [ "$DEAD_COUNT" -gt 0 ]; then + echo "โš ๏ธ Found $DEAD_COUNT potential dead code item(s)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View dead code" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + head -50 /tmp/vulture.txt >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… No dead code detected" >> $GITHUB_STEP_SUMMARY + fi + fi + else + echo "โ„น๏ธ No Python files found for dead code analysis" >> $GITHUB_STEP_SUMMARY + fi + + + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + # TIER 4 โ€” SUPPLEMENTARY (informational) + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + file-size-limits: + name: File Size Limits + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check File Sizes + run: | + set -x + echo "## ๐Ÿ“ฆ File Size Validation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Exempt file types (allowed to be large) + EXEMPT="! -name *.mmdb ! -name *.woff2 ! -name *.woff ! -name *.ttf ! -name *.otf" + + # Find large files (>15MB warning, >20MB critical) + LARGE_FILES=$(find . -type f -size +15M $EXEMPT ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" 2>/dev/null | wc -l) + HUGE_FILES=$(find . -type f -size +20M $EXEMPT ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" 2>/dev/null | wc -l) + + echo "### Size Thresholds" >> $GITHUB_STEP_SUMMARY + echo "- **Warning**: Files >15MB" >> $GITHUB_STEP_SUMMARY + echo "- **Critical**: Files >20MB" >> $GITHUB_STEP_SUMMARY + echo "- **Exempt**: .mmdb, .woff2, .woff, .ttf, .otf" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$HUGE_FILES" -gt 0 ]; then + echo "โŒ **Critical**: Found $HUGE_FILES file(s) exceeding 20MB" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View files >20MB" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + find . -type f -size +20M $EXEMPT ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" -exec ls -lh {} + 2>/dev/null | awk '{print $5, $9}' >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Action Required**: Remove or optimize files >20MB" >> $GITHUB_STEP_SUMMARY + exit 1 + elif [ "$LARGE_FILES" -gt 0 ]; then + echo "โš ๏ธ **Warning**: Found $LARGE_FILES file(s) between 15MB and 20MB" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View files >15MB" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + find . -type f -size +15M $EXEMPT ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" -exec ls -lh {} + 2>/dev/null | awk '{print $5, $9}' >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Consider optimizing large files" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… All files within acceptable size limits" >> $GITHUB_STEP_SUMMARY + fi + + binary-file-detection: + name: Binary File Detection + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Detect Binary Files + run: | + set -x + echo "## ๐Ÿ” Binary File Detection" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Find binary files excluding allowed types + BINARIES=$(find . -type f ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" \ + ! -name "*.png" ! -name "*.jpg" ! -name "*.jpeg" ! -name "*.gif" ! -name "*.svg" ! -name "*.ico" \ + ! -name "*.woff" ! -name "*.woff2" ! -name "*.ttf" ! -name "*.eot" \ + -exec file {} \; | grep -v "text" | grep -v "empty" | wc -l || echo 0) + + if [ "$BINARIES" -gt 0 ]; then + echo "โš ๏ธ Found $BINARIES non-image binary file(s)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View binary files" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + find . -type f ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" \ + ! -name "*.png" ! -name "*.jpg" ! -name "*.jpeg" ! -name "*.gif" ! -name "*.svg" ! -name "*.ico" \ + ! -name "*.woff" ! -name "*.woff2" ! -name "*.ttf" ! -name "*.eot" \ + -exec file {} \; | grep -v "text" | grep -v "empty" | cut -d: -f1 >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Source control should primarily contain text files" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… No unexpected binary files detected" >> $GITHUB_STEP_SUMMARY + fi + + # ============================================================================ + # PHASE 4: Nice to Have Checks + # ============================================================================ + + todo-fixme-tracking: + name: TODO/FIXME Tracking + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Track Technical Debt + run: | + set -x + echo "## ๐Ÿ“ TODO/FIXME Tracking" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Tracking technical debt markers in source code." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Search for technical debt markers + PATTERNS="TODO|FIXME|HACK|XXX" + EXTENSIONS="*.php *.py *.js *.ts *.go *.rs *.java *.c *.cpp *.h *.hpp *.sh" + + echo "### Technical Debt Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + TOTAL_COUNT=0 + for ext in $EXTENSIONS; do + COUNT=$(find . -type f -name "$ext" ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" -exec grep -n -E "($PATTERNS)" {} + 2>/dev/null | wc -l || echo 0) + TOTAL_COUNT=$((TOTAL_COUNT + COUNT)) + done + + if [ "$TOTAL_COUNT" -gt 0 ]; then + echo "โš ๏ธ Found **$TOTAL_COUNT** technical debt item(s)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View technical debt items" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + for ext in $EXTENSIONS; do + find . -type f -name "$ext" ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" -exec grep -n -H -E "($PATTERNS)" {} + 2>/dev/null | head -100 || true + done >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… No technical debt markers found" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Note**: This is an informational check. Technical debt items don't block compliance." >> $GITHUB_STEP_SUMMARY + + dependency-vulnerabilities: + name: Dependency Vulnerability Scanning + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: '8.1' + + - name: Setup Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.x' + + - name: Scan Dependencies + run: | + set -x + echo "## ๐Ÿ›ก๏ธ Dependency Vulnerability Scanning" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + VULNERABILITIES=0 + + # PHP Dependencies + if [ -f "composer.json" ]; then + echo "### PHP Dependencies (composer)" >> $GITHUB_STEP_SUMMARY + if composer audit --no-dev 2>&1 | tee /tmp/php_audit.txt; then + echo "โœ… No PHP vulnerabilities detected" >> $GITHUB_STEP_SUMMARY + else + VULN_COUNT=$(grep -c "vulnerability" /tmp/php_audit.txt || echo 0) + echo "โš ๏ธ Found $VULN_COUNT PHP vulnerability/vulnerabilities" >> $GITHUB_STEP_SUMMARY + VULNERABILITIES=$((VULNERABILITIES + VULN_COUNT)) + fi + echo "" >> $GITHUB_STEP_SUMMARY + fi + + # Python Dependencies + if [ -f "requirements.txt" ]; then + echo "### Python Dependencies" >> $GITHUB_STEP_SUMMARY + pip install pip-audit 2>&1 > /dev/null + if pip-audit -r requirements.txt 2>&1 | tee /tmp/py_audit.txt; then + echo "โœ… No Python vulnerabilities detected" >> $GITHUB_STEP_SUMMARY + else + VULN_COUNT=$(grep -c "vulnerability" /tmp/py_audit.txt || echo 0) + echo "โš ๏ธ Found $VULN_COUNT Python vulnerability/vulnerabilities" >> $GITHUB_STEP_SUMMARY + VULNERABILITIES=$((VULNERABILITIES + VULN_COUNT)) + fi + echo "" >> $GITHUB_STEP_SUMMARY + fi + + # NPM Dependencies + if [ -f "package.json" ]; then + echo "### NPM Dependencies" >> $GITHUB_STEP_SUMMARY + if npm audit --production 2>&1 | tee /tmp/npm_audit.txt; then + echo "โœ… No NPM vulnerabilities detected" >> $GITHUB_STEP_SUMMARY + else + VULN_COUNT=$(grep -c "vulnerability" /tmp/npm_audit.txt || echo 0) + echo "โš ๏ธ Found $VULN_COUNT NPM vulnerability/vulnerabilities" >> $GITHUB_STEP_SUMMARY + VULNERABILITIES=$((VULNERABILITIES + VULN_COUNT)) + fi + echo "" >> $GITHUB_STEP_SUMMARY + fi + + if [ "$VULNERABILITIES" -gt 0 ]; then + echo "**Total Vulnerabilities**: $VULNERABILITIES" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Action Required**: Update vulnerable dependencies" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "โœ… No dependency vulnerabilities detected" >> $GITHUB_STEP_SUMMARY + fi + + unused-dependencies: + name: Unused Dependencies Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: '8.1' + + - name: Check Unused Dependencies + run: | + set -x + echo "## ๐Ÿ“ฆ Unused Dependencies Check" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ -f "composer.json" ]; then + echo "### PHP Dependencies" >> $GITHUB_STEP_SUMMARY + + # Install composer-unused + composer global require icanhazstring/composer-unused 2>/dev/null || true + + if composer global exec composer-unused 2>&1 | tee /tmp/unused.txt; then + UNUSED_COUNT=$(grep "unused" /tmp/unused.txt | wc -l || echo 0) + if [ "$UNUSED_COUNT" -gt 0 ]; then + echo "โš ๏ธ Found $UNUSED_COUNT unused dependency/dependencies" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View unused dependencies" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + cat /tmp/unused.txt >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… No unused dependencies detected" >> $GITHUB_STEP_SUMMARY + fi + else + echo "โœ… All dependencies appear to be in use" >> $GITHUB_STEP_SUMMARY + fi + else + echo "โ„น๏ธ No composer.json found" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Remove unused dependencies to reduce attack surface" >> $GITHUB_STEP_SUMMARY + + broken-link-detection: + name: Broken Link Detection + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check Internal Links + run: | + set -x + echo "## ๐Ÿ”— Broken Link Detection" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Checking internal links in markdown files." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + BROKEN_LINKS=0 + CHECKED_LINKS=0 + + # Find all markdown files + MD_FILES=$(find . -name "*.md" ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*") + + for file in $MD_FILES; do + # Extract markdown links [text](path) + while IFS= read -r line; do + # Extract path from [text](path) + link=$(echo "$line" | sed -n 's/.*\](\([^)]*\)).*/\1/p') + + # Skip external links (http/https) + if echo "$link" | grep -qE "^https?://"; then + continue + fi + + # Skip anchors only + if echo "$link" | grep -qE "^#"; then + continue + fi + + CHECKED_LINKS=$((CHECKED_LINKS + 1)) + + # Get directory of the markdown file + basedir=$(dirname "$file") + + # Resolve relative path + if [ -n "$link" ]; then + # Remove anchor if present + clean_link=$(echo "$link" | sed 's/#.*//') + + # Check if file exists + if [ ! -e "$basedir/$clean_link" ] && [ ! -e "$clean_link" ]; then + echo "Broken link in $file: $link" >> /tmp/broken_links.txt + BROKEN_LINKS=$((BROKEN_LINKS + 1)) + fi + fi + done < <(grep -o '\[.*\](.*)' "$file" 2>/dev/null || true) + done + + echo "### Link Validation Results" >> $GITHUB_STEP_SUMMARY + echo "- **Links Checked**: $CHECKED_LINKS" >> $GITHUB_STEP_SUMMARY + echo "- **Broken Links**: $BROKEN_LINKS" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$BROKEN_LINKS" -gt 0 ]; then + echo "โš ๏ธ Found $BROKEN_LINKS broken internal link(s)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View broken links" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + cat /tmp/broken_links.txt 2>/dev/null >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Fix or remove broken links to maintain documentation quality" >> $GITHUB_STEP_SUMMARY + else + if [ "$CHECKED_LINKS" -gt 0 ]; then + echo "โœ… All internal links are valid" >> $GITHUB_STEP_SUMMARY + else + echo "โ„น๏ธ No internal links found to check" >> $GITHUB_STEP_SUMMARY + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Note**: This check validates internal file references only. External URLs are not validated." >> $GITHUB_STEP_SUMMARY + + # ============================================================================ + # PHASE 2: Medium Priority Checks + # ============================================================================ + + api-documentation: + name: API Documentation Coverage + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check Documentation + run: | + set -x + echo "## ๐Ÿ“š API Documentation Coverage" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Count public functions/classes + PUBLIC_METHODS=$(grep -r "public function" . --include="*.php" ! -path "./vendor/*" | wc -l || echo 0) + DOCUMENTED=$(grep -B5 -r "public function" . --include="*.php" ! -path "./vendor/*" | grep -c "/\*\*" || echo 0) + + if [ "$PUBLIC_METHODS" -gt 0 ]; then + COVERAGE=$((DOCUMENTED * 100 / PUBLIC_METHODS)) + echo "**Documentation Coverage**: $COVERAGE% ($DOCUMENTED/$PUBLIC_METHODS)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$COVERAGE" -lt 80 ]; then + echo "โš ๏ธ Documentation coverage below 80% threshold" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Add PHPDoc blocks to public methods" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… Good documentation coverage" >> $GITHUB_STEP_SUMMARY + fi + else + echo "โ„น๏ธ No public methods found for documentation check" >> $GITHUB_STEP_SUMMARY + fi + + accessibility-check: + name: Accessibility Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check Accessibility + run: | + set -x + echo "## โ™ฟ Accessibility Check" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + HTML_COUNT=$(find . -name "*.html" ! -path "./vendor/*" ! -path "./.git/*" ! -path "./node_modules/*" | wc -l || echo 0) + MD_IMG_COUNT=$(find . -name "*.md" ! -path "./vendor/*" ! -path "./.git/*" -exec grep -l "!\[" {} + 2>/dev/null | wc -l || echo 0) + + if [ "$HTML_COUNT" -gt 0 ] || [ "$MD_IMG_COUNT" -gt 0 ]; then + # Check for images without alt text + MISSING_ALT=0 + + if [ "$HTML_COUNT" -gt 0 ]; then + MISSING_ALT=$(grep -r "> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$MISSING_ALT" -gt 0 ]; then + echo "โš ๏ธ Found images without alt text" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Add descriptive alt text for accessibility" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… All images have alt text" >> $GITHUB_STEP_SUMMARY + fi + else + echo "โ„น๏ธ No HTML files found for accessibility check" >> $GITHUB_STEP_SUMMARY + fi + + performance-metrics: + name: Performance Metrics + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check Performance Metrics + run: | + set -x + echo "## โšก Performance Metrics" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Check if JavaScript bundles exist + if [ -f "package.json" ]; then + echo "### Bundle Analysis" >> $GITHUB_STEP_SUMMARY + + # Check for common bundle files + BUNDLE_SIZE=0 + if [ -d "dist" ]; then + BUNDLE_SIZE=$(du -sb dist/ 2>/dev/null | cut -f1 || echo 0) + elif [ -d "build" ]; then + BUNDLE_SIZE=$(du -sb build/ 2>/dev/null | cut -f1 || echo 0) + fi + + if [ "$BUNDLE_SIZE" -gt 0 ]; then + BUNDLE_MB=$((BUNDLE_SIZE / 1024 / 1024)) + echo "**Bundle Size**: ${BUNDLE_MB}MB" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$BUNDLE_MB" -gt 5 ]; then + echo "โš ๏ธ Bundle size exceeds 5MB threshold" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Optimize bundle size" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… Bundle size within acceptable limits" >> $GITHUB_STEP_SUMMARY + fi + else + echo "โ„น๏ธ No build artifacts found" >> $GITHUB_STEP_SUMMARY + fi + else + echo "โ„น๏ธ Not a JavaScript project" >> $GITHUB_STEP_SUMMARY + fi + + enterprise-readiness: + name: Enterprise Readiness Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Set up PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: '8.1' + extensions: json, mbstring + tools: composer + coverage: none + + - name: Install API Package + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' + run: | + if [ -f "composer.json" ]; then + composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader + else + echo "No composer.json โ€” pulling MokoStandards tools" + if [ ! -d "/tmp/mokostandards" ]; then + git clone --depth 1 --branch version/04 --quiet \ + "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ + /tmp/mokostandards 2>/dev/null || true + if [ -f "/tmp/mokostandards/composer.json" ]; then + cd /tmp/mokostandards && composer install --no-dev --no-interaction --quiet 2>/dev/null || true + cd - + fi + fi + fi + + - name: Check Enterprise Readiness + id: enterprise_check + run: | + echo "" >> $GITHUB_STEP_SUMMARY + + SCRIPT="" + if [ -f "api/validate/check_enterprise_readiness.php" ]; then + SCRIPT="api/validate/check_enterprise_readiness.php" + elif [ -f "/tmp/mokostandards/api/validate/check_enterprise_readiness.php" ]; then + SCRIPT="/tmp/mokostandards/api/validate/check_enterprise_readiness.php" + fi + + if [ -n "$SCRIPT" ]; then + php "$SCRIPT" --verbose | tee /tmp/enterprise-check.log + EXIT_CODE=$? + + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + cat /tmp/enterprise-check.log >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + + if [ "$EXIT_CODE" -eq 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "โœ… Repository meets enterprise readiness criteria!" >> $GITHUB_STEP_SUMMARY + exit 0 + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "โš ๏ธ Enterprise readiness issues detected" >> $GITHUB_STEP_SUMMARY + echo "**Note:** This is informational - review recommendations to improve" >> $GITHUB_STEP_SUMMARY + exit 0 # Non-blocking + fi + else + echo "โ„น๏ธ Enterprise readiness check script not found - skipping" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + repository-health: + name: Repository Health Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Set up PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: '8.1' + extensions: json, mbstring + tools: composer + coverage: none + + - name: Install API Package + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' + run: | + if [ -f "composer.json" ]; then + composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader + else + echo "No composer.json โ€” pulling MokoStandards tools" + if [ ! -d "/tmp/mokostandards" ]; then + git clone --depth 1 --branch version/04 --quiet \ + "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ + /tmp/mokostandards 2>/dev/null || true + if [ -f "/tmp/mokostandards/composer.json" ]; then + cd /tmp/mokostandards && composer install --no-dev --no-interaction --quiet 2>/dev/null || true + cd - + fi + fi + fi + + - name: Check Repository Health + id: health_check + run: | + echo "" >> $GITHUB_STEP_SUMMARY + + SCRIPT="" + if [ -f "api/validate/check_repo_health.php" ]; then + SCRIPT="api/validate/check_repo_health.php" + elif [ -f "/tmp/mokostandards/api/validate/check_repo_health.php" ]; then + SCRIPT="/tmp/mokostandards/api/validate/check_repo_health.php" + fi + + if [ -n "$SCRIPT" ]; then + php "$SCRIPT" --verbose | tee /tmp/health-check.log + EXIT_CODE=$? + + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + cat /tmp/health-check.log >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + + if [ "$EXIT_CODE" -eq 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "โœ… Repository health check passed!" >> $GITHUB_STEP_SUMMARY + exit 0 + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "โš ๏ธ Repository health issues detected" >> $GITHUB_STEP_SUMMARY + echo "**Note:** This is informational - review recommendations to improve" >> $GITHUB_STEP_SUMMARY + exit 0 # Non-blocking + fi + else + echo "โ„น๏ธ Repository health check script not found - skipping" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + terraform-validation: + name: Terraform Configuration Validation + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup Terraform + uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0 + with: + terraform_version: "1.0" + + - name: Validate Terraform Files + run: | + set -x + echo "## ๐Ÿ—๏ธ Terraform Configuration Validation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Check if terraform files exist + TF_COUNT=$(find . -name "*.tf" -type f | wc -l || echo 0) + + if [ "$TF_COUNT" -eq 0 ]; then + echo "โ„น๏ธ No Terraform files found in repository" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + echo "**Terraform Files Found**: $TF_COUNT" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Validation Results + VALIDATION_PASSED=true + WARNINGS=0 + ERRORS=0 + + # 1. Check .github/config.tf location (not root override files) + echo "### Override Configuration Check" >> $GITHUB_STEP_SUMMARY + LEGACY_OVERRIDES=$(find . -maxdepth 1 -name "*override*.tf" -o -name "MokoStandards.override.tf" 2>/dev/null | wc -l || echo 0) + if [ "$LEGACY_OVERRIDES" -gt 0 ]; then + echo "โš ๏ธ Found legacy override files in root directory" >> $GITHUB_STEP_SUMMARY + echo "**Expected Location**: .github/config.tf" >> $GITHUB_STEP_SUMMARY + echo "**Legacy files found**: $LEGACY_OVERRIDES" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + else + if [ -f ".github/config.tf" ]; then + echo "โœ… Override configuration in correct location (.github/config.tf)" >> $GITHUB_STEP_SUMMARY + else + echo "โ„น๏ธ No override configuration found" >> $GITHUB_STEP_SUMMARY + fi + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # 2. Terraform Syntax Validation + echo "### Terraform Syntax Validation" >> $GITHUB_STEP_SUMMARY + SYNTAX_ERRORS=0 + + # Find all directories with terraform files + for dir in $(find . -name "*.tf" -type f -exec dirname {} \; | sort -u); do + cd "$dir" || continue + echo "Validating: $dir" >> $GITHUB_STEP_SUMMARY + + # Initialize without backend + terraform init -backend=false > /dev/null 2>&1 || true + + # Validate + if terraform validate -no-color > /tmp/tf_validate.txt 2>&1; then + echo " โœ… Syntax valid" >> $GITHUB_STEP_SUMMARY + else + echo " โŒ Syntax errors found" >> $GITHUB_STEP_SUMMARY + cat /tmp/tf_validate.txt >> $GITHUB_STEP_SUMMARY + SYNTAX_ERRORS=$((SYNTAX_ERRORS + 1)) + VALIDATION_PASSED=false + fi + cd - > /dev/null + done + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$SYNTAX_ERRORS" -eq 0 ]; then + echo "โœ… All Terraform files have valid syntax" >> $GITHUB_STEP_SUMMARY + else + echo "โŒ Found $SYNTAX_ERRORS directories with syntax errors" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + SYNTAX_ERRORS)) + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # 3. Terraform Formatting Check + echo "### Terraform Formatting Check" >> $GITHUB_STEP_SUMMARY + FORMAT_ISSUES=0 + + for tf_file in $(find . -name "*.tf" -type f); do + if ! terraform fmt -check=true -no-color "$tf_file" > /dev/null 2>&1; then + FORMAT_ISSUES=$((FORMAT_ISSUES + 1)) + fi + done + + if [ "$FORMAT_ISSUES" -eq 0 ]; then + echo "โœ… All Terraform files properly formatted" >> $GITHUB_STEP_SUMMARY + else + echo "โš ๏ธ Found $FORMAT_ISSUES files with formatting issues" >> $GITHUB_STEP_SUMMARY + echo "**Fix**: Run \`terraform fmt -recursive\`" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # 4. Check for file_metadata blocks + echo "### File Metadata Validation" >> $GITHUB_STEP_SUMMARY + MISSING_METADATA=0 + + for tf_file in $(find . -name "*.tf" -type f); do + if ! grep -q "file_metadata" "$tf_file"; then + MISSING_METADATA=$((MISSING_METADATA + 1)) + fi + done + + if [ "$MISSING_METADATA" -eq 0 ]; then + echo "โœ… All Terraform files contain file_metadata block" >> $GITHUB_STEP_SUMMARY + else + echo "โš ๏ธ Found $MISSING_METADATA files missing file_metadata block" >> $GITHUB_STEP_SUMMARY + echo "**Reference**: docs/policy/terraform-file-standards.md" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # 5. Version Consistency Check + echo "### Version Consistency Check" >> $GITHUB_STEP_SUMMARY + VERSION_MISMATCHES=0 + EXPECTED_VERSION="04.00.04" + + for tf_file in $(find . -name "*.tf" -type f); do + if grep -q "version.*=" "$tf_file"; then + if ! grep -q "version.*=.*\"$EXPECTED_VERSION\"" "$tf_file"; then + VERSION_MISMATCHES=$((VERSION_MISMATCHES + 1)) + fi + fi + done + + if [ "$VERSION_MISMATCHES" -eq 0 ]; then + echo "โœ… All Terraform file versions match $EXPECTED_VERSION" >> $GITHUB_STEP_SUMMARY + else + echo "โš ๏ธ Found $VERSION_MISMATCHES files with version mismatches" >> $GITHUB_STEP_SUMMARY + echo "**Expected Version**: $EXPECTED_VERSION" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # 6. Copyright Header Check + echo "### Copyright Header Check" >> $GITHUB_STEP_SUMMARY + MISSING_COPYRIGHT=0 + + for tf_file in $(find . -name "*.tf" -type f); do + if ! grep -q "Copyright (C)" "$tf_file"; then + MISSING_COPYRIGHT=$((MISSING_COPYRIGHT + 1)) + fi + done + + if [ "$MISSING_COPYRIGHT" -eq 0 ]; then + echo "โœ… All Terraform files have copyright headers" >> $GITHUB_STEP_SUMMARY + else + echo "โš ๏ธ Found $MISSING_COPYRIGHT files missing copyright headers" >> $GITHUB_STEP_SUMMARY + echo "**Reference**: docs/policy/terraform-file-standards.md" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # Summary + echo "---" >> $GITHUB_STEP_SUMMARY + echo "### Validation Summary" >> $GITHUB_STEP_SUMMARY + echo "**Total Files**: $TF_COUNT" >> $GITHUB_STEP_SUMMARY + echo "**Errors**: $ERRORS" >> $GITHUB_STEP_SUMMARY + echo "**Warnings**: $WARNINGS" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$VALIDATION_PASSED" = true ] && [ "$ERRORS" -eq 0 ]; then + echo "โœ… **Terraform Validation: PASSED**" >> $GITHUB_STEP_SUMMARY + exit 0 + elif [ "$ERRORS" -gt 0 ]; then + echo "โŒ **Terraform Validation: FAILED**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Note**: This is an informational check and does not block merges" >> $GITHUB_STEP_SUMMARY + exit 0 # Informational only + else + echo "โš ๏ธ **Terraform Validation: PASSED WITH WARNINGS**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Note**: This is an informational check and does not block merges" >> $GITHUB_STEP_SUMMARY + exit 0 # Informational only + fi + + summary: + name: Compliance Summary + runs-on: ubuntu-latest + needs: [ + repository-structure, documentation-quality, coding-standards, line-length-validation, license-compliance, git-hygiene, workflow-validation, version-consistency, script-integrity, enterprise-readiness, repository-health, + todo-fixme-tracking, file-size-limits, secret-scanning, broken-link-detection, + dependency-vulnerabilities, code-duplication, unused-dependencies, readme-completeness, + code-complexity, api-documentation, insecure-patterns, binary-file-detection, + dead-code-detection, file-naming-standards, accessibility-check, performance-metrics, terraform-validation + ] + if: always() + + steps: + - name: Generate Compliance Report + run: | + set -x + echo "# ๐Ÿ“Š MokoStandards Compliance Report" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Calculate overall status + REPO_STATUS="${{ needs.repository-structure.result }}" + DOCS_STATUS="${{ needs.documentation-quality.result }}" + CODE_STATUS="${{ needs.coding-standards.result }}" + LINE_LENGTH_STATUS="${{ needs.line-length-validation.result }}" + LICENSE_STATUS="${{ needs.license-compliance.result }}" + GIT_STATUS="${{ needs.git-hygiene.result }}" + WORKFLOW_STATUS="${{ needs.workflow-validation.result }}" + VERSION_STATUS="${{ needs.version-consistency.result }}" + SCRIPT_STATUS="${{ needs.script-integrity.result }}" + ENTERPRISE_STATUS="${{ needs.enterprise-readiness.result }}" + HEALTH_STATUS="${{ needs.repository-health.result }}" + TERRAFORM_STATUS="${{ needs.terraform-validation.result }}" + + PASSED=0 + FAILED=0 + WARNINGS=0 + TOTAL=28 + + # Critical checks (must pass) + [ "$REPO_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) + [ "$DOCS_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) + [ "$CODE_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) + [ "$LICENSE_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) + [ "$GIT_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) + [ "$WORKFLOW_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) + [ "$VERSION_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) + [ "$SCRIPT_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) + + # Informational checks (don't fail build) + if [ "$ENTERPRISE_STATUS" = "success" ]; then + PASSED=$((PASSED + 1)) + else + WARNINGS=$((WARNINGS + 1)) + fi + + if [ "$HEALTH_STATUS" = "success" ]; then + PASSED=$((PASSED + 1)) + else + WARNINGS=$((WARNINGS + 1)) + fi + + if [ "$TERRAFORM_STATUS" = "success" ]; then + PASSED=$((PASSED + 1)) + else + WARNINGS=$((WARNINGS + 1)) + fi + + # Adjust total to only count critical checks for compliance percentage + CRITICAL_TOTAL=8 + CRITICAL_PASSED=$((PASSED - WARNINGS)) + COMPLIANCE_PERCENT=$((CRITICAL_PASSED * 100 / CRITICAL_TOTAL)) + + # Overall status badge + if [ "$COMPLIANCE_PERCENT" -eq 100 ]; then + echo "## โœ… Overall Status: **COMPLIANT** ($COMPLIANCE_PERCENT%)" >> $GITHUB_STEP_SUMMARY + elif [ "$COMPLIANCE_PERCENT" -ge 80 ]; then + echo "## โš ๏ธ Overall Status: **MOSTLY COMPLIANT** ($COMPLIANCE_PERCENT%)" >> $GITHUB_STEP_SUMMARY + elif [ "$COMPLIANCE_PERCENT" -ge 50 ]; then + echo "## โš ๏ธ Overall Status: **PARTIALLY COMPLIANT** ($COMPLIANCE_PERCENT%)" >> $GITHUB_STEP_SUMMARY + else + echo "## โŒ Overall Status: **NON-COMPLIANT** ($COMPLIANCE_PERCENT%)" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Critical Checks:** $CRITICAL_PASSED/$CRITICAL_TOTAL passed" >> $GITHUB_STEP_SUMMARY + echo "**Total Checks:** $PASSED/$TOTAL passed" >> $GITHUB_STEP_SUMMARY + if [ "$WARNINGS" -gt 0 ]; then + echo "**Informational:** $WARNINGS warning(s)" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # Progress bar + FILLED=$((COMPLIANCE_PERCENT / 5)) + EMPTY=$((20 - FILLED)) + BAR="" + for i in $(seq 1 $FILLED); do BAR="${BAR}โ–ˆ"; done + for i in $(seq 1 $EMPTY); do BAR="${BAR}โ–‘"; done + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "$BAR $COMPLIANCE_PERCENT%" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Detailed breakdown + echo "## Validation Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Area | Status | Result | Priority |" >> $GITHUB_STEP_SUMMARY + echo "|------|--------|--------|----------|" >> $GITHUB_STEP_SUMMARY + + # Repository Structure + if [ "$REPO_STATUS" = "success" ]; then + echo "| ๐Ÿ“ Repository Structure | โœ… Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY + else + echo "| ๐Ÿ“ Repository Structure | โŒ Fail | **Action Required** | ๐Ÿ”ด Critical |" >> $GITHUB_STEP_SUMMARY + fi + + # Documentation Quality + if [ "$DOCS_STATUS" = "success" ]; then + echo "| ๐Ÿ“š Documentation Quality | โœ… Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY + else + echo "| ๐Ÿ“š Documentation Quality | โŒ Fail | **Action Required** | ๐Ÿ”ด Critical |" >> $GITHUB_STEP_SUMMARY + fi + + # Coding Standards + if [ "$CODE_STATUS" = "success" ]; then + echo "| ๐Ÿ’ป Coding Standards | โœ… Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY + else + echo "| ๐Ÿ’ป Coding Standards | โš ๏ธ Warning | Review Recommended | ๐ŸŸก Medium |" >> $GITHUB_STEP_SUMMARY + fi + + # License Compliance + if [ "$LICENSE_STATUS" = "success" ]; then + echo "| โš–๏ธ License Compliance | โœ… Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY + else + echo "| โš–๏ธ License Compliance | โŒ Fail | **Action Required** | ๐Ÿ”ด Critical |" >> $GITHUB_STEP_SUMMARY + fi + + # Git Hygiene + if [ "$GIT_STATUS" = "success" ]; then + echo "| ๐Ÿงน Git Repository Hygiene | โœ… Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY + else + echo "| ๐Ÿงน Git Repository Hygiene | โš ๏ธ Warning | Review Recommended | ๐ŸŸก Medium |" >> $GITHUB_STEP_SUMMARY + fi + + # Workflow Configuration + if [ "$WORKFLOW_STATUS" = "success" ]; then + echo "| โš™๏ธ Workflow Configuration | โœ… Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY + else + echo "| โš™๏ธ Workflow Configuration | โš ๏ธ Warning | Review Recommended | ๐ŸŸก Medium |" >> $GITHUB_STEP_SUMMARY + fi + + # Version Consistency + if [ "$VERSION_STATUS" = "success" ]; then + echo "| ๐Ÿ”ข Version Consistency | โœ… Pass | All versions match | - |" >> $GITHUB_STEP_SUMMARY + else + echo "| ๐Ÿ”ข Version Consistency | โŒ Fail | **Action Required** | ๐Ÿ”ด Critical |" >> $GITHUB_STEP_SUMMARY + fi + + # Script Integrity + if [ "$SCRIPT_STATUS" = "success" ]; then + echo "| ๐Ÿ” Script Integrity | โœ… Pass | SHA hashes validated | - |" >> $GITHUB_STEP_SUMMARY + else + echo "| ๐Ÿ” Script Integrity | โŒ Fail | **Action Required** | ๐Ÿ”ด Critical |" >> $GITHUB_STEP_SUMMARY + fi + + # Enterprise Readiness (Informational) + if [ "$ENTERPRISE_STATUS" = "success" ]; then + echo "| ๐Ÿข Enterprise Readiness | โœ… Pass | Ready for enterprise | โ„น๏ธ Info |" >> $GITHUB_STEP_SUMMARY + else + echo "| ๐Ÿข Enterprise Readiness | โ„น๏ธ Info | Review suggestions | โ„น๏ธ Info |" >> $GITHUB_STEP_SUMMARY + fi + + # Repository Health (Informational) + if [ "$HEALTH_STATUS" = "success" ]; then + echo "| ๐Ÿฅ Repository Health | โœ… Pass | Health check passed | โ„น๏ธ Info |" >> $GITHUB_STEP_SUMMARY + else + echo "| ๐Ÿฅ Repository Health | โ„น๏ธ Info | Review recommendations | โ„น๏ธ Info |" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + + # Action items summary + if [ "$FAILED" -gt 0 ]; then + echo "## โšก Action Items" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**$FAILED validation area(s) require attention:**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + [ "$REPO_STATUS" != "success" ] && echo "- ๐Ÿ”ด **Critical:** Fix repository structure issues" >> $GITHUB_STEP_SUMMARY + [ "$DOCS_STATUS" != "success" ] && echo "- ๐Ÿ”ด **Critical:** Improve documentation quality" >> $GITHUB_STEP_SUMMARY + [ "$LICENSE_STATUS" != "success" ] && echo "- ๐Ÿ”ด **Critical:** Resolve license compliance issues" >> $GITHUB_STEP_SUMMARY + [ "$CODE_STATUS" != "success" ] && echo "- ๐ŸŸก **Medium:** Review coding standards violations" >> $GITHUB_STEP_SUMMARY + [ "$GIT_STATUS" != "success" ] && echo "- ๐ŸŸก **Medium:** Address git repository hygiene items" >> $GITHUB_STEP_SUMMARY + [ "$WORKFLOW_STATUS" != "success" ] && echo "- ๐ŸŸก **Medium:** Review workflow configuration" >> $GITHUB_STEP_SUMMARY + + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Next Steps:**" >> $GITHUB_STEP_SUMMARY + echo "1. Review detailed results in individual job outputs above" >> $GITHUB_STEP_SUMMARY + echo "2. Follow remediation steps provided for each failure" >> $GITHUB_STEP_SUMMARY + echo "3. Re-run this workflow after making corrections" >> $GITHUB_STEP_SUMMARY + echo "4. Reach 100% compliance before merging" >> $GITHUB_STEP_SUMMARY + else + echo "## ๐ŸŽ‰ Excellent!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Your repository is **fully compliant** with MokoStandards!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Achievements:**" >> $GITHUB_STEP_SUMMARY + echo "- โœ… All required directories and files present" >> $GITHUB_STEP_SUMMARY + echo "- โœ… Documentation meets quality standards" >> $GITHUB_STEP_SUMMARY + echo "- โœ… Coding standards followed" >> $GITHUB_STEP_SUMMARY + echo "- โœ… License compliance verified" >> $GITHUB_STEP_SUMMARY + echo "- โœ… Git repository well-maintained" >> $GITHUB_STEP_SUMMARY + echo "- โœ… Workflows properly configured" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "---" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "๐Ÿ“š **Resources:**" >> $GITHUB_STEP_SUMMARY + echo "- [MokoStandards Documentation](https://github.com/mokoconsulting-tech/MokoStandards)" >> $GITHUB_STEP_SUMMARY + echo "- [Repository Structure Guide](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/docs/policy/core-structure.md)" >> $GITHUB_STEP_SUMMARY + echo "- [Documentation Standards](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/docs/policy/document-formatting.md)" >> $GITHUB_STEP_SUMMARY + echo "- [Coding Standards](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/docs/policy/coding-style-guide.md)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "_Generated by MokoStandards Compliance Workflow v${WORKFLOW_VERSION}_" >> $GITHUB_STEP_SUMMARY + + # Create tracking issue for non-compliance if on push + if [ "$COMPLIANCE_PERCENT" -lt 100 ] && [ "${{ github.event_name }}" = "push" ]; then + echo "Creating tracking issue for standards violations..." + fi + + # Exit with error if not fully compliant + if [ "$COMPLIANCE_PERCENT" -lt 100 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "### โŒ Standards Compliance Failed" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Overall Compliance:** $COMPLIANCE_PERCENT%" >> $GITHUB_STEP_SUMMARY + echo "**Status:** Repository does not meet 100% compliance requirement" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Review and fix all validation failures above" >> $GITHUB_STEP_SUMMARY + echo "" + echo "โŒ ERROR: Standards compliance at $COMPLIANCE_PERCENT% - 100% required" + exit 1 + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "### โœ… Full Standards Compliance Achieved" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Overall Compliance:** 100%" >> $GITHUB_STEP_SUMMARY + echo "**Status:** Repository meets all MokoStandards requirements" >> $GITHUB_STEP_SUMMARY + echo "" + echo "โœ… SUCCESS: Repository is fully MokoStandards compliant" + + - name: Create or reopen tracking issue for standards violations + if: failure() + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + REPO="${{ github.repository }}" + RUN_URL="${{ github.server_url }}/${REPO}/actions/runs/${{ github.run_id }}" + DATE=$(date -u '+%Y-%m-%d') + SHA="${{ github.sha }}" + ACTOR="${{ github.actor }}" + BRANCH="${{ github.ref_name }}" + + # Collect failed checks + FAILED="" + [ "${{ needs.repository-structure.result }}" != "success" ] && FAILED="${FAILED}\n- Repository Structure" + [ "${{ needs.documentation-quality.result }}" != "success" ] && FAILED="${FAILED}\n- Documentation Quality" + [ "${{ needs.coding-standards.result }}" != "success" ] && FAILED="${FAILED}\n- Coding Standards" + [ "${{ needs.license-compliance.result }}" != "success" ] && FAILED="${FAILED}\n- License Compliance" + [ "${{ needs.git-hygiene.result }}" != "success" ] && FAILED="${FAILED}\n- Git Hygiene" + [ "${{ needs.workflow-validation.result }}" != "success" ] && FAILED="${FAILED}\n- Workflow Validation" + [ "${{ needs.version-consistency.result }}" != "success" ] && FAILED="${FAILED}\n- Version Consistency" + [ "${{ needs.script-integrity.result }}" != "success" ] && FAILED="${FAILED}\n- Script Integrity" + [ "${{ needs.secret-scanning.result }}" != "success" ] && FAILED="${FAILED}\n- Secret Scanning" + [ "${{ needs.line-length-validation.result }}" != "success" ] && FAILED="${FAILED}\n- Line Length" + [ "${{ needs.file-size-limits.result }}" != "success" ] && FAILED="${FAILED}\n- File Size Limits" + [ "${{ needs.readme-completeness.result }}" != "success" ] && FAILED="${FAILED}\n- README Completeness" + + if [ -z "$FAILED" ]; then + echo "No failed checks to report" + exit 0 + fi + + TITLE="[Standards] Compliance violations โ€” ${DATE}" + BODY="## Standards Compliance Violations + + | Field | Value | + |-------|-------| + | **Branch** | \`${BRANCH}\` | + | **Commit** | \`${SHA:0:7}\` | + | **Actor** | @${ACTOR} | + | **Run** | [View workflow](${RUN_URL}) | + + ### Failed Checks + $(printf '%b' "$FAILED") + + ### Required Actions + 1. Review the [workflow run](${RUN_URL}) for details + 2. Fix each failed check + 3. Push to trigger a new scan + + --- + *Auto-created by standards-compliance workflow*" + + BODY=$(echo "$BODY" | sed 's/^ //') + LABEL="standards-violation" + + gh label create "$LABEL" --repo "$REPO" --color "D73A4A" --description "Standards compliance failure" --force 2>/dev/null || true + + EXISTING=$(gh api "repos/${REPO}/issues?labels=${LABEL}&state=all&per_page=1&sort=created&direction=desc" \ + --jq '.[0].number' 2>/dev/null) + + if [ -n "$EXISTING" ] && [ "$EXISTING" != "null" ]; then + gh api "repos/${REPO}/issues/${EXISTING}" -X PATCH \ + -f title="$TITLE" -f body="$BODY" -f state="open" --silent + echo "Updated issue #${EXISTING}" + else + gh issue create --repo "$REPO" --title "$TITLE" --body "$BODY" \ + --label "$LABEL" --assignee "jmiller-moko" + fi + +# CUSTOMIZATION: +# +# 1. Adjust severity of checks (convert warnings to errors or vice versa) +# 2. Add project-specific validation rules +# 3. Integrate with custom linting tools +# 4. Add notification steps for compliance failures +# 5. Customize required files/directories for your project type + diff --git a/.gitea/workflows/sync-version-on-merge.yml b/.gitea/workflows/sync-version-on-merge.yml new file mode 100644 index 0000000..60715f6 --- /dev/null +++ b/.gitea/workflows/sync-version-on-merge.yml @@ -0,0 +1,133 @@ +# Copyright (C) 2026 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: GitHub.Workflow +# INGROUP: MokoStandards.Automation +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /templates/workflows/shared/sync-version-on-merge.yml.template +# VERSION: 04.06.00 +# BRIEF: Auto-bump patch version on every push to main and propagate to all file headers +# NOTE: Synced via bulk-repo-sync to .github/workflows/sync-version-on-merge.yml in all governed repos. +# README.md is the single source of truth for the repository version. + +name: Sync Version from README + +on: + push: + branches: + - main + - master + workflow_dispatch: + inputs: + dry_run: + description: 'Dry run (preview only, no commit)' + type: boolean + default: false + +permissions: + contents: write + issues: write + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + sync-version: + name: Propagate README version + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.GH_TOKEN || github.token }} + fetch-depth: 0 + + - name: Set up PHP + uses: shivammathur/setup-php@fcafdd6392932010c2bd5094439b8e33be2a8a09 # v2.37.0 + with: + php-version: '8.1' + tools: composer + + - name: Setup MokoStandards tools + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' + run: | + git clone --depth 1 --branch version/04 --quiet \ + "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ + /tmp/mokostandards + cd /tmp/mokostandards + composer install --no-dev --no-interaction --quiet + + - name: Auto-bump patch version + if: ${{ github.event_name == 'push' && github.actor != 'github-actions[bot]' }} + run: | + if git diff --name-only HEAD~1 HEAD 2>/dev/null | grep -q '^README\.md$'; then + echo "README.md changed in this push โ€” skipping auto-bump" + exit 0 + fi + + RESULT=$(php /tmp/mokostandards/api/cli/version_bump.php --path .) || { + echo "โš ๏ธ Could not bump version โ€” skipping" + exit 0 + } + echo "Auto-bumping patch: $RESULT" + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add README.md + git commit -m "chore(version): auto-bump patch ${RESULT} [skip ci]" \ + --author="github-actions[bot] " + git push + + - name: Extract version from README.md + id: readme_version + run: | + git pull --ff-only 2>/dev/null || true + VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null) + if [ -z "$VERSION" ]; then + echo "โš ๏ธ No VERSION in README.md โ€” skipping propagation" + echo "skip=true" >> $GITHUB_OUTPUT + exit 0 + fi + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "skip=false" >> $GITHUB_OUTPUT + echo "โœ… README.md version: $VERSION" + + - name: Run version sync + if: ${{ steps.readme_version.outputs.skip != 'true' && inputs.dry_run != true }} + run: | + php /tmp/mokostandards/api/maintenance/update_version_from_readme.php \ + --path . \ + --create-issue \ + --repo "${{ github.repository }}" + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + + - name: Commit updated files + if: ${{ steps.readme_version.outputs.skip != 'true' && inputs.dry_run != true }} + run: | + git pull --ff-only 2>/dev/null || true + if git diff --quiet; then + echo "โ„น๏ธ No version changes needed โ€” already up to date" + exit 0 + fi + VERSION="${{ steps.readme_version.outputs.version }}" + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add -A + git commit -m "chore(version): sync badges and headers to ${VERSION} [skip ci]" \ + --author="github-actions[bot] " + git push + + - name: Summary + run: | + VERSION="${{ steps.readme_version.outputs.version }}" + echo "## ๐Ÿ“ฆ Version Sync โ€” ${VERSION}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Source:** \`README.md\` FILE INFORMATION block" >> $GITHUB_STEP_SUMMARY + echo "**Version:** \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY diff --git a/.github/CLAUDE.md b/.github/CLAUDE.md index 667c3b4..4838bc7 100644 --- a/.github/CLAUDE.md +++ b/.github/CLAUDE.md @@ -307,3 +307,13 @@ This repository is governed by [MokoStandards](https://github.com/mokoconsulting | [changelog-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md | | [scripting-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/scripting-standards.md) | PHP script requirements and CliFramework usage | | [package-installation.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/package-installation.md) | Installing `mokoconsulting/mokostandards` via Composer | + + +## Release and Infrastructure Standards + +- **Release tags**: All repos must have the 5 standard tags: development, alpha, beta, release-candidate, stable +- **Update server priority**: Gitea must be priority 1, GitHub priority 2 in updateservers +- **Secrets**: All repos have GA_TOKEN and GH_TOKEN as Actions secrets +- **Branch protection**: main branch is protected, only jmiller can push directly +- **Push mirrors**: All repos mirror to GitHub via built-in push mirror with sync_on_commit + diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 667c3b4..0eb1f39 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -307,3 +307,10 @@ This repository is governed by [MokoStandards](https://github.com/mokoconsulting | [changelog-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md | | [scripting-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/scripting-standards.md) | PHP script requirements and CliFramework usage | | [package-installation.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/package-installation.md) | Installing `mokoconsulting/mokostandards` via Composer | + + +## Release and Infrastructure Standards +- Release tags: development, alpha, beta, release-candidate, stable +- Update servers: Gitea priority 1, GitHub priority 2 +- Secrets: GA_TOKEN for Gitea, GH_TOKEN for GitHub on all repos + -- 2.52.0 From 4c8ce365e086f0f4c65f134c8bffba3b52b28c45 Mon Sep 17 00:00:00 2001 From: jmiller Date: Wed, 22 Apr 2026 09:10:09 +0000 Subject: [PATCH 02/16] docs: update all documentation to current standards - Copyright year 2025 -> 2026 - Primary URLs: GitHub -> Gitea - CONTRIBUTING.md: add Infrastructure Standards section - SECURITY.md: add Gitea issue reporting - README.md: update badge URLs to Gitea - Issue templates: update URLs to Gitea Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/ISSUE_TEMPLATE/config.yml | 4 ++-- .github/copilot/README.md | 2 +- CHANGELOG.md | 2 +- CODE_OF_CONDUCT.md | 2 +- CONTRIBUTING.md | 38 +++++++++++++++++++++++++++++-- README.md | 6 ++--- SECURITY.md | 5 ++++ docs/templates/README-template.md | 2 +- 8 files changed, 50 insertions(+), 11 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 7edc8bc..e9f2f60 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -8,10 +8,10 @@ contact_links: url: https://mokoconsulting.tech/ about: Get help or ask questions through our website - name: ๐Ÿ“š MokoStandards Documentation - url: https://github.com/mokoconsulting-tech/MokoStandards + url: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards about: View our coding standards and best practices - name: ๐Ÿ”’ Report a Security Vulnerability - url: https://github.com/mokoconsulting-tech/.github-private/security/advisories/new + url: https://git.mokoconsulting.tech/MokoConsulting/.github-private/security/advisories/new about: Report security vulnerabilities privately (for critical issues) - name: ๐Ÿ’ก Community Discussions url: https://github.com/orgs/mokoconsulting-tech/discussions diff --git a/.github/copilot/README.md b/.github/copilot/README.md index df614c6..1007077 100644 --- a/.github/copilot/README.md +++ b/.github/copilot/README.md @@ -1,4 +1,4 @@ - -[![Version](https://img.shields.io/badge/version-00.00.01-blue.svg?logo=v&logoColor=white)](https://github.com/mokoconsulting-tech/MokoStandards-Template-Generic/releases/tag/v00) +[![Version](https://img.shields.io/badge/version-00.00.01-blue.svg?logo=v&logoColor=white)](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-Template-Generic/releases/tag/v00) [![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green.svg?logo=gnu&logoColor=white)](LICENSE) [![PHP](https://img.shields.io/badge/PHP-8.1%2B-777BB4.svg?logo=php&logoColor=white)](https://www.php.net) diff --git a/SECURITY.md b/SECURITY.md index e5823f3..0b5cff1 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -45,6 +45,11 @@ Security updates are provided for the following versions: Only the current major version receives security updates. Users should upgrade to the latest supported version to receive security patches. ## Reporting a Vulnerability + +Report security vulnerabilities via Gitea issue (preferred): +https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-Template-Generic/issues/new?template=security.yaml + +Or email: hello@mokoconsulting.tech ### Where to Report diff --git a/docs/templates/README-template.md b/docs/templates/README-template.md index a7f4ea4..b55ea07 100644 --- a/docs/templates/README-template.md +++ b/docs/templates/README-template.md @@ -1,4 +1,4 @@ - -[![Version](https://img.shields.io/badge/version-00.00.01-blue.svg?logo=v&logoColor=white)](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-Template-Generic/releases/tag/v00) +[![Version](https://img.shields.io/badge/version-00.00.01-blue.svg?logo=v&logoColor=white)](https://git.mokoconsulting.tech/MokoConsulting/joomla-api-mcp/releases/tag/v00) [![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green.svg?logo=gnu&logoColor=white)](LICENSE) -[![PHP](https://img.shields.io/badge/PHP-8.1%2B-777BB4.svg?logo=php&logoColor=white)](https://www.php.net) +[![Node](https://img.shields.io/badge/Node.js-20%2B-339933.svg?logo=node.js&logoColor=white)](https://nodejs.org) -# MokoStandards-Template-Generic +# joomla-api-mcp [![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme) -A template repository for generic coding projects that follows MokoStandards conventions. +A Model Context Protocol (MCP) server that exposes the Joomla 4/5/6 Web Services API as tools for AI assistants. -This template provides a standardized structure for any coding project, including proper documentation, licensing, contribution guidelines, and project organization. It is designed to help you quickly bootstrap new projects with best practices and consistent conventions. +Connects to Joomla instances over HTTP using the built-in REST API (`/api/index.php/v1`) and API token authentication. Claude, Cursor, and other MCP clients can manage articles, categories, users, menus, plugins, and more โ€” without SSH or direct server access. ## Table of Contents - [Background](#background) - [Install](#install) - [Usage](#usage) -- [Structure](#structure) +- [Tools](#tools) +- [Configuration](#configuration) - [Contributing](#contributing) - [License](#license) - [Maintainers](#maintainers) ## Background -MokoStandards-Template-Generic is a repository template designed to provide a consistent foundation for generic coding projects. It includes: +Joomla 4+ ships with a full Web Services API that supports content management, user administration, and site configuration over REST. This MCP server wraps that API into structured tools, enabling: -- Standard documentation structure (README, LICENSE, CONTRIBUTING, CODE_OF_CONDUCT, CHANGELOG) -- MokoStandards-compliant file headers and metadata -- EditorConfig for consistent coding styles across editors -- Git configuration templates -- Documentation index system for easy navigation +- Article management (list, create, update, delete, publish) +- Category management (list, create, update, delete) +- User administration (list, create, update, delete, groups) +- Menu and menu item inspection +- Plugin listing and enable/disable toggling +- Module and template listing +- Tag, contact, banner, and newsfeed management +- Media file browsing +- Application configuration read/write +- Private messaging +- Custom field inspection +- Raw API passthrough for any endpoint -This template follows the [standard-readme](https://github.com/RichardLitt/standard-readme) specification and incorporates MokoStandards conventions for enterprise-grade project organization. +Supports multiple named connections for managing several Joomla instances from a single MCP server. ## Install -To use this template: - -1. Click the "Use this template" button on GitHub -2. Create a new repository from this template -3. Clone your new repository locally: - ```sh -git clone https://github.com/your-username/your-new-repo.git -cd your-new-repo +git clone https://git.mokoconsulting.tech/MokoConsulting/joomla-api-mcp.git +cd joomla-api-mcp +npm install +npm run build +npm run setup ``` -4. Update the project-specific details: - - Update README.md with your project name and description - - Update LICENSE if using a different license - - Update file headers with appropriate REPO, DEFGROUP, and BRIEF values - - Update CHANGELOG.md with your version history +The setup wizard will prompt for your Joomla site URL and API token. Run it again to add more connections โ€” each site gets its own name, URL, and API key so you can manage multiple Joomla instances from a single MCP server. ## Usage -This template is designed to be customized for your specific project needs. +### Add to Claude Code -### Getting Started +In your Claude Code MCP settings (`~/.claude.json` or project `.mcp.json`): -1. Replace placeholder text with your project details -2. Add your source code to the `src/` directory -3. Add scripts to the `scripts/` directory -4. Add documentation to the `docs/` directory -5. Update the CHANGELOG.md as you make changes - -### Project Structure - -``` -. -โ”œโ”€โ”€ docs/ # Documentation files -โ”œโ”€โ”€ scripts/ # Build and utility scripts -โ”œโ”€โ”€ src/ # Source code -โ”œโ”€โ”€ README.md # This file -โ”œโ”€โ”€ LICENSE # License information -โ”œโ”€โ”€ CONTRIBUTING.md # Contribution guidelines -โ”œโ”€โ”€ CODE_OF_CONDUCT.md # Code of conduct -โ””โ”€โ”€ CHANGELOG.md # Version history +```json +{ + "mcpServers": { + "joomla": { + "command": "node", + "args": ["/path/to/joomla-api-mcp/dist/index.js"] + } + } +} ``` -## Structure +### Configuration -The repository follows MokoStandards conventions: +The easiest way to configure is `npm run setup`, which walks you through it interactively. Run it multiple times to add connections for each Joomla site you manage. -- **Documentation**: All `.md` files include copyright headers and file metadata -- **Index Files**: Each directory contains an `index.md` for navigation -- **EditorConfig**: Maintains consistent coding styles (tabs, width 2) -- **Git Configuration**: Includes `.gitattributes`, `.gitignore`, and `.gitmessage` templates +You can also create the config manually. Copy `config.example.json` to `~/.joomla-api-mcp.json` and edit: + +```sh +cp config.example.json ~/.joomla-api-mcp.json +``` + +Each connection needs the Joomla site's base URL and an API token (generated in Joomla under Users > Edit User > API Token): + +```json +{ + "defaultConnection": "production", + "connections": { + "local-dev": { + "baseUrl": "https://localhost:8080", + "apiToken": "your-joomla-api-token-here", + "insecure": true + }, + "production": { + "baseUrl": "https://www.example.com", + "apiToken": "your-production-api-token" + }, + "staging": { + "baseUrl": "https://staging.example.com", + "apiToken": "your-staging-api-token" + } + } +} +``` + +| Field | Required | Description | +|-------|----------|-------------| +| `baseUrl` | Yes | Joomla site URL (no trailing slash) | +| `apiToken` | Yes | Joomla API token (Bearer auth) | +| `insecure` | No | Skip TLS verification for self-signed certs | + +## Tools + +### Articles + +| Tool | Description | +|------|-------------| +| `joomla_articles_list` | List articles with optional category, state, and search filters | +| `joomla_article_get` | Get a single article by ID | +| `joomla_article_create` | Create a new article | +| `joomla_article_update` | Update an existing article | +| `joomla_article_delete` | Delete an article | + +### Categories + +| Tool | Description | +|------|-------------| +| `joomla_categories_list` | List content categories | +| `joomla_category_create` | Create a new category | +| `joomla_category_update` | Update a category | +| `joomla_category_delete` | Delete a category | + +### Users + +| Tool | Description | +|------|-------------| +| `joomla_users_list` | List users with optional search, group, and state filters | +| `joomla_user_get` | Get a single user by ID | +| `joomla_user_create` | Create a user with auto-generated secure password | +| `joomla_user_update` | Update a user | +| `joomla_user_delete` | Delete a user | +| `joomla_user_groups_list` | List user groups | + +### Menus + +| Tool | Description | +|------|-------------| +| `joomla_menus_list` | List menu types | +| `joomla_menu_items_list` | List menu items for a menu type | +| `joomla_menu_item_get` | Get a single menu item by ID | + +### Plugins + +| Tool | Description | +|------|-------------| +| `joomla_plugins_list` | List plugins with optional type, state, and search filters | +| `joomla_plugin_update` | Enable or disable a plugin | + +### Other + +| Tool | Description | +|------|-------------| +| `joomla_modules_list` | List site or admin modules | +| `joomla_templates_list` | List site or admin templates | +| `joomla_languages_list` | List installed content languages | +| `joomla_tags_list` | List tags | +| `joomla_tag_create` | Create a tag | +| `joomla_fields_list` | List custom fields for a context | +| `joomla_contacts_list` | List contacts | +| `joomla_banners_list` | List banners | +| `joomla_newsfeeds_list` | List newsfeeds | +| `joomla_messages_list` | List private messages | +| `joomla_message_get` | Get a single private message | +| `joomla_media_list` | List media files in a folder | +| `joomla_config_get` | Get application configuration | +| `joomla_config_update` | Update application configuration values | +| `joomla_api_request` | Raw API request to any Joomla endpoint | +| `joomla_list_connections` | List configured connections | + +All tools accept an optional `connection` parameter to target a specific named connection. ## Contributing @@ -118,11 +210,11 @@ This project follows the [Contributor Covenant](CODE_OF_CONDUCT.md) Code of Cond This project is licensed under the GNU General Public License v3.0 or later - see the [LICENSE](LICENSE) file for details. -Copyright ยฉ 2025 Moko Consulting +Copyright ยฉ 2026 Moko Consulting ## Maintainers -[@mokoconsulting-tech](https://github.com/mokoconsulting-tech) +[@mokoconsulting-tech](https://git.mokoconsulting.tech/MokoConsulting) For questions or support, please contact: hello@mokoconsulting.tech @@ -130,4 +222,4 @@ For questions or support, please contact: hello@mokoconsulting.tech | Date | Version | Author | Notes | | --- | --- | --- | --- | -| 2026-01-16 | 0.1.0 | Copilot | Initial MokoStandards-compliant README | +| 2026-04-23 | 0.0.1 | jmiller | Initial MCP server with Joomla Web Services API tools | diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..e74a94c --- /dev/null +++ b/config.example.json @@ -0,0 +1,18 @@ +{ + "defaultConnection": "production", + "connections": { + "local-dev": { + "baseUrl": "https://localhost:8080", + "apiToken": "your-joomla-api-token-here", + "insecure": true + }, + "production": { + "baseUrl": "https://www.example.com", + "apiToken": "your-production-api-token" + }, + "staging": { + "baseUrl": "https://staging.example.com", + "apiToken": "your-staging-api-token" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..86868f9 --- /dev/null +++ b/package.json @@ -0,0 +1,35 @@ +{ + "name": "@mokoconsulting/joomla-api-mcp", + "version": "0.0.1", + "description": "MCP server for Joomla Web Services API operations", + "type": "module", + "main": "dist/index.js", + "bin": { + "joomla-api-mcp": "dist/index.js" + }, + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "start": "node dist/index.js", + "lint": "eslint src/", + "setup": "node scripts/setup.mjs", + "clean": "rm -rf dist/" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.12.1", + "zod": "^3.24.4" + }, + "devDependencies": { + "@types/node": "^22.15.3", + "typescript": "^5.8.3" + }, + "engines": { + "node": ">=20.0.0" + }, + "license": "GPL-3.0-or-later", + "author": "Moko Consulting ", + "repository": { + "type": "git", + "url": "https://git.mokoconsulting.tech/MokoConsulting/joomla-api-mcp.git" + } +} diff --git a/scripts/setup.mjs b/scripts/setup.mjs new file mode 100644 index 0000000..917d7d1 --- /dev/null +++ b/scripts/setup.mjs @@ -0,0 +1,119 @@ +#!/usr/bin/env node +/* Copyright (C) 2026 Moko Consulting + * + * This file is part of a Moko Consulting project. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: joomla-api-mcp.Scripts + * INGROUP: joomla-api-mcp + * REPO: https://git.mokoconsulting.tech/MokoConsulting/joomla-api-mcp + * PATH: /scripts/setup.mjs + * VERSION: 00.00.01 + * BRIEF: Interactive setup โ€” prompts for Joomla API connection details and writes config + */ + +import { createInterface } from 'node:readline/promises'; +import { readFile, writeFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { homedir } from 'node:os'; + +const CONFIG_PATH = resolve(homedir(), '.joomla-api-mcp.json'); + +const rl = createInterface({ input: process.stdin, output: process.stdout }); + +async function prompt(question, defaultValue) { + const suffix = defaultValue ? ` [${defaultValue}]` : ''; + const answer = await rl.question(`${question}${suffix}: `); + return answer.trim() || defaultValue || ''; +} + +async function promptRequired(question) { + let answer = ''; + while (!answer) { + answer = (await rl.question(`${question}: `)).trim(); + if (!answer) { + console.log(' This field is required.'); + } + } + return answer; +} + +async function main() { + console.log(''); + console.log('=== joomla-api-mcp Setup ==='); + console.log(''); + console.log('This will create your configuration file at:'); + console.log(` ${CONFIG_PATH}`); + console.log(''); + + // Check for existing config + let existing = null; + try { + const raw = await readFile(CONFIG_PATH, 'utf-8'); + existing = JSON.parse(raw); + console.log('Existing config found. You can add a new connection or overwrite.'); + console.log(` Current connections: ${Object.keys(existing.connections).join(', ')}`); + console.log(''); + } catch { + // No existing config + } + + const connectionName = await prompt('Connection name', 'production'); + const baseUrl = await promptRequired('Joomla site URL (e.g. https://www.example.com)'); + const apiToken = await promptRequired('Joomla API token'); + + const cleanUrl = baseUrl.replace(/\/+$/, ''); + + const insecureAnswer = await prompt('Skip TLS verification for self-signed certs? (y/N)', 'N'); + const insecure = insecureAnswer.toLowerCase() === 'y'; + + const connection = { baseUrl: cleanUrl, apiToken }; + if (insecure) { + connection.insecure = true; + } + + let config; + if (existing) { + config = existing; + config.connections[connectionName] = connection; + const setDefault = await prompt(`Set "${connectionName}" as default connection? (y/N)`, 'N'); + if (setDefault.toLowerCase() === 'y') { + config.defaultConnection = connectionName; + } + } else { + config = { + defaultConnection: connectionName, + connections: { + [connectionName]: connection, + }, + }; + } + + await writeFile(CONFIG_PATH, JSON.stringify(config, null, '\t') + '\n', 'utf-8'); + + console.log(''); + console.log(`Config written to ${CONFIG_PATH}`); + console.log(` Connection "${connectionName}" configured for ${cleanUrl}`); + console.log(''); + + const addAnother = await prompt('Add another connection? (y/N)', 'N'); + if (addAnother.toLowerCase() === 'y') { + rl.close(); + // Re-run to add another + const { execFileSync } = await import('node:child_process'); + execFileSync('node', [new URL(import.meta.url).pathname], { stdio: 'inherit' }); + return; + } + + console.log('Setup complete. You can now use the MCP server.'); + console.log(''); + rl.close(); +} + +main().catch((err) => { + console.error(`Setup failed: ${err.message}`); + rl.close(); + process.exit(1); +}); diff --git a/src/client.ts b/src/client.ts new file mode 100644 index 0000000..52bf254 --- /dev/null +++ b/src/client.ts @@ -0,0 +1,141 @@ +/* Copyright (C) 2026 Moko Consulting + * + * This file is part of a Moko Consulting project. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: joomla-api-mcp.Client + * INGROUP: joomla-api-mcp + * REPO: https://git.mokoconsulting.tech/MokoConsulting/joomla-api-mcp + * PATH: /src/client.ts + * VERSION: 00.00.01 + * BRIEF: HTTP client for Joomla Web Services API (v1) + */ + +import * as https from 'node:https'; +import * as http from 'node:http'; +import type { JoomlaConnection, ApiResponse } from './types.js'; + +const API_PREFIX = '/api/index.php/v1'; +const TIMEOUT_MS = 30_000; + +export class JoomlaClient { + private readonly base_url: string; + private readonly headers: Record; + private readonly insecure: boolean; + + constructor(conn: JoomlaConnection) { + this.base_url = conn.baseUrl.replace(/\/+$/, '') + API_PREFIX; + this.headers = { + 'Authorization': `Bearer ${conn.apiToken}`, + 'Content-Type': 'application/json', + 'Accept': 'application/vnd.api+json', + }; + this.insecure = conn.insecure ?? false; + } + + async get(endpoint: string, params?: Record): Promise { + const url = this.buildUrl(endpoint, params); + return this.request(url, 'GET'); + } + + async post(endpoint: string, body?: unknown): Promise { + const url = this.buildUrl(endpoint); + return this.request(url, 'POST', body); + } + + async patch(endpoint: string, body: unknown): Promise { + const url = this.buildUrl(endpoint); + return this.request(url, 'PATCH', body); + } + + async delete(endpoint: string): Promise { + const url = this.buildUrl(endpoint); + return this.request(url, 'DELETE'); + } + + private buildUrl(endpoint: string, params?: Record): string { + const path = endpoint.startsWith('/') ? endpoint : `/${endpoint}`; + const url = new URL(`${this.base_url}${path}`); + if (params) { + for (const [key, value] of Object.entries(params)) { + url.searchParams.set(key, value); + } + } + return url.toString(); + } + + private tryParseJson(raw: string): unknown | null { + // Try full string first + try { return JSON.parse(raw); } catch { /* fall through */ } + + // Joomla may append HTML after JSON โ€” find the last } or ] and try parsing up to that point + const trimmed = raw.trimStart(); + if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) return null; + + const closer = trimmed.startsWith('{') ? '}' : ']'; + let idx = raw.lastIndexOf(closer); + while (idx > 0) { + try { + return JSON.parse(raw.substring(0, idx + 1)); + } catch { + idx = raw.lastIndexOf(closer, idx - 1); + } + } + return null; + } + + private request(url: string, method: string, body?: unknown): Promise { + return new Promise((resolve, reject) => { + const parsed = new URL(url); + const is_https = parsed.protocol === 'https:'; + const transport = is_https ? https : http; + + const options: https.RequestOptions = { + hostname: parsed.hostname, + port: parsed.port || (is_https ? 443 : 80), + path: parsed.pathname + parsed.search, + method, + headers: { ...this.headers }, + timeout: TIMEOUT_MS, + }; + + if (this.insecure && is_https) { + options.rejectUnauthorized = false; + } + + const payload = body !== undefined ? JSON.stringify(body) : undefined; + if (payload) { + (options.headers as Record)['Content-Length'] = Buffer.byteLength(payload).toString(); + } + + const req = transport.request(options, (res) => { + const chunks: Buffer[] = []; + res.on('data', (chunk: Buffer) => chunks.push(chunk)); + res.on('end', () => { + const raw = Buffer.concat(chunks).toString('utf-8'); + let data: unknown; + + // Joomla API may return JSON with text/html content-type, + // and may append HTML error fragments after valid JSON. + // Try to parse as JSON, trimming trailing non-JSON content. + data = this.tryParseJson(raw) ?? raw; + + resolve({ status: res.statusCode ?? 0, data }); + }); + }); + + req.on('error', (err) => reject(err)); + req.on('timeout', () => { + req.destroy(); + reject(new Error('Request timed out')); + }); + + if (payload) { + req.write(payload); + } + req.end(); + }); + } +} diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..0f785a5 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,56 @@ +/* Copyright (C) 2026 Moko Consulting + * + * This file is part of a Moko Consulting project. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: joomla-api-mcp.Config + * INGROUP: joomla-api-mcp + * REPO: https://git.mokoconsulting.tech/MokoConsulting/joomla-api-mcp + * PATH: /src/config.ts + * VERSION: 00.00.01 + * BRIEF: Configuration loader for Joomla API MCP connections + */ + +import { readFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { homedir } from 'node:os'; +import type { JoomlaConfig, JoomlaConnection } from './types.js'; + +const CONFIG_FILENAME = '.joomla-api-mcp.json'; + +export async function loadConfig(): Promise { + const config_path = resolve(homedir(), CONFIG_FILENAME); + + try { + const raw = await readFile(config_path, 'utf-8'); + const parsed = JSON.parse(raw) as Partial; + + if (!parsed.connections || Object.keys(parsed.connections).length === 0) { + throw new Error('No connections defined in config'); + } + + return { + connections: parsed.connections, + defaultConnection: parsed.defaultConnection ?? Object.keys(parsed.connections)[0], + }; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + throw new Error( + `Failed to load config from ${config_path}: ${message}\n` + + `Create ${config_path} โ€” see config.example.json for format.`, + ); + } +} + +export function getConnection(config: JoomlaConfig, name?: string): JoomlaConnection { + const key = name ?? config.defaultConnection; + const conn = config.connections[key]; + if (!conn) { + throw new Error( + `Connection "${key}" not found. Available: ${Object.keys(config.connections).join(', ')}`, + ); + } + return conn; +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..d0f456b --- /dev/null +++ b/src/index.ts @@ -0,0 +1,691 @@ +#!/usr/bin/env node +/* Copyright (C) 2026 Moko Consulting + * + * This file is part of a Moko Consulting project. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: joomla-api-mcp.Server + * INGROUP: joomla-api-mcp + * REPO: https://git.mokoconsulting.tech/MokoConsulting/joomla-api-mcp + * PATH: /src/index.ts + * VERSION: 00.00.01 + * BRIEF: MCP server entry point โ€” registers all Joomla API tools + */ + +import { randomBytes } from 'node:crypto'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { z } from 'zod'; +import { loadConfig, getConnection } from './config.js'; +import { JoomlaClient } from './client.js'; +import type { JoomlaConfig, ApiResponse } from './types.js'; + +let config: JoomlaConfig; + +function generatePassword(length = 20): string { + const charset = 'abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#$%&*'; + const bytes = randomBytes(length); + return Array.from(bytes, (b) => charset[b % charset.length]).join(''); +} + +function clientFor(connection?: string): JoomlaClient { + return new JoomlaClient(getConnection(config, connection)); +} + +function formatResponse(res: ApiResponse): { content: Array<{ type: 'text'; text: string }> } { + if (res.status >= 400) { + const errors = (res.data as { errors?: Array<{ title: string; detail?: string }> })?.errors; + const msg = errors + ? errors.map((e) => `${e.title}${e.detail ? `: ${e.detail}` : ''}`).join('\n') + : `HTTP ${res.status}: ${JSON.stringify(res.data, null, 2)}`; + return { content: [{ type: 'text' as const, text: `Error: ${msg}` }] }; + } + return { + content: [{ type: 'text' as const, text: JSON.stringify(res.data, null, 2) }], + }; +} + +const ConnectionParam = { + connection: z.string().optional().describe('Named connection from config (uses default if omitted)'), +}; + +const server = new McpServer({ + name: 'joomla-api-mcp', + version: '0.0.1', +}); + +// โ”€โ”€ Articles โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +server.tool( + 'joomla_articles_list', + 'List Joomla articles with optional filtering', + { + category_id: z.number().optional().describe('Filter by category ID'), + state: z.enum(['0', '1', '-2']).optional().describe('Filter by state: 1=published, 0=unpublished, -2=trashed'), + search: z.string().optional().describe('Search in title'), + limit: z.number().optional().describe('Max results (default 20)'), + offset: z.number().optional().describe('Pagination offset'), + ...ConnectionParam, + }, + async ({ category_id, state, search, limit, offset, connection }) => { + const client = clientFor(connection); + const params: Record = {}; + if (category_id !== undefined) params['filter[category]'] = String(category_id); + if (state !== undefined) params['filter[state]'] = state; + if (search) params['filter[search]'] = search; + if (limit !== undefined) params['list[limit]'] = String(limit); + if (offset !== undefined) params['list[offset]'] = String(offset); + return formatResponse(await client.get('/content/articles', params)); + }, +); + +server.tool( + 'joomla_article_get', + 'Get a single article by ID', + { + id: z.number().describe('Article ID'), + ...ConnectionParam, + }, + async ({ id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.get(`/content/articles/${id}`)); + }, +); + +server.tool( + 'joomla_article_create', + 'Create a new article', + { + title: z.string().describe('Article title'), + articletext: z.string().describe('Article body (HTML)'), + catid: z.number().describe('Category ID'), + state: z.number().optional().describe('State: 1=published, 0=unpublished (default 0)'), + language: z.string().optional().describe('Language code (default "*" for all)'), + featured: z.number().optional().describe('1=featured, 0=not featured'), + metadesc: z.string().optional().describe('Meta description'), + metakey: z.string().optional().describe('Meta keywords'), + ...ConnectionParam, + }, + async ({ title, articletext, catid, state, language, featured, metadesc, metakey, connection }) => { + const client = clientFor(connection); + const body: Record = { + title, + articletext, + catid, + state: state ?? 0, + language: language ?? '*', + }; + if (featured !== undefined) body.featured = featured; + if (metadesc) body.metadesc = metadesc; + if (metakey) body.metakey = metakey; + return formatResponse(await client.post('/content/articles', body)); + }, +); + +server.tool( + 'joomla_article_update', + 'Update an existing article', + { + id: z.number().describe('Article ID'), + title: z.string().optional().describe('New title'), + articletext: z.string().optional().describe('New body (HTML)'), + catid: z.number().optional().describe('New category ID'), + state: z.number().optional().describe('State: 1=published, 0=unpublished, -2=trashed'), + featured: z.number().optional().describe('1=featured, 0=not featured'), + metadesc: z.string().optional().describe('Meta description'), + ...ConnectionParam, + }, + async ({ id, title, articletext, catid, state, featured, metadesc, connection }) => { + const client = clientFor(connection); + const body: Record = {}; + if (title !== undefined) body.title = title; + if (articletext !== undefined) body.articletext = articletext; + if (catid !== undefined) body.catid = catid; + if (state !== undefined) body.state = state; + if (featured !== undefined) body.featured = featured; + if (metadesc !== undefined) body.metadesc = metadesc; + return formatResponse(await client.patch(`/content/articles/${id}`, body)); + }, +); + +server.tool( + 'joomla_article_delete', + 'Delete an article', + { + id: z.number().describe('Article ID'), + ...ConnectionParam, + }, + async ({ id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.delete(`/content/articles/${id}`)); + }, +); + +// โ”€โ”€ Categories โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +server.tool( + 'joomla_categories_list', + 'List content categories', + { + extension: z.string().optional().describe('Extension name (default "com_content")'), + search: z.string().optional().describe('Search in title'), + ...ConnectionParam, + }, + async ({ extension, search, connection }) => { + const client = clientFor(connection); + const params: Record = {}; + if (extension) params['filter[extension]'] = extension; + if (search) params['filter[search]'] = search; + return formatResponse(await client.get('/content/categories', params)); + }, +); + +server.tool( + 'joomla_category_create', + 'Create a new category', + { + title: z.string().describe('Category title'), + parent_id: z.number().optional().describe('Parent category ID (default 1 = root)'), + extension: z.string().optional().describe('Extension (default "com_content")'), + description: z.string().optional().describe('Category description'), + state: z.number().optional().describe('State: 1=published, 0=unpublished'), + language: z.string().optional().describe('Language code (default "*")'), + ...ConnectionParam, + }, + async ({ title, parent_id, extension, description, state, language, connection }) => { + const client = clientFor(connection); + const body: Record = { + title, + parent_id: parent_id ?? 1, + extension: extension ?? 'com_content', + language: language ?? '*', + state: state ?? 1, + }; + if (description) body.description = description; + return formatResponse(await client.post('/content/categories', body)); + }, +); + +server.tool( + 'joomla_category_update', + 'Update a category', + { + id: z.number().describe('Category ID'), + title: z.string().optional().describe('New title'), + description: z.string().optional().describe('New description'), + state: z.number().optional().describe('State: 1=published, 0=unpublished'), + ...ConnectionParam, + }, + async ({ id, title, description, state, connection }) => { + const client = clientFor(connection); + const body: Record = {}; + if (title !== undefined) body.title = title; + if (description !== undefined) body.description = description; + if (state !== undefined) body.state = state; + return formatResponse(await client.patch(`/content/categories/${id}`, body)); + }, +); + +server.tool( + 'joomla_category_delete', + 'Delete a category', + { + id: z.number().describe('Category ID'), + ...ConnectionParam, + }, + async ({ id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.delete(`/content/categories/${id}`)); + }, +); + +// โ”€โ”€ Users โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +server.tool( + 'joomla_users_list', + 'List Joomla users', + { + search: z.string().optional().describe('Search in name/username/email'), + group_id: z.number().optional().describe('Filter by user group ID'), + state: z.enum(['0', '1']).optional().describe('0=blocked, 1=active'), + limit: z.number().optional().describe('Max results'), + ...ConnectionParam, + }, + async ({ search, group_id, state, limit, connection }) => { + const client = clientFor(connection); + const params: Record = {}; + if (search) params['filter[search]'] = search; + if (group_id !== undefined) params['filter[group_id]'] = String(group_id); + if (state !== undefined) params['filter[state]'] = state; + if (limit !== undefined) params['list[limit]'] = String(limit); + return formatResponse(await client.get('/users', params)); + }, +); + +server.tool( + 'joomla_user_get', + 'Get a single user by ID', + { + id: z.number().describe('User ID'), + ...ConnectionParam, + }, + async ({ id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.get(`/users/${id}`)); + }, +); + +server.tool( + 'joomla_user_create', + 'Create a new Joomla user with an auto-generated secure password', + { + name: z.string().describe('Full name'), + username: z.string().describe('Username'), + email: z.string().describe('Email address'), + groups: z.array(z.number()).optional().describe('Array of group IDs (default [2] = Registered)'), + block: z.number().optional().describe('0=active, 1=blocked (default 0)'), + ...ConnectionParam, + }, + async ({ name, username, email, groups, block, connection }) => { + const client = clientFor(connection); + const generated_password = generatePassword(); + const body: Record = { + name, + username, + email, + password: generated_password, + groups: groups ?? [2], + block: block ?? 0, + }; + const res = await client.post('/users', body); + if (res.status < 400) { + const result = formatResponse(res); + result.content.push({ + type: 'text' as const, + text: `\nGenerated password: ${generated_password}\nPlease share this securely with the user and have them change it on first login.`, + }); + return result; + } + return formatResponse(res); + }, +); + +server.tool( + 'joomla_user_update', + 'Update a user', + { + id: z.number().describe('User ID'), + name: z.string().optional().describe('Full name'), + email: z.string().optional().describe('Email address'), + groups: z.array(z.number()).optional().describe('Group IDs'), + block: z.number().optional().describe('0=active, 1=blocked'), + ...ConnectionParam, + }, + async ({ id, name, email, groups, block, connection }) => { + const client = clientFor(connection); + const body: Record = {}; + if (name !== undefined) body.name = name; + if (email !== undefined) body.email = email; + if (groups !== undefined) body.groups = groups; + if (block !== undefined) body.block = block; + return formatResponse(await client.patch(`/users/${id}`, body)); + }, +); + +server.tool( + 'joomla_user_delete', + 'Delete a user', + { + id: z.number().describe('User ID'), + ...ConnectionParam, + }, + async ({ id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.delete(`/users/${id}`)); + }, +); + +// โ”€โ”€ User Groups โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +server.tool( + 'joomla_user_groups_list', + 'List user groups', + { ...ConnectionParam }, + async ({ connection }) => { + const client = clientFor(connection); + return formatResponse(await client.get('/users/groups')); + }, +); + +// โ”€โ”€ Menus โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +server.tool( + 'joomla_menus_list', + 'List menu types', + { ...ConnectionParam }, + async ({ connection }) => { + const client = clientFor(connection); + return formatResponse(await client.get('/menus')); + }, +); + +server.tool( + 'joomla_menu_items_list', + 'List menu items for a menu type', + { + menutype: z.string().describe('Menu type alias (e.g. "mainmenu")'), + ...ConnectionParam, + }, + async ({ menutype, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.get('/menus/items', { 'filter[menutype]': menutype })); + }, +); + +server.tool( + 'joomla_menu_item_get', + 'Get a single menu item by ID', + { + id: z.number().describe('Menu item ID'), + ...ConnectionParam, + }, + async ({ id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.get(`/menus/items/${id}`)); + }, +); + +// โ”€โ”€ Modules โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +server.tool( + 'joomla_modules_list', + 'List modules', + { + client_id: z.enum(['0', '1']).optional().describe('0=site, 1=admin'), + ...ConnectionParam, + }, + async ({ client_id, connection }) => { + const client = clientFor(connection); + const endpoint = client_id === '1' ? '/modules/administrator' : '/modules/site'; + return formatResponse(await client.get(endpoint)); + }, +); + +// โ”€โ”€ Plugins โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +server.tool( + 'joomla_plugins_list', + 'List plugins', + { + type: z.string().optional().describe('Filter by plugin type/folder (e.g. "system", "content")'), + state: z.enum(['0', '1']).optional().describe('0=disabled, 1=enabled'), + search: z.string().optional().describe('Search in name'), + ...ConnectionParam, + }, + async ({ type, state, search, connection }) => { + const client = clientFor(connection); + const params: Record = {}; + if (type) params['filter[folder]'] = type; + if (state !== undefined) params['filter[enabled]'] = state; + if (search) params['filter[search]'] = search; + return formatResponse(await client.get('/plugins', params)); + }, +); + +server.tool( + 'joomla_plugin_update', + 'Enable or disable a plugin', + { + id: z.number().describe('Plugin ID'), + enabled: z.number().describe('1=enable, 0=disable'), + ...ConnectionParam, + }, + async ({ id, enabled, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.patch(`/plugins/${id}`, { enabled })); + }, +); + +// โ”€โ”€ Languages โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +server.tool( + 'joomla_languages_list', + 'List installed content languages', + { ...ConnectionParam }, + async ({ connection }) => { + const client = clientFor(connection); + return formatResponse(await client.get('/languages/content')); + }, +); + +// โ”€โ”€ Tags โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +server.tool( + 'joomla_tags_list', + 'List tags', + { + search: z.string().optional().describe('Search in title'), + ...ConnectionParam, + }, + async ({ search, connection }) => { + const client = clientFor(connection); + const params: Record = {}; + if (search) params['filter[search]'] = search; + return formatResponse(await client.get('/tags', params)); + }, +); + +server.tool( + 'joomla_tag_create', + 'Create a tag', + { + title: z.string().describe('Tag title'), + parent_id: z.number().optional().describe('Parent tag ID'), + ...ConnectionParam, + }, + async ({ title, parent_id, connection }) => { + const client = clientFor(connection); + const body: Record = { title }; + if (parent_id !== undefined) body.parent_id = parent_id; + return formatResponse(await client.post('/tags', body)); + }, +); + +// โ”€โ”€ Custom Fields โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +server.tool( + 'joomla_fields_list', + 'List custom fields for a context', + { + context: z.string().optional().describe('Context (default "com_content.article")'), + ...ConnectionParam, + }, + async ({ context, connection }) => { + const client = clientFor(connection); + const ctx = context ?? 'com_content.article'; + return formatResponse(await client.get(`/fields/${ctx}`)); + }, +); + +// โ”€โ”€ Contacts โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +server.tool( + 'joomla_contacts_list', + 'List contacts', + { + search: z.string().optional().describe('Search in name'), + ...ConnectionParam, + }, + async ({ search, connection }) => { + const client = clientFor(connection); + const params: Record = {}; + if (search) params['filter[search]'] = search; + return formatResponse(await client.get('/contact', params)); + }, +); + +// โ”€โ”€ Banners โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +server.tool( + 'joomla_banners_list', + 'List banners', + { ...ConnectionParam }, + async ({ connection }) => { + const client = clientFor(connection); + return formatResponse(await client.get('/banners')); + }, +); + +// โ”€โ”€ Newsfeeds โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +server.tool( + 'joomla_newsfeeds_list', + 'List newsfeeds', + { ...ConnectionParam }, + async ({ connection }) => { + const client = clientFor(connection); + return formatResponse(await client.get('/newsfeeds')); + }, +); + +// โ”€โ”€ Messages (Private Messaging) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +server.tool( + 'joomla_messages_list', + 'List private messages', + { ...ConnectionParam }, + async ({ connection }) => { + const client = clientFor(connection); + return formatResponse(await client.get('/messages')); + }, +); + +server.tool( + 'joomla_message_get', + 'Get a single private message', + { + id: z.number().describe('Message ID'), + ...ConnectionParam, + }, + async ({ id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.get(`/messages/${id}`)); + }, +); + +// โ”€โ”€ Configuration โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +server.tool( + 'joomla_config_get', + 'Get application configuration', + { ...ConnectionParam }, + async ({ connection }) => { + const client = clientFor(connection); + return formatResponse(await client.get('/config/application')); + }, +); + +server.tool( + 'joomla_config_update', + 'Update application configuration values', + { + settings: z.record(z.string(), z.unknown()).describe('Key-value pairs of settings to update'), + ...ConnectionParam, + }, + async ({ settings, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.patch('/config/application', settings)); + }, +); + +// โ”€โ”€ Templates โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +server.tool( + 'joomla_templates_list', + 'List site or admin templates', + { + client_id: z.enum(['0', '1']).optional().describe('0=site templates, 1=admin templates'), + ...ConnectionParam, + }, + async ({ client_id, connection }) => { + const client = clientFor(connection); + const endpoint = client_id === '1' ? '/templates/administrator' : '/templates/site'; + return formatResponse(await client.get(endpoint)); + }, +); + +// โ”€โ”€ Media โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +server.tool( + 'joomla_media_list', + 'List media files in a folder', + { + path: z.string().optional().describe('Folder path relative to media root (default "")'), + ...ConnectionParam, + }, + async ({ path, connection }) => { + const client = clientFor(connection); + const params: Record = {}; + if (path) params['path'] = path; + return formatResponse(await client.get('/media/files', params)); + }, +); + +// โ”€โ”€ Generic API Call โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +server.tool( + 'joomla_api_request', + 'Make a raw API request to any Joomla Web Services endpoint', + { + method: z.enum(['GET', 'POST', 'PATCH', 'DELETE']).describe('HTTP method'), + endpoint: z.string().describe('API endpoint path (e.g. "/content/articles")'), + body: z.record(z.string(), z.unknown()).optional().describe('Request body for POST/PATCH'), + params: z.record(z.string(), z.string()).optional().describe('Query parameters'), + ...ConnectionParam, + }, + async ({ method, endpoint, body, params, connection }) => { + const client = clientFor(connection); + switch (method) { + case 'GET': + return formatResponse(await client.get(endpoint, params)); + case 'POST': + return formatResponse(await client.post(endpoint, body)); + case 'PATCH': + return formatResponse(await client.patch(endpoint, body)); + case 'DELETE': + return formatResponse(await client.delete(endpoint)); + } + }, +); + +// โ”€โ”€ Connections Management โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +server.tool( + 'joomla_list_connections', + 'List configured Joomla API connections', + {}, + async () => { + const lines = Object.entries(config.connections).map(([name, conn]) => { + const is_default = name === config.defaultConnection ? ' (default)' : ''; + return ` ${name}${is_default}: ${conn.baseUrl}`; + }); + return { + content: [{ type: 'text' as const, text: `Configured connections:\n${lines.join('\n')}` }], + }; + }, +); + +// โ”€โ”€ Start Server โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +async function main(): Promise { + config = await loadConfig(); + const transport = new StdioServerTransport(); + await server.connect(transport); +} + +main().catch((err) => { + process.stderr.write(`Fatal: ${err}\n`); + process.exit(1); +}); diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..08e5e55 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,32 @@ +/* Copyright (C) 2026 Moko Consulting + * + * This file is part of a Moko Consulting project. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: joomla-api-mcp.Types + * INGROUP: joomla-api-mcp + * REPO: https://git.mokoconsulting.tech/MokoConsulting/joomla-api-mcp + * PATH: /src/types.ts + * VERSION: 00.00.01 + * BRIEF: TypeScript type definitions for Joomla API MCP server + */ + +export interface JoomlaConnection { + baseUrl: string; + apiToken: string; + /** Skip TLS certificate verification (self-signed certs) */ + insecure?: boolean; +} + +export interface JoomlaConfig { + connections: Record; + defaultConnection: string; +} + +export interface ApiResponse { + status: number; + data: unknown; + errors?: Array<{ title: string; detail?: string }>; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0dd168c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} -- 2.52.0 From ea0a028128ecf8c3f9e8938ff58002b4f75527ae Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 23 Apr 2026 12:36:31 -0500 Subject: [PATCH 04/16] docs: add installation guide, architecture overview, and API reference - docs/INSTALLATION.md: setup instructions, config format, troubleshooting - docs/ARCHITECTURE.md: component overview and design decisions - docs/API.md: complete MCP tool reference with all parameters Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/API.md | 279 +++++++++++++++++++++++++ docs/ARCHITECTURE.md | 80 +++++++ docs/INSTALLATION.md | 486 +++++++++---------------------------------- 3 files changed, 454 insertions(+), 391 deletions(-) create mode 100644 docs/API.md create mode 100644 docs/ARCHITECTURE.md diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..7dae8a5 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,279 @@ + + +# API Reference + +All tools accept an optional `connection` parameter to target a specific named connection. If omitted, the default connection is used. + +## Articles + +### `joomla_articles_list` +List articles with optional filtering. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `category_id` | number | No | Filter by category ID | +| `state` | `"0"` / `"1"` / `"-2"` | No | 1=published, 0=unpublished, -2=trashed | +| `search` | string | No | Search in title | +| `limit` | number | No | Max results (default 20) | +| `offset` | number | No | Pagination offset | + +### `joomla_article_get` +Get a single article by ID. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `id` | number | Yes | Article ID | + +### `joomla_article_create` +Create a new article. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `title` | string | Yes | Article title | +| `articletext` | string | Yes | Article body (HTML) | +| `catid` | number | Yes | Category ID | +| `state` | number | No | 1=published, 0=unpublished (default 0) | +| `language` | string | No | Language code (default `"*"`) | +| `featured` | number | No | 1=featured, 0=not | +| `metadesc` | string | No | Meta description | +| `metakey` | string | No | Meta keywords | + +### `joomla_article_update` +Update an existing article. Only provided fields are changed. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `id` | number | Yes | Article ID | +| `title` | string | No | New title | +| `articletext` | string | No | New body (HTML) | +| `catid` | number | No | New category ID | +| `state` | number | No | State value | +| `featured` | number | No | Featured flag | +| `metadesc` | string | No | Meta description | + +### `joomla_article_delete` +Delete an article. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `id` | number | Yes | Article ID | + +## Categories + +### `joomla_categories_list` +List content categories. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `extension` | string | No | Extension name (default `"com_content"`) | +| `search` | string | No | Search in title | + +### `joomla_category_create` +Create a new category. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `title` | string | Yes | Category title | +| `parent_id` | number | No | Parent category ID (default 1 = root) | +| `extension` | string | No | Extension (default `"com_content"`) | +| `description` | string | No | Category description | +| `state` | number | No | 1=published, 0=unpublished | +| `language` | string | No | Language code (default `"*"`) | + +### `joomla_category_update` +Update a category. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `id` | number | Yes | Category ID | +| `title` | string | No | New title | +| `description` | string | No | New description | +| `state` | number | No | State value | + +### `joomla_category_delete` +Delete a category. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `id` | number | Yes | Category ID | + +## Users + +### `joomla_users_list` +List Joomla users. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `search` | string | No | Search in name/username/email | +| `group_id` | number | No | Filter by user group ID | +| `state` | `"0"` / `"1"` | No | 0=blocked, 1=active | +| `limit` | number | No | Max results | + +### `joomla_user_get` +Get a single user by ID. + +### `joomla_user_create` +Create a user with auto-generated secure password. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `name` | string | Yes | Full name | +| `username` | string | Yes | Username | +| `email` | string | Yes | Email address | +| `groups` | number[] | No | Group IDs (default `[2]` = Registered) | +| `block` | number | No | 0=active, 1=blocked (default 0) | + +Returns the generated password in the response. Share securely with the user. + +### `joomla_user_update` +Update a user. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `id` | number | Yes | User ID | +| `name` | string | No | Full name | +| `email` | string | No | Email | +| `groups` | number[] | No | Group IDs | +| `block` | number | No | 0=active, 1=blocked | + +### `joomla_user_delete` +Delete a user. + +### `joomla_user_groups_list` +List all user groups. No parameters. + +## Menus + +### `joomla_menus_list` +List menu types. No parameters. + +### `joomla_menu_items_list` +List menu items for a menu type. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `menutype` | string | Yes | Menu type alias (e.g. `"mainmenu"`) | + +### `joomla_menu_item_get` +Get a single menu item by ID. + +## Plugins + +### `joomla_plugins_list` +List plugins. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `type` | string | No | Filter by plugin folder (e.g. `"system"`, `"content"`) | +| `state` | `"0"` / `"1"` | No | 0=disabled, 1=enabled | +| `search` | string | No | Search in name | + +### `joomla_plugin_update` +Enable or disable a plugin. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `id` | number | Yes | Plugin ID | +| `enabled` | number | Yes | 1=enable, 0=disable | + +## Modules + +### `joomla_modules_list` +List site or admin modules. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `client_id` | `"0"` / `"1"` | No | 0=site, 1=admin | + +## Templates + +### `joomla_templates_list` +List site or admin templates. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `client_id` | `"0"` / `"1"` | No | 0=site, 1=admin | + +## Other Tools + +### `joomla_languages_list` +List installed content languages. + +### `joomla_tags_list` +List tags with optional search. + +### `joomla_tag_create` +Create a tag. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `title` | string | Yes | Tag title | +| `parent_id` | number | No | Parent tag ID | + +### `joomla_fields_list` +List custom fields for a context (default `"com_content.article"`). + +### `joomla_contacts_list` +List contacts with optional search. + +### `joomla_banners_list` +List banners. + +### `joomla_newsfeeds_list` +List newsfeeds. + +### `joomla_messages_list` +List private messages. + +### `joomla_message_get` +Get a single private message by ID. + +### `joomla_media_list` +List media files in a folder. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `path` | string | No | Folder path relative to media root | + +### `joomla_config_get` +Get application configuration. + +### `joomla_config_update` +Update application configuration values. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `settings` | object | Yes | Key-value pairs of settings to update | + +### `joomla_api_request` +Make a raw API request to any Joomla Web Services endpoint. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `method` | `"GET"` / `"POST"` / `"PATCH"` / `"DELETE"` | Yes | HTTP method | +| `endpoint` | string | Yes | API path (e.g. `"/content/articles"`) | +| `body` | object | No | Request body for POST/PATCH | +| `params` | object | No | Query parameters | + +### `joomla_list_connections` +List all configured connections. No parameters. + +## Revision History + +| Date | Version | Author | Notes | +| --- | --- | --- | --- | +| 2026-04-23 | 0.0.1 | jmiller | Initial API reference | diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..c0ba0fb --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,80 @@ + + +# Architecture + +## Overview + +joomla-api-mcp is a Model Context Protocol (MCP) server that bridges AI assistants (Claude Code, Cursor, etc.) with Joomla's built-in Web Services REST API. + +``` +AI Assistant <--> MCP (stdio) <--> JoomlaClient <--> Joomla REST API + /api/index.php/v1 +``` + +## Components + +### `src/index.ts` โ€” Server Entry Point + +Registers all MCP tools with the `McpServer` from `@modelcontextprotocol/sdk`. Each tool maps to one or more Joomla API endpoints. Uses Zod schemas for input validation. + +### `src/client.ts` โ€” HTTP Client + +The `JoomlaClient` class handles all HTTP communication with Joomla instances: + +- Uses `node:https` / `node:http` for requests (not `fetch`) to support self-signed TLS certificates on Node.js 24+ +- Authenticates via Bearer token in the `Authorization` header +- Sends `Accept: application/vnd.api+json` per Joomla's JSON:API spec +- Includes a JSON parser that handles Joomla responses that may append HTML error fragments after valid JSON + +### `src/config.ts` โ€” Configuration Loader + +Loads connection details from `~/.joomla-api-mcp.json`. Supports multiple named connections with a configurable default. + +### `src/types.ts` โ€” Type Definitions + +TypeScript interfaces for `JoomlaConnection`, `JoomlaConfig`, and `ApiResponse`. + +### `scripts/setup.mjs` โ€” Interactive Setup + +Node.js script using `readline/promises` that walks users through creating the config file. Supports adding multiple connections incrementally. + +## Design Decisions + +### Why `node:https` instead of `fetch`? + +Node.js 24's built-in `fetch` (undici-based) does not honor `NODE_TLS_REJECT_UNAUTHORIZED=0` for self-signed certificate bypass. The classic `node:https` module with `rejectUnauthorized: false` works reliably across all Node.js versions. + +### Why JSON recovery parsing? + +Joomla's API sometimes returns valid JSON with `text/html` content-type, and may append HTML error fragments (e.g. template errors) after the JSON body. The `tryParseJson` method handles this by finding the last valid JSON boundary when standard parsing fails. + +### Why per-connection API tokens? + +Each Joomla site requires its own API token scoped to a specific user. Multi-site support is a core use case โ€” managing staging, production, and dev environments from a single MCP server. + +## Data Flow + +1. AI assistant sends a tool call via MCP stdio transport +2. `index.ts` validates parameters with Zod and resolves the connection +3. `JoomlaClient` constructs the API URL, attaches auth headers, and makes the HTTP request +4. Response is parsed (with JSON recovery if needed) and returned as MCP tool output + +## Revision History + +| Date | Version | Author | Notes | +| --- | --- | --- | --- | +| 2026-04-23 | 0.0.1 | jmiller | Initial architecture document | diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index e32319d..9372274 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -6,436 +6,140 @@ This file is part of a Moko Consulting project. SPDX-License-Identifier: GPL-3.0-or-later # FILE INFORMATION +DEFGROUP: joomla-api-mcp.Documentation +INGROUP: joomla-api-mcp +REPO: https://git.mokoconsulting.tech/MokoConsulting/joomla-api-mcp PATH: /docs/INSTALLATION.md -VERSION: 04.00.15 -BRIEF: Installation and setup instructions for [PROJECT_NAME] +VERSION: 00.00.01 +BRIEF: Installation and setup instructions --> # Installation -## Overview - -This document provides comprehensive installation and setup instructions for **[PROJECT_NAME]**. - -## Table of Contents - -- [Prerequisites](#prerequisites) -- [Installation Methods](#installation-methods) -- [Quick Start](#quick-start) -- [Detailed Installation](#detailed-installation) -- [Configuration](#configuration) -- [Verification](#verification) -- [Troubleshooting](#troubleshooting) -- [Next Steps](#next-steps) - ## Prerequisites -### System Requirements +- **Node.js** 20.0.0 or later +- **npm** (included with Node.js) +- A Joomla 4/5/6 site with the Web Services API enabled +- A Joomla API token (generated in Users > Edit User > API Token tab) -- **Operating System**: [Specify supported OS versions] -- **Runtime**: [e.g., PHP 8.1+, Node.js 20+, Python 3.9+] -- **Memory**: [Minimum RAM required] -- **Disk Space**: [Minimum disk space required] +## Install -### Software Dependencies - -**Required:** -- [List required dependencies with versions] -- Example: Git 2.30+ -- Example: Composer 2.0+ - -**Optional:** -- [List optional dependencies] - -### Access Requirements - -- [Any required access permissions, credentials, or accounts] -- Example: GitHub account for cloning private repositories -- Example: Database access credentials - -## Installation Methods - -### Method 1: Using Package Manager (Recommended) - -**For [Platform/Package Manager]:** - -```bash -# Installation command -[package-manager] install [package-name] - -# Verify installation -[package-name] --version +```sh +git clone https://git.mokoconsulting.tech/MokoConsulting/joomla-api-mcp.git +cd joomla-api-mcp +npm install +npm run build +npm run setup ``` -### Method 2: From Source +The setup wizard will prompt for: -**Clone the repository:** +1. **Connection name** โ€” a label for this site (e.g. `production`, `staging`) +2. **Joomla site URL** โ€” the base URL of the site (e.g. `https://www.example.com`) +3. **API token** โ€” the Joomla API token for authentication +4. **TLS verification** โ€” whether to skip certificate verification (for self-signed certs) -```bash -# Clone from GitHub -git clone https://github.com/[organization]/[repository].git -cd [repository] +Run `npm run setup` again to add more connections. -# Checkout stable version (recommended) -git checkout tags/v[VERSION] +## Register with Claude Code + +Add to your global Claude Code config (`~/.claude.json`): + +```json +{ + "mcpServers": { + "joomla-api": { + "type": "stdio", + "command": "node", + "args": ["/path/to/joomla-api-mcp/dist/index.js"] + } + } +} ``` -### Method 3: Using Pre-built Binary/Package +Or add to a project-level `.mcp.json`: -**Download and install:** - -```bash -# Download release -wget https://github.com/[organization]/[repository]/releases/download/v[VERSION]/[package-name] - -# Make executable (if applicable) -chmod +x [package-name] - -# Move to system path (optional) -sudo mv [package-name] /usr/local/bin/ +```json +{ + "mcpServers": { + "joomla-api": { + "command": "node", + "args": ["/path/to/joomla-api-mcp/dist/index.js"] + } + } +} ``` -## Quick Start +Restart Claude Code after adding the server. -For users who want to get started quickly: +## Generating a Joomla API Token -```bash -# 1. Install -[installation-command] +1. Log in to the Joomla admin panel +2. Go to **Users** > **Manage** > select your user +3. Click the **API Token** tab +4. Click **Save** to generate a token +5. Copy the token value โ€” it will not be shown again -# 2. Configure -[configuration-command] +The token must belong to a user with sufficient permissions for the operations you want to perform (e.g. Super Users for full access). -# 3. Run -[run-command] +## Configuration File -# 4. Verify -[verification-command] +The config is stored at `~/.joomla-api-mcp.json`: + +```json +{ + "defaultConnection": "production", + "connections": { + "production": { + "baseUrl": "https://www.example.com", + "apiToken": "your-api-token" + }, + "staging": { + "baseUrl": "https://staging.example.com", + "apiToken": "your-staging-token", + "insecure": true + } + } +} ``` -## Detailed Installation - -### Step 1: Prepare Environment - -**1.1 Install System Dependencies** - -For Ubuntu/Debian: -```bash -sudo apt update -sudo apt install [dependencies] -``` - -For macOS: -```bash -brew install [dependencies] -``` - -For Windows: -```powershell -# PowerShell commands or link to Windows-specific guide -``` - -**1.2 Set Up Environment Variables** - -```bash -# Add to ~/.bashrc or ~/.zshrc -export [VAR_NAME]=[value] - -# Reload shell configuration -source ~/.bashrc -``` - -### Step 2: Install Application - -**2.1 Install via [Method]** - -```bash -[Detailed installation commands with explanations] -``` - -**2.2 Install Dependencies** - -```bash -# For PHP projects -composer install --no-dev - -# For Node.js projects -npm install --production - -# For Python projects -pip install -r requirements.txt -``` - -### Step 3: Initial Configuration - -**3.1 Create Configuration File** - -```bash -# Copy example configuration -cp config/config.example.php config/config.php - -# Or use configuration wizard -php bin/configure.php -``` - -**3.2 Configure Database (if applicable)** - -```bash -# Create database -mysql -u root -p -e "CREATE DATABASE [db_name];" - -# Import schema -mysql -u root -p [db_name] < database/schema.sql - -# Update configuration -nano config/database.php -``` - -**3.3 Set Permissions** - -```bash -# Set appropriate ownership -sudo chown -R www-data:www-data /var/www/[project] - -# Set directory permissions (755) -find /var/www/[project] -type d -exec chmod 755 {} \; - -# Set file permissions (644 for most files) -find /var/www/[project] -type f -exec chmod 644 {} \; - -# Make executable files executable (if needed) -chmod +x /var/www/[project]/bin/* - -# Restrict sensitive directories (storage, cache, logs) -chmod 750 /var/www/[project]/storage -chmod 750 /var/www/[project]/cache -``` - -### Step 4: Initialize Application - -**4.1 Run Setup Script** - -```bash -# Run initialization -php bin/setup.php - -# Or for other platforms -./scripts/setup.sh -``` - -**4.2 Create Admin User (if applicable)** - -```bash -# Create first admin user -php bin/create-admin.php --email=admin@example.com --name="Admin User" -``` - -## Configuration - -### Configuration Files - -| File | Purpose | Required | -|------|---------|----------| -| `config/config.php` | Main configuration | Yes | -| `config/database.php` | Database settings | Yes | -| `config/cache.php` | Cache configuration | No | -| `.env` | Environment variables | Yes | - -### Essential Configuration Options - -**config/config.php:** - -```php -return [ - 'app_name' => '[APPLICATION_NAME]', - 'app_url' => 'https://example.com', - 'debug' => false, // Set to true for development - 'timezone' => 'UTC', -]; -``` - -**Database Configuration:** - -```php -return [ - 'host' => 'localhost', - 'port' => 3306, - 'database' => '[db_name]', - 'username' => '[db_user]', - 'password' => '[db_password]', -]; -``` - -### Environment Variables - -Create `.env` file: - -```bash -APP_ENV=production -APP_DEBUG=false -APP_URL=https://example.com - -DB_HOST=localhost -DB_PORT=3306 -DB_DATABASE=[db_name] -DB_USERNAME=[db_user] -DB_PASSWORD=[db_password] -``` +| Field | Required | Description | +|-------|----------|-------------| +| `defaultConnection` | Yes | Name of the default connection | +| `connections` | Yes | Map of named connections | +| `baseUrl` | Yes | Joomla site URL (no trailing slash) | +| `apiToken` | Yes | Joomla API token (Bearer auth) | +| `insecure` | No | Set `true` to skip TLS verification | ## Verification -### Verify Installation +After setup, verify the server starts correctly: -**Check version:** - -```bash -[command] --version -# Expected output: v[VERSION] +```sh +npm start ``` -**Run health check:** - -```bash -[command] health-check -# or -php bin/health-check.php -``` - -**Test basic functionality:** - -```bash -# Run test command -[command] test - -# Access web interface -curl http://localhost:[port]/health -``` - -### Expected Output - -``` -โœ“ Application installed successfully -โœ“ Database connection established -โœ“ All dependencies available -โœ“ Configuration valid -โœ“ System ready for use -``` +If configured correctly, the server will start listening on stdio. If the config is missing or invalid, it will print an error with instructions. ## Troubleshooting -### Common Issues +### "Failed to load config" error -#### Issue: Installation fails with dependency error +The config file `~/.joomla-api-mcp.json` is missing or malformed. Run `npm run setup` to create it. -**Symptom:** -``` -Error: Package [package-name] not found -``` +### "terminated" or connection errors -**Solution:** -```bash -# Update package manager -[package-manager] update +- Verify the Joomla site is reachable from your machine +- For self-signed certs, set `"insecure": true` in the connection config +- Ensure the API token is valid and not expired -# Retry installation -[package-manager] install [package-name] -``` +### "Resource not found" on certain endpoints -#### Issue: Database connection fails +Some Joomla API endpoints require specific components to be installed and enabled. For example, the menus API requires `com_menus` to expose its Web Services routes. -**Symptom:** -``` -Error: SQLSTATE[HY000] [2002] Connection refused -``` +## Revision History -**Solution:** -1. Verify database service is running: - ```bash - sudo systemctl status mysql - ``` - -2. Check database credentials in configuration - -3. Verify database host and port are correct - -#### Issue: Permission denied errors - -**Symptom:** -``` -Error: Permission denied: /var/www/[project]/storage -``` - -**Solution:** -```bash -# Fix ownership -sudo chown -R www-data:www-data /var/www/[project] - -# Fix permissions -sudo chmod -R 755 /var/www/[project]/storage -``` - -### Getting Help - -If you encounter issues not covered here: - -1. **Check Logs:** - ```bash - tail -f logs/application.log - tail -f /var/log/apache2/error.log - ``` - -2. **Enable Debug Mode:** - ```bash - # In config/config.php - 'debug' => true - ``` - -3. **Consult Documentation:** - - [Troubleshooting Guide](guide/troubleshooting.md) - - [FAQ](guide/faq.md) - -4. **Community Support:** - - GitHub Issues: [link] - - Discussion Forum: [link] - - Email: support@example.com - -## Next Steps - -After successful installation: - -1. **Review Configuration:** - - [Configuration Guide](guide/configuration.md) - - [Security Hardening](guide/security.md) - -2. **Read Getting Started:** - - [Quick Start Guide](guide/quickstart.md) - - [User Guide](guide/user-guide.md) - -3. **For Developers:** - - [Development Setup](development/setup.md) - - [Contributing Guidelines](../CONTRIBUTING.md) - -4. **For Operators:** - - [Deployment Guide](deployment/procedures.md) - - [Monitoring Setup](operations/monitoring.md) - -## Additional Resources - -- [Project Documentation](README.md) -- [API Reference](reference/api/) -- [Change Log](../CHANGELOG.md) -- [Security Policy](../SECURITY.md) - ---- - -## Support - -For installation support: -- **Documentation**: Review all guides in [docs/guide/](guide/) -- **Issues**: Report problems at [GitHub Issues](https://github.com/[organization]/[repository]/issues) -- **Email**: support@mokoconsulting.tech - ---- - -*Last Updated: [DATE]* -*Version: [VERSION]* +| Date | Version | Author | Notes | +| --- | --- | --- | --- | +| 2026-04-23 | 0.0.1 | jmiller | Initial installation guide | -- 2.52.0 From 17f5ce7342281d07b6a39cd3921612f6f9b201d1 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 23 Apr 2026 15:06:21 -0500 Subject: [PATCH 05/16] fix: deploy-dev passes silently without path, add env config override - deploy-dev.yml: skip silently when DEV_FTP_PATH is missing instead of failing - config.ts: support JOOMLA_API_MCP_CONFIG env var for custom config path - Clean up stale template files from src/ - Update sftp-config template with per-repo deploy key paths Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/deploy-dev.yml | 9 ++--- docs/templates/sftp-config.json.template | 43 ++++++------------------ src/!.gitkeep | 0 src/config.ts | 4 ++- src/index.md | 16 --------- 5 files changed, 18 insertions(+), 54 deletions(-) delete mode 100644 src/!.gitkeep delete mode 100644 src/index.md diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index 7781d00..a3f0d31 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -331,10 +331,11 @@ jobs: BASE="$DEV_FTP_PATH" if [ -z "$BASE" ]; then - echo "โŒ DEV_FTP_PATH is not set." - echo " Configure it as an org-level variable (Settings โ†’ Variables) and" - echo " ensure this repository has been granted access to it." - exit 1 + echo "โญ๏ธ DEV_FTP_PATH is not set โ€” skipping deployment." + echo " Configure it as a repo or org-level variable to enable deploy-dev." + echo "skip=true" >> "$GITHUB_OUTPUT" + echo "path=" >> "$GITHUB_OUTPUT" + exit 0 fi # DEV_FTP_SUFFIX is required โ€” it identifies the remote subdirectory for this repo. diff --git a/docs/templates/sftp-config.json.template b/docs/templates/sftp-config.json.template index b03cb01..b9171e9 100644 --- a/docs/templates/sftp-config.json.template +++ b/docs/templates/sftp-config.json.template @@ -1,8 +1,7 @@ { - // The tab key will cycle through the settings when first created + // Sublime Text SFTP Plugin // Visit https://codexns.io/products/sftp_for_subime/settings for help - // sftp, ftp or ftps "type": "sftp", "save_before_upload": true, @@ -14,43 +13,21 @@ "confirm_sync": true, "confirm_overwrite_newer": false, - "host": "example.com", - "user": "username", + "host": "dev.mokoconsulting.tech", + "user": "mokoconsulting_dev", + "port": "22", -//Default - //"port": "22", + "ssh_key_file": "C:/Users/jmill/OneDrive/Documents/Keys/repos/joomla-api-mcp", + //"password": "", -//Uncomment One: - //"ssh_key_file": "absolute/path/to/key/file", - //"password": "password", - - "remote_path": "/example/path/", + "remote_path": "/home/mokoconsulting_dev/", "ignore_regexes": [ "\\.sublime-(project|workspace|settings)", "\\.libsass.json/", - "sftp-config(-alt\\d?)?\\.json", + "sftp-config(-alt\\d?)?\\.json", "sftp-config.json.template", "sftp-settings\\.json", "/venv/", "\\.svn/", "\\.hg/", "\\.git*", "\\.bzr", "_darcs", "CVS", "\\.DS_Store", "Thumbs\\.db", "robots\\.txt", - "desktop\\.ini", "configuration\\.php", - "administrator/components/com_akeebabackup/backup", "\\.ffs*", "\\.md", - "\\.zip", "\\.editorconfig" + "desktop\\.ini", "\\.ffs*", "\\.editorconfig", "\\.md", "\\.zip", "docs/" ], - //"file_permissions": "664", - //"dir_permissions": "775", - //"extra_list_connections": 0, - - "connect_timeout": 30, - //"keepalive": 120, - //"ftp_passive_mode": true, - //"ftp_obey_passive_host": false, - //"ssh_key_file": "~/.ssh/id_rsa", - //"sftp_sudo": false, - //"sftp_debug": false, - //"sftp_flags": ["-F", "/path/to/ssh_config"], - - //"preserve_modification_times": false, - //"remote_time_offset_in_hours": 0, - //"remote_encoding": "utf-8", - //"remote_locale": "C", - //"allow_config_upload": false, + "connect_timeout": 30 } diff --git a/src/!.gitkeep b/src/!.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/config.ts b/src/config.ts index 0f785a5..1757484 100644 --- a/src/config.ts +++ b/src/config.ts @@ -21,7 +21,9 @@ import type { JoomlaConfig, JoomlaConnection } from './types.js'; const CONFIG_FILENAME = '.joomla-api-mcp.json'; export async function loadConfig(): Promise { - const config_path = resolve(homedir(), CONFIG_FILENAME); + const config_path = process.env.JOOMLA_API_MCP_CONFIG + ? resolve(process.env.JOOMLA_API_MCP_CONFIG) + : resolve(homedir(), CONFIG_FILENAME); try { const raw = await readFile(config_path, 'utf-8'); diff --git a/src/index.md b/src/index.md deleted file mode 100644 index 4c641af..0000000 --- a/src/index.md +++ /dev/null @@ -1,16 +0,0 @@ -# Docs Index: /templates/repos/generic/src - -## Purpose - -This index provides navigation to documentation within this folder. - -## Metadata - -- **Document Type:** index -- **Auto-generated:** This file is automatically generated by rebuild_indexes.py - -## Revision History - -| Change | Notes | Author | -| --- | --- | --- | -| Automated update | Generated by documentation index automation | rebuild_indexes.py | -- 2.52.0 From 07504255f65841813b5c1cb50a10b17a70a67bec Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 23 Apr 2026 15:09:32 -0500 Subject: [PATCH 06/16] =?UTF-8?q?release:=20v1.0.0=20=E2=80=94=20stable=20?= =?UTF-8?q?release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Joomla Web Services API MCP server with: - 30+ tools for articles, categories, users, menus, plugins, modules, templates, tags, contacts, banners, media, config, and raw API access - Multi-site support with per-connection API tokens - Node 24 TLS compatibility (node:https for self-signed certs) - Dirty JSON recovery for Joomla responses with HTML fragments - Interactive setup wizard (npm run setup) - JOOMLA_API_MCP_CONFIG env var for custom config path Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 4 ++-- package.json | 2 +- src/client.ts | 2 +- src/config.ts | 2 +- src/index.ts | 4 ++-- src/types.ts | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index f4782f9..c21a8ad 100644 --- a/README.md +++ b/README.md @@ -14,13 +14,13 @@ DEFGROUP: joomla-api-mcp.Documentation INGROUP: joomla-api-mcp REPO: https://git.mokoconsulting.tech/MokoConsulting/joomla-api-mcp - VERSION: 00.00.01 + VERSION: 01.00.00 PATH: ./README.md BRIEF: MCP server for Joomla Web Services API operations --> -[![Version](https://img.shields.io/badge/version-00.00.01-blue.svg?logo=v&logoColor=white)](https://git.mokoconsulting.tech/MokoConsulting/joomla-api-mcp/releases/tag/v00) +[![Version](https://img.shields.io/badge/version-01.00.00-blue.svg?logo=v&logoColor=white)](https://git.mokoconsulting.tech/MokoConsulting/joomla-api-mcp/releases/tag/v00) [![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green.svg?logo=gnu&logoColor=white)](LICENSE) [![Node](https://img.shields.io/badge/Node.js-20%2B-339933.svg?logo=node.js&logoColor=white)](https://nodejs.org) diff --git a/package.json b/package.json index 86868f9..cf00982 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mokoconsulting/joomla-api-mcp", - "version": "0.0.1", + "version": "1.0.0", "description": "MCP server for Joomla Web Services API operations", "type": "module", "main": "dist/index.js", diff --git a/src/client.ts b/src/client.ts index 52bf254..b774c56 100644 --- a/src/client.ts +++ b/src/client.ts @@ -9,7 +9,7 @@ * INGROUP: joomla-api-mcp * REPO: https://git.mokoconsulting.tech/MokoConsulting/joomla-api-mcp * PATH: /src/client.ts - * VERSION: 00.00.01 + * VERSION: 01.00.00 * BRIEF: HTTP client for Joomla Web Services API (v1) */ diff --git a/src/config.ts b/src/config.ts index 1757484..3b9c728 100644 --- a/src/config.ts +++ b/src/config.ts @@ -9,7 +9,7 @@ * INGROUP: joomla-api-mcp * REPO: https://git.mokoconsulting.tech/MokoConsulting/joomla-api-mcp * PATH: /src/config.ts - * VERSION: 00.00.01 + * VERSION: 01.00.00 * BRIEF: Configuration loader for Joomla API MCP connections */ diff --git a/src/index.ts b/src/index.ts index d0f456b..81335b0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,7 +10,7 @@ * INGROUP: joomla-api-mcp * REPO: https://git.mokoconsulting.tech/MokoConsulting/joomla-api-mcp * PATH: /src/index.ts - * VERSION: 00.00.01 + * VERSION: 01.00.00 * BRIEF: MCP server entry point โ€” registers all Joomla API tools */ @@ -53,7 +53,7 @@ const ConnectionParam = { const server = new McpServer({ name: 'joomla-api-mcp', - version: '0.0.1', + version: '1.0.0', }); // โ”€โ”€ Articles โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ diff --git a/src/types.ts b/src/types.ts index 08e5e55..d9f2935 100644 --- a/src/types.ts +++ b/src/types.ts @@ -9,7 +9,7 @@ * INGROUP: joomla-api-mcp * REPO: https://git.mokoconsulting.tech/MokoConsulting/joomla-api-mcp * PATH: /src/types.ts - * VERSION: 00.00.01 + * VERSION: 01.00.00 * BRIEF: TypeScript type definitions for Joomla API MCP server */ -- 2.52.0 From d7fddb49d3737fee7dd6b3250b6f58358f7ac8d5 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 26 Apr 2026 16:35:57 +0000 Subject: [PATCH 07/16] chore: add TODO.md from MokoStandards --- TODO.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 TODO.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..488a8ab --- /dev/null +++ b/TODO.md @@ -0,0 +1,12 @@ +# TODO + +> **Note:** This file is not tracked in version control (.gitignore). It is for local task tracking only. + +## Critical + - + +## Normal + - + +## Low + - -- 2.52.0 From e6e90d30bcc302a57652aa2049792ccbdf289833 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 26 Apr 2026 13:38:45 -0500 Subject: [PATCH 08/16] chore: add .mokostandards platform definition (default-repository) --- .github/.mokostandards | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/.mokostandards diff --git a/.github/.mokostandards b/.github/.mokostandards new file mode 100644 index 0000000..80141b0 --- /dev/null +++ b/.github/.mokostandards @@ -0,0 +1 @@ +platform: default-repository -- 2.52.0 From 1a63ed6722cb76c2cd245c8ba44c10a4cafd3817 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 26 Apr 2026 16:03:11 -0500 Subject: [PATCH 09/16] chore: add profile.ps1 to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 01449c1..6748a67 100644 --- a/.gitignore +++ b/.gitignore @@ -200,3 +200,4 @@ venv/ *.coverage hypothesis/ +profile.ps1 -- 2.52.0 From 00302af06dd403d0126accd33b37202b819874d8 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 26 Apr 2026 18:44:01 -0500 Subject: [PATCH 10/16] chore: remove copilot-agent.yml (GitHub-only, not needed on Gitea) --- .github/workflows/copilot-agent.yml | 44 ----------------------------- 1 file changed, 44 deletions(-) delete mode 100644 .github/workflows/copilot-agent.yml diff --git a/.github/workflows/copilot-agent.yml b/.github/workflows/copilot-agent.yml deleted file mode 100644 index 782945b..0000000 --- a/.github/workflows/copilot-agent.yml +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright (C) 2025 Moko Consulting -# SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later -# -# GitHub Actions workflow for Copilot coding agent -# This workflow demonstrates how to use the firewall configuration - -name: Copilot Coding Agent - -on: - pull_request: - types: [opened, synchronize, reopened] - issue_comment: - types: [created] - -permissions: - contents: write - pull-requests: write - issues: write - -jobs: - copilot-agent: - name: Run Copilot Coding Agent - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Configure Copilot Firewall - run: | - echo "Configuring firewall allowlist for enterprise-ready sites..." - bash .github/copilot/setup-firewall.sh - echo "Firewall configuration completed" - - - name: Run Copilot Agent - uses: github/copilot-swe-agent@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - issue_number: ${{ github.event.issue.number || github.event.pull_request.number }} - env: - # Environment variables are set by setup-firewall.sh - COPILOT_FIREWALL_ALLOWLIST: ${{ env.COPILOT_FIREWALL_ALLOWLIST }} -- 2.52.0 From 61dce0abac67a6307272c651dcabf51813f52d48 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 26 Apr 2026 22:35:29 -0500 Subject: [PATCH 11/16] chore: replace jmiller-moko with jmiller, move .mokostandards to .gitea/ --- {.github => .gitea}/.mokostandards | 0 .gitea/workflows/auto-assign.yml | 4 ++-- .gitea/workflows/auto-dev-issue.yml | 4 ++-- .gitea/workflows/deploy-demo.yml | 4 ++-- .gitea/workflows/deploy-dev.yml | 2 +- .gitea/workflows/repository-cleanup.yml | 2 +- .gitea/workflows/standards-compliance.yml | 2 +- .github/workflows/auto-assign.yml | 4 ++-- .github/workflows/auto-dev-issue.yml | 4 ++-- .github/workflows/deploy-demo.yml | 4 ++-- .github/workflows/deploy-dev.yml | 2 +- .github/workflows/repository-cleanup.yml | 2 +- .github/workflows/standards-compliance.yml | 2 +- .gitignore | 1 + 14 files changed, 19 insertions(+), 18 deletions(-) rename {.github => .gitea}/.mokostandards (100%) diff --git a/.github/.mokostandards b/.gitea/.mokostandards similarity index 100% rename from .github/.mokostandards rename to .gitea/.mokostandards diff --git a/.gitea/workflows/auto-assign.yml b/.gitea/workflows/auto-assign.yml index d0b70f6..1996c1c 100644 --- a/.gitea/workflows/auto-assign.yml +++ b/.gitea/workflows/auto-assign.yml @@ -7,7 +7,7 @@ # REPO: https://github.com/mokoconsulting-tech/MokoStandards # PATH: /.github/workflows/auto-assign.yml # VERSION: 04.06.00 -# BRIEF: Auto-assign jmiller-moko to unassigned issues and PRs every 15 minutes +# BRIEF: Auto-assign jmiller to unassigned issues and PRs every 15 minutes name: Auto-Assign Issues & PRs @@ -35,7 +35,7 @@ jobs: GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} run: | REPO="${{ github.repository }}" - ASSIGNEE="jmiller-moko" + ASSIGNEE="jmiller" echo "## ๐Ÿท๏ธ Auto-Assign Report" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY diff --git a/.gitea/workflows/auto-dev-issue.yml b/.gitea/workflows/auto-dev-issue.yml index 9b5fbe2..f61e1fc 100644 --- a/.gitea/workflows/auto-dev-issue.yml +++ b/.gitea/workflows/auto-dev-issue.yml @@ -135,7 +135,7 @@ jobs: --title "$SUB_FULL_TITLE" \ --body "$SUB_BODY" \ --label "${SUB_LABELS}" \ - --assignee "jmiller-moko" 2>&1) + --assignee "jmiller" 2>&1) SUB_NUM=$(echo "$SUB_URL" | grep -oE '[0-9]+$') if [ -n "$SUB_NUM" ]; then @@ -154,7 +154,7 @@ jobs: --title "$TITLE" \ --body "$PARENT_BODY" \ --label "${LABEL_TYPE},version" \ - --assignee "jmiller-moko" 2>&1) + --assignee "jmiller" 2>&1) PARENT_NUM=$(echo "$PARENT_URL" | grep -oE '[0-9]+$') diff --git a/.gitea/workflows/deploy-demo.yml b/.gitea/workflows/deploy-demo.yml index f5fac4a..206d178 100644 --- a/.gitea/workflows/deploy-demo.yml +++ b/.gitea/workflows/deploy-demo.yml @@ -94,7 +94,7 @@ jobs: AUTHORIZED="false" # Hardcoded authorized users โ€” always allowed to deploy - AUTHORIZED_USERS="jmiller-moko github-actions[bot]" + AUTHORIZED_USERS="jmiller github-actions[bot]" for user in $AUTHORIZED_USERS; do if [ "$ACTOR" = "$user" ]; then AUTHORIZED="true" @@ -704,7 +704,7 @@ jobs: --title "$TITLE" \ --body "$BODY" \ --label "$LABEL" \ - --assignee "jmiller-moko" \ + --assignee "jmiller" \ | tee -a "$GITHUB_STEP_SUMMARY" fi diff --git a/.gitea/workflows/deploy-dev.yml b/.gitea/workflows/deploy-dev.yml index 7781d00..1814ea0 100644 --- a/.gitea/workflows/deploy-dev.yml +++ b/.gitea/workflows/deploy-dev.yml @@ -99,7 +99,7 @@ jobs: AUTHORIZED="false" # Hardcoded authorized users โ€” always allowed to deploy - AUTHORIZED_USERS="jmiller-moko github-actions[bot]" + AUTHORIZED_USERS="jmiller github-actions[bot]" for user in $AUTHORIZED_USERS; do if [ "$ACTOR" = "$user" ]; then AUTHORIZED="true" diff --git a/.gitea/workflows/repository-cleanup.yml b/.gitea/workflows/repository-cleanup.yml index ea9219d..96c2a8c 100644 --- a/.gitea/workflows/repository-cleanup.yml +++ b/.gitea/workflows/repository-cleanup.yml @@ -80,7 +80,7 @@ jobs: echo "โœ… Scheduled run โ€” authorized" exit 0 fi - AUTHORIZED_USERS="jmiller-moko github-actions[bot]" + AUTHORIZED_USERS="jmiller github-actions[bot]" for user in $AUTHORIZED_USERS; do if [ "$ACTOR" = "$user" ]; then echo "โœ… ${ACTOR} authorized" diff --git a/.gitea/workflows/standards-compliance.yml b/.gitea/workflows/standards-compliance.yml index 79aaedd..44ab47d 100644 --- a/.gitea/workflows/standards-compliance.yml +++ b/.gitea/workflows/standards-compliance.yml @@ -2601,7 +2601,7 @@ jobs: echo "Updated issue #${EXISTING}" else gh issue create --repo "$REPO" --title "$TITLE" --body "$BODY" \ - --label "$LABEL" --assignee "jmiller-moko" + --label "$LABEL" --assignee "jmiller" fi # CUSTOMIZATION: diff --git a/.github/workflows/auto-assign.yml b/.github/workflows/auto-assign.yml index d0b70f6..1996c1c 100644 --- a/.github/workflows/auto-assign.yml +++ b/.github/workflows/auto-assign.yml @@ -7,7 +7,7 @@ # REPO: https://github.com/mokoconsulting-tech/MokoStandards # PATH: /.github/workflows/auto-assign.yml # VERSION: 04.06.00 -# BRIEF: Auto-assign jmiller-moko to unassigned issues and PRs every 15 minutes +# BRIEF: Auto-assign jmiller to unassigned issues and PRs every 15 minutes name: Auto-Assign Issues & PRs @@ -35,7 +35,7 @@ jobs: GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} run: | REPO="${{ github.repository }}" - ASSIGNEE="jmiller-moko" + ASSIGNEE="jmiller" echo "## ๐Ÿท๏ธ Auto-Assign Report" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/auto-dev-issue.yml b/.github/workflows/auto-dev-issue.yml index 9b5fbe2..f61e1fc 100644 --- a/.github/workflows/auto-dev-issue.yml +++ b/.github/workflows/auto-dev-issue.yml @@ -135,7 +135,7 @@ jobs: --title "$SUB_FULL_TITLE" \ --body "$SUB_BODY" \ --label "${SUB_LABELS}" \ - --assignee "jmiller-moko" 2>&1) + --assignee "jmiller" 2>&1) SUB_NUM=$(echo "$SUB_URL" | grep -oE '[0-9]+$') if [ -n "$SUB_NUM" ]; then @@ -154,7 +154,7 @@ jobs: --title "$TITLE" \ --body "$PARENT_BODY" \ --label "${LABEL_TYPE},version" \ - --assignee "jmiller-moko" 2>&1) + --assignee "jmiller" 2>&1) PARENT_NUM=$(echo "$PARENT_URL" | grep -oE '[0-9]+$') diff --git a/.github/workflows/deploy-demo.yml b/.github/workflows/deploy-demo.yml index f5fac4a..206d178 100644 --- a/.github/workflows/deploy-demo.yml +++ b/.github/workflows/deploy-demo.yml @@ -94,7 +94,7 @@ jobs: AUTHORIZED="false" # Hardcoded authorized users โ€” always allowed to deploy - AUTHORIZED_USERS="jmiller-moko github-actions[bot]" + AUTHORIZED_USERS="jmiller github-actions[bot]" for user in $AUTHORIZED_USERS; do if [ "$ACTOR" = "$user" ]; then AUTHORIZED="true" @@ -704,7 +704,7 @@ jobs: --title "$TITLE" \ --body "$BODY" \ --label "$LABEL" \ - --assignee "jmiller-moko" \ + --assignee "jmiller" \ | tee -a "$GITHUB_STEP_SUMMARY" fi diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index a3f0d31..acc6d07 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -99,7 +99,7 @@ jobs: AUTHORIZED="false" # Hardcoded authorized users โ€” always allowed to deploy - AUTHORIZED_USERS="jmiller-moko github-actions[bot]" + AUTHORIZED_USERS="jmiller github-actions[bot]" for user in $AUTHORIZED_USERS; do if [ "$ACTOR" = "$user" ]; then AUTHORIZED="true" diff --git a/.github/workflows/repository-cleanup.yml b/.github/workflows/repository-cleanup.yml index ea9219d..96c2a8c 100644 --- a/.github/workflows/repository-cleanup.yml +++ b/.github/workflows/repository-cleanup.yml @@ -80,7 +80,7 @@ jobs: echo "โœ… Scheduled run โ€” authorized" exit 0 fi - AUTHORIZED_USERS="jmiller-moko github-actions[bot]" + AUTHORIZED_USERS="jmiller github-actions[bot]" for user in $AUTHORIZED_USERS; do if [ "$ACTOR" = "$user" ]; then echo "โœ… ${ACTOR} authorized" diff --git a/.github/workflows/standards-compliance.yml b/.github/workflows/standards-compliance.yml index 79aaedd..44ab47d 100644 --- a/.github/workflows/standards-compliance.yml +++ b/.github/workflows/standards-compliance.yml @@ -2601,7 +2601,7 @@ jobs: echo "Updated issue #${EXISTING}" else gh issue create --repo "$REPO" --title "$TITLE" --body "$BODY" \ - --label "$LABEL" --assignee "jmiller-moko" + --label "$LABEL" --assignee "jmiller" fi # CUSTOMIZATION: diff --git a/.gitignore b/.gitignore index 6748a67..726a684 100644 --- a/.gitignore +++ b/.gitignore @@ -201,3 +201,4 @@ venv/ hypothesis/ profile.ps1 +.mcp.json -- 2.52.0 From 770deb1fb56e81c8a96d45573c30e251907b155c Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sun, 26 Apr 2026 22:55:11 -0500 Subject: [PATCH 12/16] =?UTF-8?q?chore:=20remove=20.github/=20=E2=80=94=20?= =?UTF-8?q?all=20workflows=20in=20.gitea/=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/CLAUDE.md | 319 -- .github/CODEOWNERS | 54 - .github/ISSUE_TEMPLATE/adr.md | 125 - .github/ISSUE_TEMPLATE/bug_report.md | 63 - .github/ISSUE_TEMPLATE/config.yml | 18 - .github/ISSUE_TEMPLATE/documentation.md | 67 - .github/ISSUE_TEMPLATE/enterprise_support.md | 100 - .github/ISSUE_TEMPLATE/feature_request.md | 66 - .github/ISSUE_TEMPLATE/firewall-request.md | 203 -- .github/ISSUE_TEMPLATE/question.md | 86 - .github/ISSUE_TEMPLATE/request-license.md | 120 - .github/ISSUE_TEMPLATE/rfc.md | 141 - .github/ISSUE_TEMPLATE/security.md | 66 - .github/copilot-instructions.md | 316 -- .github/copilot/README.md | 126 - .github/copilot/firewall-allowlist.json | 73 - .github/copilot/setup-firewall.sh | 57 - .github/workflows/auto-assign.yml | 76 - .github/workflows/auto-dev-issue.yml | 207 -- .github/workflows/auto-release.yml | 337 --- .github/workflows/changelog-validation.yml | 101 - .github/workflows/codeql-analysis.yml | 115 - .github/workflows/deploy-demo.yml | 734 ----- .github/workflows/deploy-dev.yml | 701 ----- .../workflows/enterprise-firewall-setup.yml | 758 ----- .github/workflows/repository-cleanup.yml | 525 ---- .github/workflows/standards-compliance.yml | 2614 ----------------- .github/workflows/sync-version-on-merge.yml | 133 - 28 files changed, 8301 deletions(-) delete mode 100644 .github/CLAUDE.md delete mode 100644 .github/CODEOWNERS delete mode 100644 .github/ISSUE_TEMPLATE/adr.md delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md delete mode 100644 .github/ISSUE_TEMPLATE/config.yml delete mode 100644 .github/ISSUE_TEMPLATE/documentation.md delete mode 100644 .github/ISSUE_TEMPLATE/enterprise_support.md delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md delete mode 100644 .github/ISSUE_TEMPLATE/firewall-request.md delete mode 100644 .github/ISSUE_TEMPLATE/question.md delete mode 100644 .github/ISSUE_TEMPLATE/request-license.md delete mode 100644 .github/ISSUE_TEMPLATE/rfc.md delete mode 100644 .github/ISSUE_TEMPLATE/security.md delete mode 100644 .github/copilot-instructions.md delete mode 100644 .github/copilot/README.md delete mode 100644 .github/copilot/firewall-allowlist.json delete mode 100755 .github/copilot/setup-firewall.sh delete mode 100644 .github/workflows/auto-assign.yml delete mode 100644 .github/workflows/auto-dev-issue.yml delete mode 100644 .github/workflows/auto-release.yml delete mode 100644 .github/workflows/changelog-validation.yml delete mode 100644 .github/workflows/codeql-analysis.yml delete mode 100644 .github/workflows/deploy-demo.yml delete mode 100644 .github/workflows/deploy-dev.yml delete mode 100644 .github/workflows/enterprise-firewall-setup.yml delete mode 100644 .github/workflows/repository-cleanup.yml delete mode 100644 .github/workflows/standards-compliance.yml delete mode 100644 .github/workflows/sync-version-on-merge.yml diff --git a/.github/CLAUDE.md b/.github/CLAUDE.md deleted file mode 100644 index 4838bc7..0000000 --- a/.github/CLAUDE.md +++ /dev/null @@ -1,319 +0,0 @@ - - -> [!IMPORTANT] -> **๐Ÿ”ง AI Self-Update Required on First Use** -> -> This file may contain unfilled `{{TOKEN}}` placeholders. The MokoStandards bulk-sync system -> replaces them automatically at sync time; if you can still see them, sync has not yet run for -> this repository โ€” or this is a brand-new repo. -> -> **On your very first interaction in this repository, before answering any other question:** -> -> 1. Check whether any `{{TOKEN}}` placeholders remain in this file. -> 2. If they do, locate the values as described in the table below and replace every occurrence -> in **both** `.github/copilot-instructions.md` and `.github/CLAUDE.md`. -> 3. After replacing all tokens, **delete this entire notice block** (from `> [!IMPORTANT]` -> through the closing `> ---` line) in both files. -> 4. Tell the developer: *"I've self-updated the AI context files with this repository's -> values โ€” all set."* -> -> | Placeholder | Where to find the value | -> |---|---| -> | `MokoStandards-Template-Generic` | The GitHub repository name (visible in the URL, `README.md` heading, or `git remote -v`) | -> | `https://github.com/mokoconsulting-tech/MokoStandards-Template-Generic` | Full GitHub URL, e.g. `https://github.com/mokoconsulting-tech/` | -> | `PHP` | The dominant programming language (check file extensions in the repository) | -> | `Generic project` | The project type: `PHP library`, `Joomla extension`, `Dolibarr module`, `WaaS site`, etc. โ€” infer from repo structure | -> -> --- - -# MokoStandards-Template-Generic โ€” GitHub Copilot Custom Instructions - -## What This Repo Is - -This is a **Moko Consulting** repository governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). All coding standards, workflows, and policies are defined there and enforced here via bulk sync. - -Repository URL: https://github.com/mokoconsulting-tech/MokoStandards-Template-Generic -Primary language: **PHP** -Platform type: **Generic project** - ---- - -## Primary Language - -**PHP is the primary language for this repository.** Follow the conventions documented in [MokoStandards coding-style-guide](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md). - -YAML uses 2-space indentation (spaces, not tabs). All other text files use tabs per `.editorconfig`. - ---- - -## File Header โ€” Always Required on New Files - -Every new file needs a copyright header as its first content. Use the minimal form unless the file is a policy doc, README, or public API. - -**PHP:** -```php - - * - * This file is part of a Moko Consulting project. - * - * SPDX-License-Identifier: GPL-3.0-or-later - * - * FILE INFORMATION - * DEFGROUP: MokoStandards-Template-Generic.Module - * INGROUP: MokoStandards-Template-Generic - * REPO: https://github.com/mokoconsulting-tech/MokoStandards-Template-Generic - * PATH: /path/to/file.php - * VERSION: XX.YY.ZZ - * BRIEF: One-line description of purpose - */ - -declare(strict_types=1); -``` - -**Markdown:** -```markdown - -``` - -**YAML / Shell:** Use `#` comments with the same fields. JSON files are exempt. - ---- - -## Version Management - -**`README.md` is the single source of truth for the repository version.** - -- **Bump the patch version on every PR** โ€” increment `XX.YY.ZZ` (e.g. `01.02.03` โ†’ `01.02.04`) in `README.md` before opening the PR; the `sync-version-on-merge` workflow propagates it automatically to all badges and `FILE INFORMATION` headers on merge to `main`. -- The `VERSION: XX.YY.ZZ` field in the README.md `FILE INFORMATION` block governs all other version references. -- Update the version in `README.md` only โ€” the `sync-version-on-merge` workflow propagates it automatically to all badges and `FILE INFORMATION` headers on merge to `main`. -- Version format is zero-padded semver: `XX.YY.ZZ` (e.g. `04.00.04`). -- Never hardcode a specific version in document body text โ€” use the badge or FILE INFORMATION header only. - ---- - -## GitHub Actions โ€” Token Usage - -Every workflow must use **`secrets.GH_TOKEN`** (the org-level Personal Access Token). This applies to all `actions/checkout`, `gh` CLI calls, and any step that talks to the GitHub API. - -```yaml -# โœ… Correct -- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.GH_TOKEN }} - -env: - GH_TOKEN: ${{ secrets.GH_TOKEN }} -``` - -```yaml -# โŒ Wrong โ€” never use these in workflows -token: ${{ github.token }} -token: ${{ secrets.GITHUB_TOKEN }} -``` - -PHP scripts read the token with: `getenv('GH_TOKEN') ?: getenv('GITHUB_TOKEN')` โ€” `GH_TOKEN` is always preferred; `GITHUB_TOKEN` is accepted only as a local-dev fallback. - ---- - -## Composer Package (PHP repositories) - -This repository requires the MokoStandards enterprise library. The `composer.json` must include: - -```json -{ - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/mokoconsulting-tech/MokoStandards" - } - ], - "require": { - "mokoconsulting/mokostandards": "^4.0" - } -} -``` - -Run `composer install` after adding the dependency. See [package-installation.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/package-installation.md) for full instructions. - ---- - -## PHP Script Pattern - -All PHP scripts **must** extend `MokoStandards\Enterprise\CliFramework`. Never write standalone classes or extend the legacy `CliBase`. - -```php -#!/usr/bin/env php -setDescription('One-line description'); - $this->addArgument('--path', 'Repository root', '.'); - $this->addArgument('--dry-run', 'Preview without writing', false); - } - - protected function run(): int - { - $path = $this->getArgument('--path'); - $dryRun = (bool) $this->getArgument('--dry-run'); - - $this->log('INFO', "Processing: {$path}"); - return 0; - } -} - -$script = new MyScript('my_script', 'One-line description'); -exit($script->execute()); -``` - -**Key rules:** -- Abstract methods to implement: `configure()` and `run()` โ€” **not** `execute()` -- `execute()` is the **public entry point** that orchestrates setup (arg parsing, `initialize()`) and then calls your `run()` implementation; call it at the bottom with `exit($script->execute())` -- Entry point at the bottom: `$script->execute()` โ€” **not** `$script->run()` -- Constructor always takes `(string $name, string $description = '')`; pass the description here โ€” `setDescription()` inside `configure()` is only needed to override it -- `log(string $level, string $message)` โ€” level is the **first** argument (INFO / SUCCESS / WARNING / ERROR) -- `$this->dryRun` and `$this->verbose` are set automatically from `--dry-run` / `--verbose` - ---- - -## Naming Conventions - -| Context | Convention | Example | -|---------|-----------|---------| -| PHP class | `PascalCase` | `MyService` | -| PHP method / function | `camelCase` | `getUserData()` | -| PHP variable | `$snake_case` | `$repo_path` | -| PHP constant | `UPPER_SNAKE_CASE` | `DEFAULT_THRESHOLD` | -| PHP class file | `PascalCase.php` | `ApiClient.php` | -| PHP script file | `snake_case.php` | `check_health.php` | -| YAML workflow | `kebab-case.yml` | `bulk-repo-sync.yml` | -| Markdown doc | `kebab-case.md` | `coding-style-guide.md` | - ---- - -## Commit Messages - -Format: `(): ` โ€” imperative, lower-case subject, no trailing period. - -Valid types: `feat` ยท `fix` ยท `docs` ยท `chore` ยท `ci` ยท `refactor` ยท `style` ยท `test` ยท `perf` ยท `revert` ยท `build` - -Examples: -- `feat(module): add user preference caching` -- `fix(api): handle null response from external service` -- `docs(readme): update installation instructions` -- `chore(deps): bump phpunit to 11.x` - ---- - -## Branch Naming - -Approved prefixes: `dev/` ยท `rc/` ยท `version/` ยท `copilot/` ยท `dependabot/` - -- `dev/XX.YY` or `dev/feature-name` โ€” development (version optional) -- `rc/XX.YY.ZZ` โ€” release candidate (three-part required) -- `version/XX.YY` โ€” archive branch (auto-created, two-part) -- Release tags: `vXX` (major only โ€” one release per major version) -- Patch `00` = development (no release), first release = `01` - -Examples: -- โœ… `dev/04.06` ยท `dev/new-dashboard` ยท `rc/04.06.01` -- โŒ `feature/my-thing` โ€” rejected by branch protection - ---- - -## Keeping Documentation Current - -Whenever you make code changes, update the corresponding documentation in the same commit or PR. Do not leave docs stale. - -| Change type | Documentation to update | -|-------------|------------------------| -| New or renamed public PHP method | PHPDoc block on the method; `docs/api/` index for that class | -| New or changed CLI script argument | Script's own `--help` text; `docs/api/` or equivalent | -| New or changed GitHub Actions workflow | `docs/workflows/.md` | -| New or changed policy | Corresponding file under `docs/policy/` | -| New library class or major feature | `CHANGELOG.md` entry under `Added` | -| Bug fix | `CHANGELOG.md` entry under `Fixed` | -| Breaking change | `CHANGELOG.md` entry under `Changed`; update `CONTRIBUTING.md` if contributor steps change | -| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block | -| **Every PR** | **Bump the patch version** โ€” increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it to all headers and badges on merge | - -If your code change makes any existing doc sentence false or incomplete, fix the doc before closing the PR. - ---- - -## Key Constraints - -- Never commit directly to `main` โ€” all changes go via PR, squash-merged -- Never skip the FILE INFORMATION block on a new file -- Never use bare `catch (\Throwable $e) {}` without logging or re-throwing -- Never hardcode version numbers in body text โ€” update `README.md` and let automation propagate -- Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows โ€” always use `secrets.GH_TOKEN` -- Never extend `CliBase` in PHP scripts โ€” extend `MokoStandards\Enterprise\CliFramework` -- Never call `$script->run()` as the entry point โ€” call `$script->execute()` -- Policy documents and guides must not be mixed - ---- - -## MokoStandards Reference - -This repository is governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). Authoritative policies: - -| Document | Purpose | -|----------|---------| -| [file-header-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/file-header-standards.md) | Copyright-header rules for every file type | -| [coding-style-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md) | Naming and formatting conventions | -| [branching-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/branching-strategy.md) | Branch naming, hierarchy, and release workflow | -| [merge-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR title/body conventions | -| [changelog-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md | -| [scripting-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/scripting-standards.md) | PHP script requirements and CliFramework usage | -| [package-installation.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/package-installation.md) | Installing `mokoconsulting/mokostandards` via Composer | - - -## Release and Infrastructure Standards - -- **Release tags**: All repos must have the 5 standard tags: development, alpha, beta, release-candidate, stable -- **Update server priority**: Gitea must be priority 1, GitHub priority 2 in updateservers -- **Secrets**: All repos have GA_TOKEN and GH_TOKEN as Actions secrets -- **Branch protection**: main branch is protected, only jmiller can push directly -- **Push mirrors**: All repos mirror to GitHub via built-in push mirror with sync_on_commit - diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index 0108cc2..0000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# SPDX-License-Identifier: GPL-3.0-or-later -# -# CODEOWNERS โ€” require approval from jmiller-moko for protected paths -# Synced from MokoStandards. Do not edit manually. -# -# Changes to these paths require review from the listed owners before merge. -# Combined with branch protection (require PR reviews), this prevents -# unauthorized modifications to workflows, configs, and governance files. - -# โ”€โ”€ Synced workflows (managed by MokoStandards โ€” do not edit manually) โ”€โ”€โ”€โ”€ -/.github/workflows/deploy-dev.yml @jmiller-moko -/.github/workflows/deploy-demo.yml @jmiller-moko -/.github/workflows/deploy-manual.yml @jmiller-moko -/.github/workflows/auto-release.yml @jmiller-moko -/.github/workflows/auto-dev-issue.yml @jmiller-moko -/.github/workflows/auto-assign.yml @jmiller-moko -/.github/workflows/sync-version-on-merge.yml @jmiller-moko -/.github/workflows/enterprise-firewall-setup.yml @jmiller-moko -/.github/workflows/repository-cleanup.yml @jmiller-moko -/.github/workflows/standards-compliance.yml @jmiller-moko -/.github/workflows/codeql-analysis.yml @jmiller-moko -/.github/workflows/repo_health.yml @jmiller-moko -/.github/workflows/ci-joomla.yml @jmiller-moko -/.github/workflows/update-server.yml @jmiller-moko -/.github/workflows/deploy-manual.yml @jmiller-moko -/.github/workflows/ci-dolibarr.yml @jmiller-moko -/.github/workflows/publish-to-mokodolimods.yml @jmiller-moko -/.github/workflows/changelog-validation.yml @jmiller-moko -# Custom workflows in .github/workflows/ not listed above are repo-owned. - -# โ”€โ”€ GitHub configuration โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -/.github/ISSUE_TEMPLATE/ @jmiller-moko -/.github/CODEOWNERS @jmiller-moko -/.github/copilot.yml @jmiller-moko -/.github/copilot-instructions.md @jmiller-moko -/.github/CLAUDE.md @jmiller-moko -/.github/.mokostandards @jmiller-moko - -# โ”€โ”€ Build and config files โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -/composer.json @jmiller-moko -/phpstan.neon @jmiller-moko -/Makefile @jmiller-moko -/.ftpignore @jmiller-moko -/.gitignore @jmiller-moko -/.gitattributes @jmiller-moko -/.editorconfig @jmiller-moko - -# โ”€โ”€ Governance documents โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -/LICENSE @jmiller-moko -/CONTRIBUTING.md @jmiller-moko -/SECURITY.md @jmiller-moko -/GOVERNANCE.md @jmiller-moko -/CODE_OF_CONDUCT.md @jmiller-moko diff --git a/.github/ISSUE_TEMPLATE/adr.md b/.github/ISSUE_TEMPLATE/adr.md deleted file mode 100644 index 6fea768..0000000 --- a/.github/ISSUE_TEMPLATE/adr.md +++ /dev/null @@ -1,125 +0,0 @@ ---- -name: Architecture Decision Record (ADR) -about: Propose or document an architectural decision -title: '[ADR] ' -labels: 'architecture, decision' -assignees: '' - ---- - - - -## ADR Number -ADR-XXXX - -## Status -- [ ] Proposed -- [ ] Accepted -- [ ] Deprecated -- [ ] Superseded by ADR-XXXX - -## Context -Describe the issue or problem that motivates this decision. - -## Decision -State the architecture decision and provide rationale. - -## Consequences -### Positive -- List positive consequences - -### Negative -- List negative consequences or trade-offs - -### Neutral -- List neutral aspects - -## Alternatives Considered -### Alternative 1 -- Description -- Pros -- Cons -- Why not chosen - -### Alternative 2 -- Description -- Pros -- Cons -- Why not chosen - -## Implementation Plan -1. Step 1 -2. Step 2 -3. Step 3 - -## Stakeholders -- **Decision Makers**: @user1, @user2 -- **Consulted**: @user3, @user4 -- **Informed**: team-name - -## Technical Details -### Architecture Diagram -``` -[Add diagram or link] -``` - -### Dependencies -- Dependency 1 -- Dependency 2 - -### Impact Analysis -- **Performance**: [Impact description] -- **Security**: [Impact description] -- **Scalability**: [Impact description] -- **Maintainability**: [Impact description] - -## Testing Strategy -- [ ] Unit tests -- [ ] Integration tests -- [ ] Performance tests -- [ ] Security tests - -## Documentation -- [ ] Architecture documentation updated -- [ ] API documentation updated -- [ ] Developer guide updated -- [ ] Runbook created - -## Migration Path -Describe how to migrate from current state to new architecture. - -## Rollback Plan -Describe how to rollback if issues occur. - -## Timeline -- **Proposal Date**: -- **Decision Date**: -- **Implementation Start**: -- **Expected Completion**: - -## References -- Related ADRs: -- External resources: -- RFCs: - -## Review Checklist -- [ ] Aligns with enterprise architecture principles -- [ ] Security implications reviewed -- [ ] Performance implications reviewed -- [ ] Cost implications reviewed -- [ ] Compliance requirements met -- [ ] Team consensus achieved diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index c57ce5b..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -name: Bug Report -about: Report a bug or issue with the project -title: '[BUG] ' -labels: 'bug' -assignees: '' - ---- - - - -## Bug Description -A clear and concise description of what the bug is. - -## Steps to Reproduce -1. Go to '...' -2. Click on '...' -3. Scroll down to '...' -4. See error - -## Expected Behavior -A clear and concise description of what you expected to happen. - -## Actual Behavior -A clear and concise description of what actually happened. - -## Screenshots -If applicable, add screenshots to help explain your problem. - -## Environment -- **Project**: [e.g., MokoDoliTools, moko-cassiopeia] -- **Version**: [e.g., 1.2.3] -- **Platform**: [e.g., Dolibarr 18.0, Joomla 5.0] -- **PHP Version**: [e.g., 8.1] -- **Database**: [e.g., MySQL 8.0, PostgreSQL 14] -- **Browser** (if applicable): [e.g., Chrome 120, Firefox 121] -- **OS**: [e.g., Ubuntu 22.04, Windows 11] - -## Additional Context -Add any other context about the problem here. - -## Possible Solution -If you have suggestions on how to fix the issue, please describe them here. - -## Checklist -- [ ] I have searched for similar issues before creating this one -- [ ] I have provided all the requested information -- [ ] I have tested this on the latest stable version -- [ ] I have checked the documentation and couldn't find a solution diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index e9f2f60..0000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,18 +0,0 @@ ---- -blank_issues_enabled: true -contact_links: - - name: ๐Ÿ’ผ Enterprise Support - url: https://mokoconsulting.tech/enterprise - about: Enterprise-level support and consultation services - - name: ๐Ÿ’ฌ Ask a Question - url: https://mokoconsulting.tech/ - about: Get help or ask questions through our website - - name: ๐Ÿ“š MokoStandards Documentation - url: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards - about: View our coding standards and best practices - - name: ๐Ÿ”’ Report a Security Vulnerability - url: https://git.mokoconsulting.tech/MokoConsulting/.github-private/security/advisories/new - about: Report security vulnerabilities privately (for critical issues) - - name: ๐Ÿ’ก Community Discussions - url: https://github.com/orgs/mokoconsulting-tech/discussions - about: Join community discussions and Q&A diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.md deleted file mode 100644 index 133e8b6..0000000 --- a/.github/ISSUE_TEMPLATE/documentation.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -name: Documentation Issue -about: Report an issue with documentation -title: '[DOCS] ' -labels: 'documentation' -assignees: '' - ---- - - - -## Documentation Issue - -**Location**: - - -## Issue Type - -- [ ] Typo or grammar error -- [ ] Outdated information -- [ ] Missing documentation -- [ ] Unclear explanation -- [ ] Broken links -- [ ] Missing examples -- [ ] Other (specify below) - -## Description - - -## Current Content - -``` -Current text here -``` - -## Suggested Improvement - -``` -Suggested text here -``` - -## Additional Context - - -## Standards Alignment -- [ ] Follows MokoStandards documentation guidelines -- [ ] Uses en_US/en_GB localization -- [ ] Includes proper SPDX headers where applicable - -## Checklist -- [ ] I have searched for similar documentation issues -- [ ] I have provided a clear description -- [ ] I have suggested an improvement (if applicable) diff --git a/.github/ISSUE_TEMPLATE/enterprise_support.md b/.github/ISSUE_TEMPLATE/enterprise_support.md deleted file mode 100644 index 6b1133d..0000000 --- a/.github/ISSUE_TEMPLATE/enterprise_support.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -name: Enterprise Support Request -about: Request enterprise-level support or consultation -title: '[ENTERPRISE] ' -labels: 'enterprise, support' -assignees: '' - ---- - - - -## Support Request Type -- [ ] Critical Production Issue -- [ ] Performance Optimization -- [ ] Security Audit -- [ ] Architecture Review -- [ ] Custom Development -- [ ] Migration Support -- [ ] Training & Onboarding -- [ ] Other (please specify) - -## Priority Level -- [ ] P0 - Critical (Production Down) -- [ ] P1 - High (Major Feature Broken) -- [ ] P2 - Medium (Non-Critical Issue) -- [ ] P3 - Low (Enhancement/Question) - -## Organization Details -- **Company Name**: -- **Contact Person**: -- **Email**: -- **Phone** (for P0/P1 issues): -- **Timezone**: - -## Issue Description -Provide a clear and detailed description of your request or issue. - -## Business Impact -Describe the impact on your business operations: -- Number of users affected: -- Revenue impact (if applicable): -- Deadline/SLA requirements: - -## Environment Details -- **Deployment Type**: [On-Premise / Cloud / Hybrid] -- **Platform**: [Joomla / Dolibarr / Custom] -- **Version**: -- **Infrastructure**: [AWS / Azure / GCP / Other] -- **Scale**: [Users / Transactions / Data Volume] - -## Current Configuration -```yaml -# Paste relevant configuration (sanitize sensitive data) -``` - -## Logs and Diagnostics -``` -# Paste relevant logs (sanitize sensitive data) -``` - -## Attempted Solutions -Describe any troubleshooting steps already taken. - -## Expected Resolution -Describe your expected outcome or resolution. - -## Additional Resources -- **Documentation Links**: -- **Related Issues**: -- **Screenshots/Videos**: - -## Enterprise SLA -- [ ] Standard Support (initial response within 1โ€“3 weeks) -- [ ] Premium Support (initial response within 5 business days) -- [ ] Critical Support (initial response within 72 hours) -- [ ] Custom SLA (specify): - -## Compliance Requirements -- [ ] GDPR -- [ ] HIPAA -- [ ] SOC 2 -- [ ] ISO 27001 -- [ ] Other (specify): - ---- -**Note**: Enterprise support requests require an active support contract. If you don't have one, please contact us at enterprise@mokoconsulting.tech diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index e945325..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -name: Feature Request -about: Suggest a new feature or enhancement -title: '[FEATURE] ' -labels: 'enhancement' -assignees: '' - ---- - - - -## Feature Description -A clear and concise description of the feature you'd like to see. - -## Problem or Use Case -Describe the problem this feature would solve or the use case it addresses. -Ex. I'm always frustrated when [...] - -## Proposed Solution -A clear and concise description of what you want to happen. - -## Alternative Solutions -A clear and concise description of any alternative solutions or features you've considered. - -## Benefits -Describe how this feature would benefit users: -- Who would use this feature? -- What problems does it solve? -- What value does it add? - -## Implementation Details (Optional) -If you have ideas about how this could be implemented, share them here: -- Technical approach -- Files/components that might need changes -- Any concerns or challenges you foresee - -## Additional Context -Add any other context, mockups, or screenshots about the feature request here. - -## Relevant Standards -Does this relate to any standards in [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards)? -- [ ] Accessibility (WCAG 2.1 AA) -- [ ] Localization (en_US/en_GB) -- [ ] Security best practices -- [ ] Code quality standards -- [ ] Other: [specify] - -## Checklist -- [ ] I have searched for similar feature requests before creating this one -- [ ] I have clearly described the use case and benefits -- [ ] I have considered alternative solutions -- [ ] This feature aligns with the project's goals and scope diff --git a/.github/ISSUE_TEMPLATE/firewall-request.md b/.github/ISSUE_TEMPLATE/firewall-request.md deleted file mode 100644 index 1b51d01..0000000 --- a/.github/ISSUE_TEMPLATE/firewall-request.md +++ /dev/null @@ -1,203 +0,0 @@ ---- -name: Firewall Request -about: Request firewall rule changes or access to external resources -title: '[FIREWALL] [Resource Name] - [Brief Description]' -labels: ['firewall-request', 'infrastructure', 'security'] -assignees: [] ---- - - - -## Firewall Request - -### Request Type -- [ ] Allow outbound access to external service/API -- [ ] Allow inbound access from external source -- [ ] Modify existing firewall rule -- [ ] Remove/revoke firewall rule -- [ ] Other (specify): - -### Resource Information -**Service/Domain Name**: -**IP Address(es)**: -**Port(s)**: -**Protocol**: -- [ ] HTTP (80) -- [ ] HTTPS (443) -- [ ] SSH (22) -- [ ] FTP (21) -- [ ] SFTP (22) -- [ ] Custom (specify): _______________ - -### Requestor Information -**Name**: -**GitHub Username**: @ -**Email**: @mokoconsulting.tech -**Team/Department**: -**Manager**: @ - -### Business Justification -**Why is this access needed?** - -**Which project(s) require this access?** - -**What functionality will break without this access?** - -**Is there an alternative solution?** -- [ ] Yes (explain): -- [ ] No - -### Security Considerations -**Data Classification**: -- [ ] Public -- [ ] Internal -- [ ] Confidential -- [ ] Restricted - -**Sensitive Data Transmission**: -- [ ] No sensitive data will be transmitted -- [ ] Sensitive data will be transmitted (encryption required) -- [ ] Authentication credentials will be transmitted (secure storage required) - -**Third-Party Service**: -- [ ] This is a trusted/verified third-party service -- [ ] This is a new/unverified service (security review required) - -**Service Documentation**: -(Provide link to service documentation or API specs) - -### Access Scope -**Affected Systems**: -- [ ] Development environment only -- [ ] Staging environment only -- [ ] Production environment -- [ ] All environments - -**Access Duration**: -- [ ] Permanent (ongoing business need) -- [ ] Temporary (specify end date): _______________ -- [ ] Testing only (specify duration): _______________ - -### Technical Details -**Source System(s)**: -(Which internal systems need access?) - -**Destination System(s)**: -(Which external systems need to be accessed?) - -**Expected Traffic Volume**: -(e.g., requests per hour/day) - -**Traffic Pattern**: -- [ ] Continuous -- [ ] Periodic (specify frequency): _______________ -- [ ] On-demand/manual -- [ ] Scheduled (specify schedule): _______________ - -### Testing Requirements -**Pre-Production Testing**: -- [ ] Request includes dev/staging access for testing -- [ ] Testing can be done with production access only -- [ ] No testing required (modify existing rule) - -**Testing Plan**: - -**Rollback Plan**: -(What happens if access needs to be revoked?) - -### Compliance & Audit -**Compliance Requirements**: -- [ ] GDPR considerations -- [ ] SOC 2 compliance required -- [ ] PCI DSS considerations -- [ ] Other regulatory requirements: _______________ -- [ ] No specific compliance requirements - -**Audit/Logging Requirements**: -- [ ] Standard logging sufficient -- [ ] Enhanced logging/monitoring required -- [ ] Real-time alerting required - -### Urgency -- [ ] Critical (production down, immediate access needed) -- [ ] High (needed within 24 hours) -- [ ] Normal (needed within 1 week) -- [ ] Low priority (needed within 1 month) - -**If critical/high urgency, explain why:** - -### Approvals -**Manager Approval**: -- [ ] Manager has been notified and approves this request - -**Security Team Review Required**: -- [ ] Yes (new external service, sensitive data) -- [ ] No (minor change, established service) - -### Additional Information - -**Related Documentation**: -(Links to relevant docs, RFCs, tickets, etc.) - -**Dependencies**: -(Other systems or changes this depends on) - -**Comments/Questions**: - ---- - -## For Infrastructure/Security Team Use Only - -**Do not edit below this line** - -### Security Review -- [ ] Security team review completed -- [ ] Risk assessment: Low / Medium / High -- [ ] Encryption required: Yes / No -- [ ] VPN required: Yes / No -- [ ] Additional security controls: _______________ - -**Reviewed By**: @_______________ -**Review Date**: _______________ -**Review Notes**: - -### Implementation -- [ ] Firewall rule created/modified -- [ ] Rule tested in dev/staging -- [ ] Rule deployed to production -- [ ] Monitoring/alerting configured -- [ ] Documentation updated - -**Firewall Rule ID**: _______________ -**Implementation Date**: _______________ -**Implemented By**: @_______________ - -**Configuration Details**: -``` -Source: -Destination: -Port/Protocol: -Action: Allow/Deny -``` - -### Verification -- [ ] Requestor confirmed access working -- [ ] Logs reviewed (no anomalies) -- [ ] Security scan completed (if applicable) - -**Verification Date**: _______________ -**Verified By**: @_______________ - -### Notes diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md deleted file mode 100644 index 6bd94b5..0000000 --- a/.github/ISSUE_TEMPLATE/question.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -name: Question -about: Ask a question about usage, features, or best practices -title: '[QUESTION] ' -labels: ['question'] -assignees: [] ---- - - - -## Question - -**Your question:** - - -## Context - -**What are you trying to accomplish?** - - -**What have you already tried?** - - -**Category**: -- [ ] Script usage -- [ ] Configuration -- [ ] Workflow setup -- [ ] Documentation interpretation -- [ ] Best practices -- [ ] Integration -- [ ] Other: __________ - -## Environment (if relevant) - -**Your setup**: -- Operating System: -- Version: - -## What You've Researched - -**Documentation reviewed**: -- [ ] README.md -- [ ] Project documentation -- [ ] Other (specify): __________ - -**Similar issues/questions found**: -- # -- # - -## Expected Outcome - -**What result are you hoping for?** - - -## Code/Configuration Samples - -**Relevant code or configuration** (if applicable): - -```bash -# Your code here -``` - -## Additional Context - -**Any other relevant information:** - - -**Screenshots** (if helpful): - - -## Urgency - -- [ ] Urgent (blocking work) -- [ ] Normal (can work on other things meanwhile) -- [ ] Low priority (just curious) - -## Checklist - -- [ ] I have searched existing issues and discussions -- [ ] I have reviewed relevant documentation -- [ ] I have provided sufficient context -- [ ] I have included code/configuration samples if relevant -- [ ] This is a genuine question (not a bug report or feature request) diff --git a/.github/ISSUE_TEMPLATE/request-license.md b/.github/ISSUE_TEMPLATE/request-license.md deleted file mode 100644 index d5e53e5..0000000 --- a/.github/ISSUE_TEMPLATE/request-license.md +++ /dev/null @@ -1,120 +0,0 @@ ---- -name: License Request -about: Request an organization license for Sublime Text -title: '[LICENSE REQUEST] Sublime Text - [Your Name]' -labels: ['license-request', 'admin'] -assignees: [] ---- - - - -## License Request - -### Tool Information -**Tool Name**: Sublime Text - -**License Type Requested**: Organization Pool - -**Personal Purchase**: -- [ ] I prefer to purchase my own license ($99 USD - recommended, immediate access) -- [ ] I prefer an organization license (1-2 business days, organization use only) -- [ ] I have already purchased my own license (registration only for support) - -### Requestor Information -**Name**: -**GitHub Username**: @ -**Email**: @mokoconsulting.tech -**Team/Department**: -**Manager**: @ - -### Justification -**Why do you need this license?** - -**Primary use case**: -- [ ] Remote development (SFTP to servers) -- [ ] Local development -- [ ] Code review -- [ ] Documentation editing -- [ ] Other (specify): - -**Which projects/repositories will you work on?** - -**Have you evaluated the free trial?** -- [ ] Yes, I've used the trial and Sublime Text meets my needs -- [ ] No, requesting license before trial - -**Alternative tools considered**: -- [ ] VS Code (free alternative) -- [ ] Vim/Neovim (free, terminal-based) -- [ ] Other: _______________ - -### Platform -- [ ] Windows -- [ ] macOS -- [ ] Linux (distribution: ________) - -### Urgency -- [ ] Urgent (needed within 24 hours - please justify) -- [ ] Normal (1-2 business days) -- [ ] Low priority (when available) - -**If urgent, please explain why:** - -### SFTP Plugin -**Note**: Sublime SFTP plugin ($16 USD) is a **separate personal purchase** and is NOT provided by the organization. - -- [ ] I understand SFTP plugin requires separate personal purchase -- [ ] I have already purchased SFTP plugin -- [ ] I will purchase SFTP plugin if needed for my work -- [ ] I don't need SFTP plugin (local development only) - -### Acknowledgments -- [ ] I have read the License Management Policy (/docs/github-private/LICENSE_MANAGEMENT.md) -- [ ] I understand organization licenses are for work use only -- [ ] I understand organization licenses must be returned upon leaving -- [ ] I understand personal purchases ($99) are an alternative with lifetime access -- [ ] I understand SFTP plugin ($16) requires separate personal purchase -- [ ] I agree to the terms of use - -### Additional Information - -**Expected daily usage hours**: _____ hours/day - -**Duration of need**: -- [ ] Permanent (ongoing role) -- [ ] Temporary project (_____ months) -- [ ] Trial/Evaluation (_____ weeks) - -**Comments/Questions**: - ---- - -## For Admin Use Only - -**Do not edit below this line** - -- [ ] Manager approval received (@manager-username) -- [ ] License available in pool (current: __/20) -- [ ] License type confirmed (Organization / Personal registration) -- [ ] License key sent via encrypted email -- [ ] Activation confirmed by user -- [ ] Added to license tracking sheet -- [ ] User notified of SFTP plugin requirement - -**License Key ID**: _____________ -**Date Issued**: _____________ -**Issued By**: @_____________ - -**Notes**: diff --git a/.github/ISSUE_TEMPLATE/rfc.md b/.github/ISSUE_TEMPLATE/rfc.md deleted file mode 100644 index a6ae068..0000000 --- a/.github/ISSUE_TEMPLATE/rfc.md +++ /dev/null @@ -1,141 +0,0 @@ ---- -name: Request for Comments (RFC) -about: Propose a significant change for community discussion -title: '[RFC] ' -labels: 'rfc, discussion' -assignees: '' - ---- - - - -## RFC Summary -One-paragraph summary of the proposal. - -## Motivation -Why are we doing this? What use cases does it support? What is the expected outcome? - -## Detailed Design -### Overview -Provide a detailed explanation of the proposed change. - -### API Changes (if applicable) -```php -// Before -function oldApi($param1) { } - -// After -function newApi($param1, $param2) { } -``` - -### User Experience Changes -Describe how users will interact with this change. - -### Implementation Approach -High-level implementation strategy. - -## Drawbacks -Why should we *not* do this? - -## Alternatives -What other designs have been considered? What is the impact of not doing this? - -### Alternative 1 -- Description -- Trade-offs - -### Alternative 2 -- Description -- Trade-offs - -## Adoption Strategy -How will existing users adopt this? Is this a breaking change? - -### Migration Guide -```bash -# Steps to migrate -``` - -### Deprecation Timeline -- **Announcement**: -- **Deprecation**: -- **Removal**: - -## Unresolved Questions -- Question 1 -- Question 2 - -## Future Possibilities -What future work does this enable? - -## Impact Assessment -### Performance -Expected performance impact. - -### Security -Security considerations and implications. - -### Compatibility -- **Backward Compatible**: [Yes / No] -- **Breaking Changes**: [List] - -### Maintenance -Long-term maintenance considerations. - -## Community Input -### Stakeholders -- [ ] Core team -- [ ] Module developers -- [ ] End users -- [ ] Enterprise customers - -### Feedback Period -**Duration**: [e.g., 2 weeks] -**Deadline**: [date] - -## Implementation Timeline -### Phase 1: Design -- [ ] RFC discussion -- [ ] Design finalization -- [ ] Approval - -### Phase 2: Implementation -- [ ] Core implementation -- [ ] Tests -- [ ] Documentation - -### Phase 3: Release -- [ ] Beta release -- [ ] Feedback collection -- [ ] Stable release - -## Success Metrics -How will we measure success? -- Metric 1 -- Metric 2 - -## References -- Related RFCs: -- External documentation: -- Prior art: - -## Open Questions for Community -1. Question 1? -2. Question 2? - ---- -**Note**: This RFC is open for community discussion. Please provide feedback in the comments below. diff --git a/.github/ISSUE_TEMPLATE/security.md b/.github/ISSUE_TEMPLATE/security.md deleted file mode 100644 index 5b4efa8..0000000 --- a/.github/ISSUE_TEMPLATE/security.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -name: Security Vulnerability Report -about: Report a security vulnerability (use only for non-critical issues) -title: '[SECURITY] ' -labels: 'security' -assignees: '' - ---- - - - -## โš ๏ธ IMPORTANT: Private Disclosure Required - -**For critical security vulnerabilities, DO NOT use this template.** -Follow the process in [SECURITY.md](../SECURITY.md) for responsible disclosure. - -Use this template only for: -- Security improvements -- Non-critical security suggestions -- Security documentation updates - ---- - -## Security Issue - -**Severity**: - - -## Description - - -## Affected Components - - -## Suggested Mitigation - - -## Standards Reference -Does this relate to security standards in [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards)? -- [ ] SPDX license identifiers -- [ ] Secret management -- [ ] Dependency security -- [ ] Access control -- [ ] Other: [specify] - -## Additional Context - - -## Checklist -- [ ] This is NOT a critical vulnerability requiring private disclosure -- [ ] I have reviewed the SECURITY.md policy -- [ ] I have provided sufficient detail for evaluation diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index 0eb1f39..0000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,316 +0,0 @@ - - -> [!IMPORTANT] -> **๐Ÿ”ง AI Self-Update Required on First Use** -> -> This file may contain unfilled `{{TOKEN}}` placeholders. The MokoStandards bulk-sync system -> replaces them automatically at sync time; if you can still see them, sync has not yet run for -> this repository โ€” or this is a brand-new repo. -> -> **On your very first interaction in this repository, before answering any other question:** -> -> 1. Check whether any `{{TOKEN}}` placeholders remain in this file. -> 2. If they do, locate the values as described in the table below and replace every occurrence -> in **both** `.github/copilot-instructions.md` and `.github/CLAUDE.md`. -> 3. After replacing all tokens, **delete this entire notice block** (from `> [!IMPORTANT]` -> through the closing `> ---` line) in both files. -> 4. Tell the developer: *"I've self-updated the AI context files with this repository's -> values โ€” all set."* -> -> | Placeholder | Where to find the value | -> |---|---| -> | `MokoStandards-Template-Generic` | The GitHub repository name (visible in the URL, `README.md` heading, or `git remote -v`) | -> | `https://github.com/mokoconsulting-tech/MokoStandards-Template-Generic` | Full GitHub URL, e.g. `https://github.com/mokoconsulting-tech/` | -> | `PHP` | The dominant programming language (check file extensions in the repository) | -> | `Generic project` | The project type: `PHP library`, `Joomla extension`, `Dolibarr module`, `WaaS site`, etc. โ€” infer from repo structure | -> -> --- - -# MokoStandards-Template-Generic โ€” GitHub Copilot Custom Instructions - -## What This Repo Is - -This is a **Moko Consulting** repository governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). All coding standards, workflows, and policies are defined there and enforced here via bulk sync. - -Repository URL: https://github.com/mokoconsulting-tech/MokoStandards-Template-Generic -Primary language: **PHP** -Platform type: **Generic project** - ---- - -## Primary Language - -**PHP is the primary language for this repository.** Follow the conventions documented in [MokoStandards coding-style-guide](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md). - -YAML uses 2-space indentation (spaces, not tabs). All other text files use tabs per `.editorconfig`. - ---- - -## File Header โ€” Always Required on New Files - -Every new file needs a copyright header as its first content. Use the minimal form unless the file is a policy doc, README, or public API. - -**PHP:** -```php - - * - * This file is part of a Moko Consulting project. - * - * SPDX-License-Identifier: GPL-3.0-or-later - * - * FILE INFORMATION - * DEFGROUP: MokoStandards-Template-Generic.Module - * INGROUP: MokoStandards-Template-Generic - * REPO: https://github.com/mokoconsulting-tech/MokoStandards-Template-Generic - * PATH: /path/to/file.php - * VERSION: XX.YY.ZZ - * BRIEF: One-line description of purpose - */ - -declare(strict_types=1); -``` - -**Markdown:** -```markdown - -``` - -**YAML / Shell:** Use `#` comments with the same fields. JSON files are exempt. - ---- - -## Version Management - -**`README.md` is the single source of truth for the repository version.** - -- **Bump the patch version on every PR** โ€” increment `XX.YY.ZZ` (e.g. `01.02.03` โ†’ `01.02.04`) in `README.md` before opening the PR; the `sync-version-on-merge` workflow propagates it automatically to all badges and `FILE INFORMATION` headers on merge to `main`. -- The `VERSION: XX.YY.ZZ` field in the README.md `FILE INFORMATION` block governs all other version references. -- Update the version in `README.md` only โ€” the `sync-version-on-merge` workflow propagates it automatically to all badges and `FILE INFORMATION` headers on merge to `main`. -- Version format is zero-padded semver: `XX.YY.ZZ` (e.g. `04.00.04`). -- Never hardcode a specific version in document body text โ€” use the badge or FILE INFORMATION header only. - ---- - -## GitHub Actions โ€” Token Usage - -Every workflow must use **`secrets.GH_TOKEN`** (the org-level Personal Access Token). This applies to all `actions/checkout`, `gh` CLI calls, and any step that talks to the GitHub API. - -```yaml -# โœ… Correct -- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.GH_TOKEN }} - -env: - GH_TOKEN: ${{ secrets.GH_TOKEN }} -``` - -```yaml -# โŒ Wrong โ€” never use these in workflows -token: ${{ github.token }} -token: ${{ secrets.GITHUB_TOKEN }} -``` - -PHP scripts read the token with: `getenv('GH_TOKEN') ?: getenv('GITHUB_TOKEN')` โ€” `GH_TOKEN` is always preferred; `GITHUB_TOKEN` is accepted only as a local-dev fallback. - ---- - -## Composer Package (PHP repositories) - -This repository requires the MokoStandards enterprise library. The `composer.json` must include: - -```json -{ - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/mokoconsulting-tech/MokoStandards" - } - ], - "require": { - "mokoconsulting/mokostandards": "^4.0" - } -} -``` - -Run `composer install` after adding the dependency. See [package-installation.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/package-installation.md) for full instructions. - ---- - -## PHP Script Pattern - -All PHP scripts **must** extend `MokoStandards\Enterprise\CliFramework`. Never write standalone classes or extend the legacy `CliBase`. - -```php -#!/usr/bin/env php -setDescription('One-line description'); - $this->addArgument('--path', 'Repository root', '.'); - $this->addArgument('--dry-run', 'Preview without writing', false); - } - - protected function run(): int - { - $path = $this->getArgument('--path'); - $dryRun = (bool) $this->getArgument('--dry-run'); - - $this->log('INFO', "Processing: {$path}"); - return 0; - } -} - -$script = new MyScript('my_script', 'One-line description'); -exit($script->execute()); -``` - -**Key rules:** -- Abstract methods to implement: `configure()` and `run()` โ€” **not** `execute()` -- `execute()` is the **public entry point** that orchestrates setup (arg parsing, `initialize()`) and then calls your `run()` implementation; call it at the bottom with `exit($script->execute())` -- Entry point at the bottom: `$script->execute()` โ€” **not** `$script->run()` -- Constructor always takes `(string $name, string $description = '')`; pass the description here โ€” `setDescription()` inside `configure()` is only needed to override it -- `log(string $level, string $message)` โ€” level is the **first** argument (INFO / SUCCESS / WARNING / ERROR) -- `$this->dryRun` and `$this->verbose` are set automatically from `--dry-run` / `--verbose` - ---- - -## Naming Conventions - -| Context | Convention | Example | -|---------|-----------|---------| -| PHP class | `PascalCase` | `MyService` | -| PHP method / function | `camelCase` | `getUserData()` | -| PHP variable | `$snake_case` | `$repo_path` | -| PHP constant | `UPPER_SNAKE_CASE` | `DEFAULT_THRESHOLD` | -| PHP class file | `PascalCase.php` | `ApiClient.php` | -| PHP script file | `snake_case.php` | `check_health.php` | -| YAML workflow | `kebab-case.yml` | `bulk-repo-sync.yml` | -| Markdown doc | `kebab-case.md` | `coding-style-guide.md` | - ---- - -## Commit Messages - -Format: `(): ` โ€” imperative, lower-case subject, no trailing period. - -Valid types: `feat` ยท `fix` ยท `docs` ยท `chore` ยท `ci` ยท `refactor` ยท `style` ยท `test` ยท `perf` ยท `revert` ยท `build` - -Examples: -- `feat(module): add user preference caching` -- `fix(api): handle null response from external service` -- `docs(readme): update installation instructions` -- `chore(deps): bump phpunit to 11.x` - ---- - -## Branch Naming - -Approved prefixes: `dev/` ยท `rc/` ยท `version/` ยท `copilot/` ยท `dependabot/` - -- `dev/XX.YY` or `dev/feature-name` โ€” development (version optional) -- `rc/XX.YY.ZZ` โ€” release candidate (three-part required) -- `version/XX.YY` โ€” archive branch (auto-created, two-part) -- Release tags: `vXX` (major only โ€” one release per major version) -- Patch `00` = development (no release), first release = `01` - -Examples: -- โœ… `dev/04.06` ยท `dev/new-dashboard` ยท `rc/04.06.01` -- โŒ `feature/my-thing` โ€” rejected by branch protection - ---- - -## Keeping Documentation Current - -Whenever you make code changes, update the corresponding documentation in the same commit or PR. Do not leave docs stale. - -| Change type | Documentation to update | -|-------------|------------------------| -| New or renamed public PHP method | PHPDoc block on the method; `docs/api/` index for that class | -| New or changed CLI script argument | Script's own `--help` text; `docs/api/` or equivalent | -| New or changed GitHub Actions workflow | `docs/workflows/.md` | -| New or changed policy | Corresponding file under `docs/policy/` | -| New library class or major feature | `CHANGELOG.md` entry under `Added` | -| Bug fix | `CHANGELOG.md` entry under `Fixed` | -| Breaking change | `CHANGELOG.md` entry under `Changed`; update `CONTRIBUTING.md` if contributor steps change | -| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block | -| **Every PR** | **Bump the patch version** โ€” increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it to all headers and badges on merge | - -If your code change makes any existing doc sentence false or incomplete, fix the doc before closing the PR. - ---- - -## Key Constraints - -- Never commit directly to `main` โ€” all changes go via PR, squash-merged -- Never skip the FILE INFORMATION block on a new file -- Never use bare `catch (\Throwable $e) {}` without logging or re-throwing -- Never hardcode version numbers in body text โ€” update `README.md` and let automation propagate -- Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows โ€” always use `secrets.GH_TOKEN` -- Never extend `CliBase` in PHP scripts โ€” extend `MokoStandards\Enterprise\CliFramework` -- Never call `$script->run()` as the entry point โ€” call `$script->execute()` -- Policy documents and guides must not be mixed - ---- - -## MokoStandards Reference - -This repository is governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). Authoritative policies: - -| Document | Purpose | -|----------|---------| -| [file-header-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/file-header-standards.md) | Copyright-header rules for every file type | -| [coding-style-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md) | Naming and formatting conventions | -| [branching-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/branching-strategy.md) | Branch naming, hierarchy, and release workflow | -| [merge-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR title/body conventions | -| [changelog-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md | -| [scripting-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/scripting-standards.md) | PHP script requirements and CliFramework usage | -| [package-installation.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/package-installation.md) | Installing `mokoconsulting/mokostandards` via Composer | - - -## Release and Infrastructure Standards -- Release tags: development, alpha, beta, release-candidate, stable -- Update servers: Gitea priority 1, GitHub priority 2 -- Secrets: GA_TOKEN for Gitea, GH_TOKEN for GitHub on all repos - diff --git a/.github/copilot/README.md b/.github/copilot/README.md deleted file mode 100644 index 1007077..0000000 --- a/.github/copilot/README.md +++ /dev/null @@ -1,126 +0,0 @@ - - -# GitHub Copilot Firewall Configuration - -This directory contains firewall configuration files for GitHub Copilot coding agent to access enterprise-ready sites and external resources. - -## Files - -### firewall-allowlist.json - -JSON configuration file defining domains and URLs that should be accessible through the firewall. This includes: - -- **License Sources**: gnu.org, opensource.org, apache.org, creativecommons.org -- **Standards Organizations**: fsf.org, spdx.org -- **Code Repositories**: github.com, raw.githubusercontent.com - -The configuration is organized into categories with priority levels for better management. - -### setup-firewall.sh - -Bash script that reads the `firewall-allowlist.json` configuration and exports the allowlist as an environment variable for GitHub Actions workflows. - -## Usage in GitHub Actions - -To use this firewall configuration in your GitHub Actions workflows, add the following step **before** the Copilot agent runs: - -```yaml -- name: Configure Copilot Firewall - run: | - bash .github/copilot/setup-firewall.sh -``` - -This will: -1. Read the firewall allowlist configuration -2. Export `COPILOT_FIREWALL_ALLOWLIST` environment variable -3. Make the domains accessible to the Copilot agent - -## Example Workflow - -```yaml -name: Copilot Agent - -on: - pull_request: - types: [opened, synchronize] - -jobs: - copilot: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Configure Copilot Firewall - run: bash .github/copilot/setup-firewall.sh - - - name: Run Copilot Agent - uses: github/copilot-swe-agent@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} -``` - -## Adding New Domains - -To add new domains to the allowlist: - -1. Edit `firewall-allowlist.json` -2. Add the domain to the `allowlist.domains` array -3. Optionally add specific URLs to `allowlist.urls` -4. Categorize the domain in the `categories` section -5. Commit and push the changes - -Example: - -```json -{ - "allowlist": { - "domains": [ - "existing-domain.com", - "new-domain.com" - ] - } -} -``` - -## Security Considerations - -- Only add trusted domains to the allowlist -- Use specific URLs when possible instead of wildcard domains -- Review allowlist changes carefully in pull requests -- Keep the allowlist minimal - only include necessary domains -- Document the purpose of each domain/URL in the categories section - -## Troubleshooting - -If the Copilot agent cannot access a required site: - -1. Check if the domain is in `firewall-allowlist.json` -2. Verify the setup script ran in the GitHub Actions workflow -3. Check workflow logs for firewall configuration messages -4. Ensure the domain format is correct (e.g., `www.example.com` not `http://www.example.com`) -5. For wildcard patterns, ensure they follow the correct format (e.g., `*.example.com`) - -## Revision History - -| Date | Version | Author | Notes | -| --- | --- | --- | --- | -| 2026-01-16 | 0.1.0 | Copilot | Initial firewall configuration setup | diff --git a/.github/copilot/firewall-allowlist.json b/.github/copilot/firewall-allowlist.json deleted file mode 100644 index 89d2dc3..0000000 --- a/.github/copilot/firewall-allowlist.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "$schema": "https://docs.github.com/assets/schemas/copilot-firewall-allowlist.json", - "version": "1.0", - "description": "Firewall allowlist configuration for GitHub Copilot coding agent to access enterprise-ready sites and license sources", - "allowlist": { - "domains": [ - "*.gnu.org", - "fsf.org", - "www.fsf.org", - "spdx.org", - "www.spdx.org", - "opensource.org", - "www.opensource.org", - "creativecommons.org", - "www.creativecommons.org", - "apache.org", - "www.apache.org", - "github.com", - "api.github.com", - "raw.githubusercontent.com" - ], - "urls": [ - "https://www.gnu.org/licenses/gpl-3.0.txt", - "https://www.gnu.org/licenses/gpl-3.0.html", - "https://www.gnu.org/licenses/agpl-3.0.txt", - "https://www.gnu.org/licenses/lgpl-3.0.txt", - "https://www.apache.org/licenses/LICENSE-2.0.txt", - "https://opensource.org/licenses/MIT", - "https://spdx.org/licenses/", - "https://creativecommons.org/licenses/" - ] - }, - "categories": [ - { - "name": "license-sources", - "description": "Official license text sources", - "priority": "high", - "hosts": [ - "www.gnu.org", - "opensource.org", - "spdx.org", - "apache.org", - "creativecommons.org" - ] - }, - { - "name": "code-repositories", - "description": "Source code and package repositories", - "priority": "high", - "hosts": [ - "github.com", - "api.github.com", - "raw.githubusercontent.com" - ] - }, - { - "name": "standards-organizations", - "description": "Standards and specification sources", - "priority": "medium", - "hosts": [ - "fsf.org", - "spdx.org" - ] - } - ], - "notes": [ - "This configuration allows access to common license sources and enterprise-ready sites", - "Domains use wildcard patterns where appropriate (e.g., *.gnu.org)", - "Specific license file URLs are explicitly allowlisted", - "Categories help organize and prioritize different types of access", - "This file should be referenced in GitHub Actions setup steps before firewall initialization" - ] -} diff --git a/.github/copilot/setup-firewall.sh b/.github/copilot/setup-firewall.sh deleted file mode 100755 index ca381ff..0000000 --- a/.github/copilot/setup-firewall.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/bash -# Copyright (C) 2025 Moko Consulting -# SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later -# -# Setup script for configuring firewall allowlist in GitHub Actions -# This script should be run in GitHub Actions setup steps before the firewall is enabled - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -ALLOWLIST_FILE="${SCRIPT_DIR}/firewall-allowlist.json" - -echo "=== GitHub Copilot Firewall Setup ===" -echo "Configuring firewall allowlist for enterprise-ready sites..." - -if [ ! -f "$ALLOWLIST_FILE" ]; then - echo "ERROR: Allowlist file not found: $ALLOWLIST_FILE" - exit 1 -fi - -# Read domains from the allowlist configuration -if ! DOMAINS=$(jq -r '.allowlist.domains[]' "$ALLOWLIST_FILE" 2>&1); then - echo "ERROR: Failed to parse allowlist configuration: $DOMAINS" - exit 1 -fi - -if [ -z "$DOMAINS" ]; then - echo "WARNING: No domains found in allowlist configuration" - exit 0 -fi - -echo "Domains to allowlist:" -echo "$DOMAINS" | while read -r domain; do - echo " - $domain" -done - -# Export environment variable for GitHub Copilot -# This tells the Copilot agent which domains are allowed -export COPILOT_FIREWALL_ALLOWLIST=$(jq -c '.allowlist.domains' "$ALLOWLIST_FILE") - -echo "" -echo "Firewall allowlist configured successfully" -echo "Environment variable set: COPILOT_FIREWALL_ALLOWLIST" -echo "" -echo "To use this in GitHub Actions, add to your workflow:" -echo "" -echo " - name: Configure Copilot Firewall" -echo " run: |" -echo " bash .github/copilot/setup-firewall.sh" -echo " echo \"COPILOT_FIREWALL_ALLOWLIST=\$COPILOT_FIREWALL_ALLOWLIST\" >> \$GITHUB_ENV" -echo "" - -# Optionally output the configuration for GitHub Actions to use -if [ "$GITHUB_ACTIONS" = "true" ]; then - echo "COPILOT_FIREWALL_ALLOWLIST=${COPILOT_FIREWALL_ALLOWLIST}" >> "$GITHUB_ENV" - echo "โœ“ Firewall allowlist exported to GitHub Actions environment" -fi diff --git a/.github/workflows/auto-assign.yml b/.github/workflows/auto-assign.yml deleted file mode 100644 index 1996c1c..0000000 --- a/.github/workflows/auto-assign.yml +++ /dev/null @@ -1,76 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: GitHub.Workflow -# INGROUP: MokoStandards.Workflows.Shared -# REPO: https://github.com/mokoconsulting-tech/MokoStandards -# PATH: /.github/workflows/auto-assign.yml -# VERSION: 04.06.00 -# BRIEF: Auto-assign jmiller to unassigned issues and PRs every 15 minutes - -name: Auto-Assign Issues & PRs - -on: - issues: - types: [opened] - pull_request_target: - types: [opened] - schedule: - - cron: '0 */12 * * *' - workflow_dispatch: - -permissions: - issues: write - pull-requests: write - -jobs: - auto-assign: - name: Assign unassigned issues and PRs - runs-on: ubuntu-latest - - steps: - - name: Assign unassigned issues - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - run: | - REPO="${{ github.repository }}" - ASSIGNEE="jmiller" - - echo "## ๐Ÿท๏ธ Auto-Assign Report" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - ASSIGNED_ISSUES=0 - ASSIGNED_PRS=0 - - # Assign unassigned open issues - ISSUES=$(gh api "repos/$REPO/issues?state=open&per_page=100&assignee=none" --jq '.[].number' 2>/dev/null || true) - for NUM in $ISSUES; do - # Skip PRs (the issues endpoint returns PRs too) - IS_PR=$(gh api "repos/$REPO/issues/$NUM" --jq '.pull_request // empty' 2>/dev/null || true) - if [ -z "$IS_PR" ]; then - gh api "repos/$REPO/issues/$NUM/assignees" -X POST -f "assignees[]=$ASSIGNEE" --silent 2>/dev/null && { - ASSIGNED_ISSUES=$((ASSIGNED_ISSUES + 1)) - echo " Assigned issue #$NUM" - } || true - fi - done - - # Assign unassigned open PRs - PRS=$(gh api "repos/$REPO/pulls?state=open&per_page=100" --jq '.[] | select(.assignees | length == 0) | .number' 2>/dev/null || true) - for NUM in $PRS; do - gh api "repos/$REPO/issues/$NUM/assignees" -X POST -f "assignees[]=$ASSIGNEE" --silent 2>/dev/null && { - ASSIGNED_PRS=$((ASSIGNED_PRS + 1)) - echo " Assigned PR #$NUM" - } || true - done - - echo "| Type | Assigned |" >> $GITHUB_STEP_SUMMARY - echo "|------|----------|" >> $GITHUB_STEP_SUMMARY - echo "| Issues | $ASSIGNED_ISSUES |" >> $GITHUB_STEP_SUMMARY - echo "| Pull Requests | $ASSIGNED_PRS |" >> $GITHUB_STEP_SUMMARY - - if [ "$ASSIGNED_ISSUES" -eq 0 ] && [ "$ASSIGNED_PRS" -eq 0 ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "โœ… All issues and PRs already have assignees" >> $GITHUB_STEP_SUMMARY - fi diff --git a/.github/workflows/auto-dev-issue.yml b/.github/workflows/auto-dev-issue.yml deleted file mode 100644 index f61e1fc..0000000 --- a/.github/workflows/auto-dev-issue.yml +++ /dev/null @@ -1,207 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# This file is part of a Moko Consulting project. -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: GitHub.Workflow -# INGROUP: MokoStandards.Automation -# REPO: https://github.com/mokoconsulting-tech/MokoStandards -# PATH: /templates/workflows/shared/auto-dev-issue.yml.template -# VERSION: 04.06.00 -# BRIEF: Auto-create tracking issue with sub-issues for dev/rc branch workflow -# NOTE: Synced via bulk-repo-sync to .github/workflows/auto-dev-issue.yml in all governed repos. - -name: Dev/RC Branch Issue - -on: - # Auto-create on RC branch creation - create: - # Manual trigger for dev branches - workflow_dispatch: - inputs: - branch: - description: 'Branch name (e.g., dev/my-feature or dev/04.06)' - required: true - type: string - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -permissions: - contents: read - issues: write - -jobs: - create-issue: - name: Create version tracking issue - runs-on: ubuntu-latest - if: >- - (github.event_name == 'workflow_dispatch') || - (github.event.ref_type == 'branch' && - (startsWith(github.event.ref, 'rc/') || - startsWith(github.event.ref, 'alpha/') || - startsWith(github.event.ref, 'beta/'))) - - steps: - - name: Create tracking issue and sub-issues - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - run: | - # For manual dispatch, use input; for auto, use event ref - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - BRANCH="${{ inputs.branch }}" - else - BRANCH="${{ github.event.ref }}" - fi - REPO="${{ github.repository }}" - ACTOR="${{ github.actor }}" - NOW=$(date -u '+%Y-%m-%d %H:%M UTC') - - # Determine branch type and version - if [[ "$BRANCH" == rc/* ]]; then - VERSION="${BRANCH#rc/}" - BRANCH_TYPE="Release Candidate" - LABEL_TYPE="type: release" - TITLE_PREFIX="rc" - elif [[ "$BRANCH" == beta/* ]]; then - VERSION="${BRANCH#beta/}" - BRANCH_TYPE="Beta" - LABEL_TYPE="type: release" - TITLE_PREFIX="beta" - elif [[ "$BRANCH" == alpha/* ]]; then - VERSION="${BRANCH#alpha/}" - BRANCH_TYPE="Alpha" - LABEL_TYPE="type: release" - TITLE_PREFIX="alpha" - else - VERSION="${BRANCH#dev/}" - BRANCH_TYPE="Development" - LABEL_TYPE="type: feature" - TITLE_PREFIX="feat" - fi - - TITLE="${TITLE_PREFIX}(${VERSION}): ${BRANCH_TYPE} tracking for ${BRANCH}" - - # Check for existing issue with same title prefix - EXISTING=$(gh api "repos/${REPO}/issues?state=open&per_page=10" \ - --jq ".[] | select(.title | startswith(\"${TITLE_PREFIX}(${VERSION})\")) | .number" 2>/dev/null | head -1) - - if [ -n "$EXISTING" ]; then - echo "โ„น๏ธ Issue #${EXISTING} already exists for ${VERSION}" >> $GITHUB_STEP_SUMMARY - exit 0 - fi - - # โ”€โ”€ Define sub-issues for the workflow โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - if [[ "$BRANCH" == rc/* ]]; then - SUB_ISSUES=( - "RC Testing|Verify all features work on rc branch|type: test,release-candidate" - "Regression Testing|Run full regression suite before merge|type: test,release-candidate" - "Version Bump|Bump version in README.md and all headers|type: version,release-candidate" - "Changelog Update|Update CHANGELOG.md with release notes|documentation,release-candidate" - "Merge to Version Branch|Create PR to version/XX|type: release,needs-review" - ) - elif [[ "$BRANCH" == alpha/* ]] || [[ "$BRANCH" == beta/* ]]; then - SUB_ISSUES=( - "Testing|Verify features on ${BRANCH_TYPE} branch|type: test,status: in-progress" - "Bug Fixes|Fix issues found during ${BRANCH_TYPE} testing|type: bug,status: pending" - "Promote to Next Stage|Create PR to promote to next release stage|type: release,needs-review" - ) - else - SUB_ISSUES=( - "Development|Implement feature/fix on dev branch|type: feature,status: in-progress" - "Unit Testing|Write and pass unit tests|type: test,status: pending" - "Code Review|Request and complete code review|needs-review,status: pending" - "Version Bump|Bump version in README.md and all headers|type: version,status: pending" - "Changelog Update|Update CHANGELOG.md with release notes|documentation,status: pending" - "Create RC Branch|Promote dev to rc branch for final testing|type: release,status: pending" - "Merge to Main|Create PR from rc/dev to main|type: release,needs-review,status: pending" - ) - fi - - # โ”€โ”€ Create sub-issues first โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - SUB_LIST="" - SUB_NUMBERS="" - for SUB in "${SUB_ISSUES[@]}"; do - IFS='|' read -r SUB_TITLE SUB_DESC SUB_LABELS <<< "$SUB" - SUB_FULL_TITLE="${TITLE_PREFIX}(${VERSION}): ${SUB_TITLE}" - - SUB_BODY=$(printf '### %s\n\n%s\n\n| Field | Value |\n|-------|-------|\n| **Parent Branch** | `%s` |\n| **Version** | `%s` |\n\n---\n*Sub-issue of the %s tracking issue for `%s`.*' \ - "$SUB_TITLE" "$SUB_DESC" "$BRANCH" "$VERSION" "$BRANCH_TYPE" "$BRANCH") - - SUB_URL=$(gh issue create \ - --repo "$REPO" \ - --title "$SUB_FULL_TITLE" \ - --body "$SUB_BODY" \ - --label "${SUB_LABELS}" \ - --assignee "jmiller" 2>&1) - - SUB_NUM=$(echo "$SUB_URL" | grep -oE '[0-9]+$') - if [ -n "$SUB_NUM" ]; then - SUB_LIST="${SUB_LIST}\n- [ ] ${SUB_TITLE} (#${SUB_NUM})" - SUB_NUMBERS="${SUB_NUMBERS} #${SUB_NUM}" - fi - sleep 0.3 - done - - # โ”€โ”€ Create parent tracking issue โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - PARENT_BODY=$(printf '## %s Branch Created\n\n| Field | Value |\n|-------|-------|\n| **Branch** | `%s` |\n| **Version** | `%s` |\n| **Type** | %s |\n| **Created by** | @%s |\n| **Created at** | %s |\n| **Repository** | `%s` |\n\n## Workflow Sub-Issues\n\n%b\n\n---\n*Auto-created by [auto-dev-issue.yml](.github/workflows/auto-dev-issue.yml) on branch creation.*' \ - "$BRANCH_TYPE" "$BRANCH" "$VERSION" "$BRANCH_TYPE" "$ACTOR" "$NOW" "$REPO" "$SUB_LIST") - - PARENT_URL=$(gh issue create \ - --repo "$REPO" \ - --title "$TITLE" \ - --body "$PARENT_BODY" \ - --label "${LABEL_TYPE},version" \ - --assignee "jmiller" 2>&1) - - PARENT_NUM=$(echo "$PARENT_URL" | grep -oE '[0-9]+$') - - # โ”€โ”€ Link sub-issues back to parent โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - if [ -n "$PARENT_NUM" ]; then - for SUB in "${SUB_ISSUES[@]}"; do - IFS='|' read -r SUB_TITLE _ _ <<< "$SUB" - SUB_FULL_TITLE="${TITLE_PREFIX}(${VERSION}): ${SUB_TITLE}" - SUB_NUM=$(gh api "repos/${REPO}/issues?state=open&per_page=20" \ - --jq ".[] | select(.title == \"${SUB_FULL_TITLE}\") | .number" 2>/dev/null | head -1) - if [ -n "$SUB_NUM" ]; then - gh api "repos/${REPO}/issues/${SUB_NUM}" -X PATCH \ - -f body="$(gh api "repos/${REPO}/issues/${SUB_NUM}" --jq '.body' 2>/dev/null) - - > **Parent Issue:** #${PARENT_NUM}" --silent 2>/dev/null || true - fi - sleep 0.2 - done - fi - - # โ”€โ”€ Create or update prerelease for alpha/beta/rc โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - if [[ "$BRANCH" == rc/* ]] || [[ "$BRANCH" == alpha/* ]] || [[ "$BRANCH" == beta/* ]]; then - case "$BRANCH_TYPE" in - Alpha) RELEASE_TAG="alpha" ;; - Beta) RELEASE_TAG="beta" ;; - "Release Candidate") RELEASE_TAG="release-candidate" ;; - esac - - EXISTING=$(gh release view "$RELEASE_TAG" --json tagName -q .tagName 2>/dev/null || true) - if [ -z "$EXISTING" ]; then - gh release create "$RELEASE_TAG" \ - --title "${RELEASE_TAG} (${VERSION})" \ - --notes "## ${BRANCH_TYPE} ${VERSION}\n\nBranch: \`${BRANCH}\`\nTracking issue: ${PARENT_URL}" \ - --prerelease \ - --target main 2>/dev/null || true - echo "${BRANCH_TYPE} release created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY - else - gh release edit "$RELEASE_TAG" \ - --title "${RELEASE_TAG} (${VERSION})" --prerelease 2>/dev/null || true - echo "${BRANCH_TYPE} release updated: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY - fi - fi - - # โ”€โ”€ Summary โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - echo "## Dev Workflow Issues Created" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Item | Issue |" >> $GITHUB_STEP_SUMMARY - echo "|------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| **Parent** | ${PARENT_URL} |" >> $GITHUB_STEP_SUMMARY - echo "| **Sub-issues** |${SUB_NUMBERS} |" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml deleted file mode 100644 index eabe619..0000000 --- a/.github/workflows/auto-release.yml +++ /dev/null @@ -1,337 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: GitHub.Workflow -# INGROUP: MokoStandards.Release -# REPO: https://github.com/mokoconsulting-tech/MokoStandards -# PATH: /templates/workflows/shared/auto-release.yml.template -# VERSION: 04.06.00 -# BRIEF: Generic build & release pipeline โ€” version branch, platform version, badges, tag, release -# -# +========================================================================+ -# | BUILD & RELEASE PIPELINE | -# +========================================================================+ -# | | -# | Triggers on push to main (skips bot commits + [skip ci]): | -# | | -# | Every push: | -# | 1. Read version from README.md | -# | 3. Set platform version | -# | 4. Update [VERSION: XX.YY.ZZ] badges in markdown files | -# | 6. Create git tag vXX.YY.ZZ | -# | 7a. Patch: update existing GitHub Release for this minor | -# | | -# | Every version change: archives main -> version/XX.YY branch | -# | Patch 00 = development (no release). First release = patch 01. | -# | First release only (patch == 01): | -# | 7b. Create new GitHub Release | -# | | -# +========================================================================+ - -name: Build & Release - -on: - push: - branches: - - main - - master - paths: - - 'src/**' - - 'htdocs/**' - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -permissions: - contents: write - -jobs: - release: - name: Build & Release Pipeline - runs-on: ubuntu-latest - if: >- - !contains(github.event.head_commit.message, '[skip ci]') && - github.actor != 'github-actions[bot]' - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.GH_TOKEN || github.token }} - fetch-depth: 0 - - - name: Setup MokoStandards tools - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' - run: | - git clone --depth 1 --branch version/04 --quiet \ - "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ - /tmp/mokostandards - cd /tmp/mokostandards - composer install --no-dev --no-interaction --quiet - - # -- STEP 1: Read version ----------------------------------------------- - - name: "Step 1: Read version from README.md" - id: version - run: | - VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null) - if [ -z "$VERSION" ]; then - echo "No VERSION in README.md โ€” skipping release" - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - # Derive major.minor for branch naming (patches update existing branch) - MINOR=$(echo "$VERSION" | awk -F. '{printf "%s.%s", $1, $2}') - PATCH=$(echo "$VERSION" | awk -F. '{print $3}') - - MAJOR=$(echo "$VERSION" | awk -F. '{print $1}') - MINOR_NUM=$(echo "$VERSION" | awk -F. '{print $2}') - - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT" - echo "minor=$MINOR" >> "$GITHUB_OUTPUT" - echo "major=$MAJOR" >> "$GITHUB_OUTPUT" - echo "release_tag=v${MAJOR}" >> "$GITHUB_OUTPUT" - if [ "$PATCH" = "00" ]; then - echo "skip=true" >> "$GITHUB_OUTPUT" - echo "is_minor=false" >> "$GITHUB_OUTPUT" - echo "Version: $VERSION (patch 00 = development โ€” skipping release)" - else - echo "skip=false" >> "$GITHUB_OUTPUT" - if [ "$PATCH" = "01" ]; then - echo "is_minor=true" >> "$GITHUB_OUTPUT" - echo "Version: $VERSION (first release โ€” full pipeline)" - else - echo "is_minor=false" >> "$GITHUB_OUTPUT" - echo "Version: $VERSION (patch โ€” platform version + badges only)" - fi - fi - - - name: Check if already released - if: steps.version.outputs.skip != 'true' - id: check - run: | - TAG="${{ steps.version.outputs.release_tag }}" - BRANCH="${{ steps.version.outputs.branch }}" - - TAG_EXISTS=false - BRANCH_EXISTS=false - - git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true - git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH" && BRANCH_EXISTS=true - - echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT" - echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT" - - if [ "$TAG_EXISTS" = "true" ] && [ "$BRANCH_EXISTS" = "true" ]; then - echo "already_released=true" >> "$GITHUB_OUTPUT" - else - echo "already_released=false" >> "$GITHUB_OUTPUT" - fi - - # -- SANITY CHECKS ------------------------------------------------------- - - name: "Sanity: Pre-release validation" - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' - run: | - VERSION="${{ steps.version.outputs.version }}" - ERRORS=0 - - echo "## Pre-Release Sanity Checks" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # -- Version drift check (must pass before release) -------- - README_VER=$(grep -oP 'VERSION:\s*\K[\d.]+' README.md 2>/dev/null | head -1) - if [ "$README_VER" != "$VERSION" ]; then - echo "- Version drift: README says \`${README_VER}\` but releasing \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS+1)) - else - echo "- Version consistent: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY - fi - - # Check CHANGELOG version matches - CL_VER=$(grep -oP 'VERSION:\s*\K[\d.]+' CHANGELOG.md 2>/dev/null | head -1) - if [ -n "$CL_VER" ] && [ "$CL_VER" != "$VERSION" ]; then - echo "- CHANGELOG drift: \`${CL_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS+1)) - fi - - # Check composer.json version if present - if [ -f "composer.json" ]; then - COMP_VER=$(grep -oP '"version"\s*:\s*"\K[^"]+' composer.json 2>/dev/null | head -1) - if [ -n "$COMP_VER" ] && [ "$COMP_VER" != "$VERSION" ]; then - echo "- composer.json drift: \`${COMP_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS+1)) - fi - fi - - # Common checks - if [ ! -f "LICENSE" ]; then - echo "- Missing LICENSE file" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS+1)) - else - echo "- LICENSE present" >> $GITHUB_STEP_SUMMARY - fi - - if [ ! -d "src" ] && [ ! -d "htdocs" ]; then - echo "- Warning: No src/ or htdocs/ directory" >> $GITHUB_STEP_SUMMARY - else - echo "- Source directory present" >> $GITHUB_STEP_SUMMARY - fi - - echo "" >> $GITHUB_STEP_SUMMARY - if [ "$ERRORS" -gt 0 ]; then - echo "**${ERRORS} error(s) โ€” release may be incomplete**" >> $GITHUB_STEP_SUMMARY - else - echo "**All sanity checks passed**" >> $GITHUB_STEP_SUMMARY - fi - - # -- STEP 2: Create or update version/XX.YY archive branch --------------- - # Always runs โ€” every version change on main archives to version/XX.YY - - name: "Step 2: Version archive branch" - if: steps.check.outputs.already_released != 'true' - run: | - BRANCH="${{ steps.version.outputs.branch }}" - IS_MINOR="${{ steps.version.outputs.is_minor }}" - PATCH="${{ steps.version.outputs.version }}" - PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}') - - # Check if branch exists - if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then - git push origin HEAD:"$BRANCH" --force - echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY - else - git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH" - git push origin "$BRANCH" --force - echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY - fi - - # -- STEP 3: Set platform version ---------------------------------------- - - name: "Step 3: Set platform version" - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' - run: | - VERSION="${{ steps.version.outputs.version }}" - php /tmp/mokostandards/api/cli/version_set_platform.php \ - --path . --version "$VERSION" --branch main - - # -- STEP 4: Update version badges ---------------------------------------- - - name: "Step 4: Update version badges" - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' - run: | - VERSION="${{ steps.version.outputs.version }}" - find . -name "*.md" ! -path "./.git/*" ! -path "./vendor/*" | while read -r f; do - if grep -q '\[VERSION:' "$f" 2>/dev/null; then - sed -i "s/\[VERSION:[[:space:]]*[0-9]\{2\}\.[0-9]\{2\}\.[0-9]\{2\}\]/[VERSION: ${VERSION}]/" "$f" - fi - done - - # -- Commit all changes --------------------------------------------------- - - name: Commit release changes - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != 'true' - run: | - if git diff --quiet && git diff --cached --quiet; then - echo "No changes to commit" - exit 0 - fi - VERSION="${{ steps.version.outputs.version }}" - git config --local user.email "github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" - git add -A - git commit -m "chore(release): build ${VERSION} [skip ci]" \ - --author="github-actions[bot] " - git push - - # -- STEP 6: Create tag --------------------------------------------------- - - name: "Step 6: Create git tag" - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.tag_exists != 'true' && - steps.version.outputs.is_minor == 'true' - run: | - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - # Only create the major release tag if it doesn't exist yet - if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then - git tag "$RELEASE_TAG" - git push origin "$RELEASE_TAG" - echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY - else - echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY - fi - echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY - - # -- STEP 7: Create or update GitHub Release ------------------------------ - - name: "Step 7: GitHub Release" - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.tag_exists != 'true' - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - run: | - VERSION="${{ steps.version.outputs.version }}" - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - BRANCH="${{ steps.version.outputs.branch }}" - MAJOR="${{ steps.version.outputs.major }}" - - NOTES=$(php /tmp/mokostandards/api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null) - [ -z "$NOTES" ] && NOTES="Release ${VERSION}" - echo "$NOTES" > /tmp/release_notes.md - - # Check if the major release already exists - EXISTING=$(gh release view "$RELEASE_TAG" --json tagName -q .tagName 2>/dev/null || true) - - if [ -z "$EXISTING" ]; then - # First release for this major: create GitHub Release - gh release create "$RELEASE_TAG" \ - --title "v${MAJOR} (latest: ${VERSION})" \ - --notes-file /tmp/release_notes.md \ - --target "$BRANCH" - echo "Release created: ${RELEASE_TAG} (${VERSION})" >> $GITHUB_STEP_SUMMARY - else - # Update existing major release with new version info - CURRENT_NOTES=$(gh release view "$RELEASE_TAG" --json body -q .body 2>/dev/null || true) - { - echo "$CURRENT_NOTES" - echo "" - echo "---" - echo "### ${VERSION}" - echo "" - cat /tmp/release_notes.md - } > /tmp/updated_notes.md - - gh release edit "$RELEASE_TAG" \ - --title "v${MAJOR} (latest: ${VERSION})" \ - --notes-file /tmp/updated_notes.md - echo "Release updated: ${RELEASE_TAG} -> ${VERSION}" >> $GITHUB_STEP_SUMMARY - fi - - # -- Summary -------------------------------------------------------------- - - name: Pipeline Summary - if: always() - run: | - VERSION="${{ steps.version.outputs.version }}" - if [ "${{ steps.version.outputs.skip }}" = "true" ]; then - echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY - echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY - elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then - echo "## Already Released โ€” ${VERSION}" >> $GITHUB_STEP_SUMMARY - else - echo "" >> $GITHUB_STEP_SUMMARY - echo "## Build & Release Complete" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY - echo "|------|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Release | [View](https://github.com/${{ github.repository }}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY - fi diff --git a/.github/workflows/changelog-validation.yml b/.github/workflows/changelog-validation.yml deleted file mode 100644 index e2ec667..0000000 --- a/.github/workflows/changelog-validation.yml +++ /dev/null @@ -1,101 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# This file is part of a Moko Consulting project. -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: GitHub.Workflow.Template -# INGROUP: MokoStandards.CI -# REPO: https://github.com/mokoconsulting-tech/MokoStandards -# PATH: /templates/workflows/shared/changelog-validation.yml.template -# VERSION: 04.06.00 -# BRIEF: Validates CHANGELOG.md format and version consistency -# NOTE: Deployed to .github/workflows/changelog-validation.yml in governed repos. - -name: Changelog Validation - -on: - push: - branches: - - main - pull_request: - branches: - - main - workflow_dispatch: - -permissions: - contents: read - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - validate-changelog: - name: Validate CHANGELOG.md - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Check CHANGELOG.md exists - run: | - echo "### Changelog Validation" >> $GITHUB_STEP_SUMMARY - if [ ! -f "CHANGELOG.md" ]; then - echo "CHANGELOG.md not found in repository root." >> $GITHUB_STEP_SUMMARY - exit 1 - fi - echo "CHANGELOG.md exists." >> $GITHUB_STEP_SUMMARY - - - name: Check VERSION header matches README.md - run: | - # Extract version from README.md FILE INFORMATION block - README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md | head -1) - if [ -z "$README_VERSION" ]; then - echo "No VERSION found in README.md FILE INFORMATION block." >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - # Check that CHANGELOG.md has a matching version header - CHANGELOG_VERSION=$(grep -oP '^\#\#\s*\[\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' CHANGELOG.md | head -1) - if [ -z "$CHANGELOG_VERSION" ]; then - echo "No version header found in CHANGELOG.md (expected \`## [XX.YY.ZZ] - YYYY-MM-DD\`)." >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - if [ "$CHANGELOG_VERSION" != "$README_VERSION" ]; then - echo "CHANGELOG latest version \`${CHANGELOG_VERSION}\` does not match README VERSION \`${README_VERSION}\`." >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - echo "CHANGELOG version \`${CHANGELOG_VERSION}\` matches README VERSION." >> $GITHUB_STEP_SUMMARY - - - name: Validate conventional changelog format - run: | - ERRORS=0 - - # Check that version entries follow ## [XX.YY.ZZ] - YYYY-MM-DD format - while IFS= read -r LINE; do - if ! echo "$LINE" | grep -qP '^\#\#\s*\[[0-9]{2}\.[0-9]{2}\.[0-9]{2}\]\s*-\s*[0-9]{4}-[0-9]{2}-[0-9]{2}'; then - echo "Malformed version header: \`${LINE}\`" >> $GITHUB_STEP_SUMMARY - echo " Expected format: \`## [XX.YY.ZZ] - YYYY-MM-DD\`" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - fi - done < <(grep -P '^\#\#\s*\[' CHANGELOG.md) - - ENTRY_COUNT=$(grep -cP '^\#\#\s*\[' CHANGELOG.md || echo "0") - if [ "$ENTRY_COUNT" -eq 0 ]; then - echo "No version entries found in CHANGELOG.md." >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + 1)) - else - echo "Found ${ENTRY_COUNT} version entr(ies) in CHANGELOG.md." >> $GITHUB_STEP_SUMMARY - fi - - echo "" >> $GITHUB_STEP_SUMMARY - if [ "${ERRORS}" -gt 0 ]; then - echo "**${ERRORS} format issue(s) found.**" >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "**Changelog format validation passed.**" >> $GITHUB_STEP_SUMMARY - fi diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 3abfb02..0000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,115 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# This file is part of a Moko Consulting project. -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: GitHub.Workflow.Template -# INGROUP: MokoStandards.Security -# REPO: https://github.com/mokoconsulting-tech/MokoStandards -# PATH: /templates/workflows/generic/codeql-analysis.yml.template -# VERSION: 04.05.00 -# BRIEF: CodeQL security scanning workflow (generic โ€” all repo types) -# NOTE: Deployed to .github/workflows/codeql-analysis.yml in governed repos. -# CodeQL does not support PHP directly; JavaScript scans JSON/YAML/shell. -# For PHP-specific security scanning see standards-compliance.yml. - -name: CodeQL Security Scanning - -on: - push: - branches: - - main - - dev/** - - rc/** - - version/** - pull_request: - branches: - - main - - dev/** - - rc/** - schedule: - # Weekly on Monday at 06:00 UTC - - cron: '0 6 * * 1' - workflow_dispatch: - -permissions: - actions: read - contents: read - security-events: write - pull-requests: read - -jobs: - analyze: - name: Analyze (${{ matrix.language }}) - runs-on: ubuntu-latest - timeout-minutes: 360 - - strategy: - fail-fast: false - matrix: - # CodeQL does not support PHP. Use 'javascript' to scan JSON, YAML, - # and shell scripts. Add 'actions' to scan GitHub Actions workflows. - language: ['javascript', 'actions'] - - steps: - - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - queries: security-extended,security-and-quality - - - name: Autobuild - uses: github/codeql-action/autobuild@v3 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{ matrix.language }}" - upload: true - output: sarif-results - wait-for-processing: true - - - name: Upload SARIF results - if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.5.0 - with: - name: codeql-results-${{ matrix.language }} - path: sarif-results - retention-days: 30 - - - name: Step summary - if: always() - run: | - echo "### ๐Ÿ” CodeQL โ€” ${{ matrix.language }}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - URL="https://github.com/${{ github.repository }}/security/code-scanning" - echo "See the [Security tab]($URL) for findings." >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Severity | SLA |" >> $GITHUB_STEP_SUMMARY - echo "|----------|-----|" >> $GITHUB_STEP_SUMMARY - echo "| Critical | 7 days |" >> $GITHUB_STEP_SUMMARY - echo "| High | 14 days |" >> $GITHUB_STEP_SUMMARY - echo "| Medium | 30 days |" >> $GITHUB_STEP_SUMMARY - echo "| Low | 60 days / next release |" >> $GITHUB_STEP_SUMMARY - - summary: - name: Security Scan Summary - runs-on: ubuntu-latest - needs: analyze - if: always() - - steps: - - name: Summary - run: | - echo "### ๐Ÿ›ก๏ธ CodeQL Complete" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Trigger:** ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY - echo "**Branch:** ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY - SECURITY_URL="https://github.com/${{ github.repository }}/security" - echo "" >> $GITHUB_STEP_SUMMARY - echo "๐Ÿ“Š [View all security alerts]($SECURITY_URL)" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/deploy-demo.yml b/.github/workflows/deploy-demo.yml deleted file mode 100644 index 206d178..0000000 --- a/.github/workflows/deploy-demo.yml +++ /dev/null @@ -1,734 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# This file is part of a Moko Consulting project. -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# FILE INFORMATION -# DEFGROUP: GitHub.Workflow -# INGROUP: MokoStandards.Deploy -# REPO: https://github.com/mokoconsulting-tech/MokoStandards -# PATH: /templates/workflows/shared/deploy-demo.yml.template -# VERSION: 04.06.00 -# BRIEF: SFTP deployment workflow for demo server โ€” synced to all governed repos -# NOTE: Synced via bulk-repo-sync to .github/workflows/deploy-demo.yml in all governed repos. -# Port is resolved in order: DEMO_FTP_PORT variable โ†’ :port suffix in DEMO_FTP_HOST โ†’ 22. - -name: Deploy to Demo Server (SFTP) - -# Deploys the contents of the src/ directory to the demo server via SFTP. -# Triggers on push/merge to main โ€” deploys the production-ready build to the demo server. -# -# Required org-level variables: DEMO_FTP_HOST, DEMO_FTP_PATH, DEMO_FTP_USERNAME -# Optional org-level variable: DEMO_FTP_PORT (auto-detected from host or defaults to 22) -# Optional org/repo variable: DEMO_FTP_SUFFIX โ€” when set, appended to DEMO_FTP_PATH to form the -# full remote destination: DEMO_FTP_PATH/DEMO_FTP_SUFFIX -# Ignore rules: Place a .ftpignore file in the src/ directory. Each non-empty, -# non-comment line is a glob pattern tested against the relative path -# of each file (e.g. "subdir/file.txt"). The .gitignore is NOT used. -# Required org-level secret: DEMO_FTP_KEY (preferred) or DEMO_FTP_PASSWORD -# -# Access control: only users with admin or maintain role on the repository may deploy. - -on: - push: - branches: - - main - - master - paths: - - 'src/**' - - 'htdocs/**' - pull_request: - types: [opened, synchronize, reopened, closed] - branches: - - main - - master - paths: - - 'src/**' - - 'htdocs/**' - workflow_dispatch: - inputs: - clear_remote: - description: 'Delete all files inside the remote destination folder before uploading' - required: false - default: false - type: boolean - -permissions: - contents: read - pull-requests: write - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - check-permission: - name: Verify Deployment Permission - runs-on: ubuntu-latest - steps: - - name: Check actor permission - env: - # Prefer the org-scoped GH_TOKEN secret (needed for the org membership - # fallback). Falls back to the built-in github.token so the collaborator - # endpoint still works even if GH_TOKEN is not configured. - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - run: | - ACTOR="${{ github.actor }}" - REPO="${{ github.repository }}" - ORG="${{ github.repository_owner }}" - - METHOD="" - AUTHORIZED="false" - - # Hardcoded authorized users โ€” always allowed to deploy - AUTHORIZED_USERS="jmiller github-actions[bot]" - for user in $AUTHORIZED_USERS; do - if [ "$ACTOR" = "$user" ]; then - AUTHORIZED="true" - METHOD="hardcoded allowlist" - PERMISSION="admin" - break - fi - done - - # For other actors, check repo/org permissions via API - if [ "$AUTHORIZED" != "true" ]; then - PERMISSION=$(gh api "repos/${REPO}/collaborators/${ACTOR}/permission" \ - --jq '.permission' 2>/dev/null) - METHOD="repo collaborator API" - - if [ -z "$PERMISSION" ]; then - ORG_ROLE=$(gh api "orgs/${ORG}/memberships/${ACTOR}" \ - --jq '.role' 2>/dev/null) - METHOD="org membership API" - if [ "$ORG_ROLE" = "owner" ]; then - PERMISSION="admin" - else - PERMISSION="none" - fi - fi - - case "$PERMISSION" in - admin|maintain) AUTHORIZED="true" ;; - esac - fi - - # Write detailed summary - { - echo "## ๐Ÿ” Deploy Authorization" - echo "" - echo "| Field | Value |" - echo "|-------|-------|" - echo "| **Actor** | \`${ACTOR}\` |" - echo "| **Repository** | \`${REPO}\` |" - echo "| **Permission** | \`${PERMISSION}\` |" - echo "| **Method** | ${METHOD} |" - echo "| **Authorized** | ${AUTHORIZED} |" - echo "| **Trigger** | \`${{ github.event_name }}\` |" - echo "| **Branch** | \`${{ github.ref_name }}\` |" - echo "" - } >> "$GITHUB_STEP_SUMMARY" - - if [ "$AUTHORIZED" = "true" ]; then - echo "โœ… ${ACTOR} authorized to deploy (${METHOD})" >> "$GITHUB_STEP_SUMMARY" - else - echo "โŒ ${ACTOR} is NOT authorized to deploy." >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "Deployment requires one of:" >> "$GITHUB_STEP_SUMMARY" - echo "- Being in the hardcoded allowlist" >> "$GITHUB_STEP_SUMMARY" - echo "- Having \`admin\` or \`maintain\` role on the repository" >> "$GITHUB_STEP_SUMMARY" - exit 1 - fi - - deploy: - name: SFTP Deploy โ†’ Demo - runs-on: ubuntu-latest - needs: [check-permission] - if: >- - !startsWith(github.head_ref || github.ref_name, 'chore/') && - (github.event_name == 'workflow_dispatch' || - github.event_name == 'push' || - (github.event_name == 'pull_request' && - (github.event.action == 'opened' || - github.event.action == 'synchronize' || - github.event.action == 'reopened' || - github.event.pull_request.merged == true))) - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Resolve source directory - id: source - run: | - # Resolve source directory: src/ preferred, htdocs/ as fallback - if [ -d "src" ]; then - SRC="src" - elif [ -d "htdocs" ]; then - SRC="htdocs" - else - echo "โš ๏ธ No src/ or htdocs/ directory found โ€” skipping deployment" - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - COUNT=$(find "$SRC" -type f | wc -l) - echo "โœ… Source: ${SRC}/ (${COUNT} file(s))" - echo "skip=false" >> "$GITHUB_OUTPUT" - echo "dir=${SRC}" >> "$GITHUB_OUTPUT" - - - name: Preview files to deploy - if: steps.source.outputs.skip == 'false' - env: - SOURCE_DIR: ${{ steps.source.outputs.dir }} - run: | - # โ”€โ”€ Convert a ftpignore-style glob line to an ERE pattern โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - ftpignore_to_regex() { - local line="$1" - local anchored=false - # Strip inline comments and whitespace - line=$(printf '%s' "$line" | sed 's/[[:space:]]*#.*$//' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') - [ -z "$line" ] && return - # Skip negation patterns (not supported) - [[ "$line" == !* ]] && return - # Trailing slash = directory marker; strip it - line="${line%/}" - # Leading slash = anchored to root; strip it - if [[ "$line" == /* ]]; then - anchored=true - line="${line#/}" - fi - # Escape ERE special chars, then restore glob semantics - local regex - regex=$(printf '%s' "$line" \ - | sed 's/[.+^${}()|[\\]/\\&/g' \ - | sed 's/\\\*\\\*/\x01/g' \ - | sed 's/\\\*/[^\/]*/g' \ - | sed 's/\x01/.*/g' \ - | sed 's/\\\?/[^\/]/g') - if $anchored; then - printf '^%s(/|$)' "$regex" - else - printf '(^|/)%s(/|$)' "$regex" - fi - } - - # โ”€โ”€ Read .ftpignore (ftpignore-style globs) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - IGNORE_PATTERNS=() - IGNORE_SOURCES=() - if [ -f "${SOURCE_DIR}/.ftpignore" ]; then - while IFS= read -r line; do - [[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]] && continue - regex=$(ftpignore_to_regex "$line") - [ -n "$regex" ] && IGNORE_PATTERNS+=("$regex") && IGNORE_SOURCES+=("$line") - done < "${SOURCE_DIR}/.ftpignore" - fi - - # โ”€โ”€ Walk src/ and classify every file โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - WILL_UPLOAD=() - IGNORED_FILES=() - while IFS= read -r -d '' file; do - rel="${file#${SOURCE_DIR}/}" - SKIP=false - for i in "${!IGNORE_PATTERNS[@]}"; do - if echo "$rel" | grep -qE "${IGNORE_PATTERNS[$i]}" 2>/dev/null; then - IGNORED_FILES+=("$rel | .ftpignore \`${IGNORE_SOURCES[$i]}\`") - SKIP=true; break - fi - done - $SKIP && continue - WILL_UPLOAD+=("$rel") - done < <(find "$SOURCE_DIR" -type f -print0 | sort -z) - - UPLOAD_COUNT="${#WILL_UPLOAD[@]}" - IGNORE_COUNT="${#IGNORED_FILES[@]}" - - echo "โ„น๏ธ ${UPLOAD_COUNT} file(s) will be uploaded, ${IGNORE_COUNT} ignored" - - # โ”€โ”€ Write deployment preview to step summary โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - { - echo "## ๐Ÿ“‹ Deployment Preview" - echo "" - echo "| Field | Value |" - echo "|---|---|" - echo "| Source | \`${SOURCE_DIR}/\` |" - echo "| Files to upload | **${UPLOAD_COUNT}** |" - echo "| Files ignored | **${IGNORE_COUNT}** |" - echo "" - if [ "${UPLOAD_COUNT}" -gt 0 ]; then - echo "### ๐Ÿ“‚ Files that will be uploaded" - echo '```' - printf '%s\n' "${WILL_UPLOAD[@]}" - echo '```' - echo "" - fi - if [ "${IGNORE_COUNT}" -gt 0 ]; then - echo "### โญ๏ธ Files excluded" - echo "| File | Reason |" - echo "|---|---|" - for entry in "${IGNORED_FILES[@]}"; do - f="${entry% | *}"; r="${entry##* | }" - echo "| \`${f}\` | ${r} |" - done - echo "" - fi - } >> "$GITHUB_STEP_SUMMARY" - - - name: Resolve SFTP host and port - if: steps.source.outputs.skip == 'false' - id: conn - env: - HOST_RAW: ${{ vars.DEMO_FTP_HOST }} - PORT_VAR: ${{ vars.DEMO_FTP_PORT }} - run: | - HOST="$HOST_RAW" - PORT="$PORT_VAR" - - if [ -z "$HOST" ]; then - echo "โญ๏ธ DEMO_FTP_HOST not configured โ€” skipping demo deployment." - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - - # Priority 1 โ€” explicit DEMO_FTP_PORT variable - if [ -n "$PORT" ]; then - echo "โ„น๏ธ Using explicit DEMO_FTP_PORT=${PORT}" - - # Priority 2 โ€” port embedded in DEMO_FTP_HOST (host:port) - elif [[ "$HOST" == *:* ]]; then - PORT="${HOST##*:}" - HOST="${HOST%:*}" - echo "โ„น๏ธ Extracted port ${PORT} from DEMO_FTP_HOST" - - # Priority 3 โ€” SFTP default - else - PORT="22" - echo "โ„น๏ธ No port specified โ€” defaulting to SFTP port 22" - fi - - echo "host=${HOST}" >> "$GITHUB_OUTPUT" - echo "port=${PORT}" >> "$GITHUB_OUTPUT" - echo "SFTP target: ${HOST}:${PORT}" - - - name: Build remote path - if: steps.source.outputs.skip == 'false' && steps.conn.outputs.skip != 'true' - id: remote - env: - DEMO_FTP_PATH: ${{ vars.DEMO_FTP_PATH }} - DEMO_FTP_SUFFIX: ${{ vars.DEMO_FTP_SUFFIX }} - run: | - BASE="$DEMO_FTP_PATH" - - if [ -z "$BASE" ]; then - echo "โญ๏ธ DEMO_FTP_PATH not configured โ€” skipping demo deployment." - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - - # DEMO_FTP_SUFFIX is required โ€” it identifies the remote subdirectory for this repo. - # Without it we cannot safely determine the deployment target. - if [ -z "$DEMO_FTP_SUFFIX" ]; then - echo "โญ๏ธ DEMO_FTP_SUFFIX variable is not set โ€” skipping deployment." - echo " Set DEMO_FTP_SUFFIX as a repo or org variable to enable deploy-demo." - echo "skip=true" >> "$GITHUB_OUTPUT" - echo "path=" >> "$GITHUB_OUTPUT" - exit 0 - fi - - REMOTE="${BASE%/}/${DEMO_FTP_SUFFIX#/}" - - # โ”€โ”€ Platform-specific path safety guards โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - PLATFORM="" - MOKO_FILE=".github/.mokostandards"; [ ! -f "$MOKO_FILE" ] && MOKO_FILE=".mokostandards"; if [ -f "$MOKO_FILE" ]; then - PLATFORM=$(grep -E '^platform:' "$MOKO_FILE" | sed 's/.*:[[:space:]]*//' | tr -d '"') - fi - - if [ "$PLATFORM" = "crm-module" ]; then - # Dolibarr modules must deploy under htdocs/custom/ โ€” guard against - # accidentally overwriting server root or unrelated directories. - if [[ "$REMOTE" != *custom* ]]; then - echo "โŒ Safety check failed: Dolibarr (crm-module) remote path must contain 'custom'." - echo " Current path: ${REMOTE}" - echo " Set DEMO_FTP_SUFFIX to the module's htdocs/custom/ subdirectory." - exit 1 - fi - fi - - if [ "$PLATFORM" = "waas-component" ]; then - # Joomla extensions may only deploy to the server's tmp/ directory. - if [[ "$REMOTE" != *tmp* ]]; then - echo "โŒ Safety check failed: Joomla (waas-component) remote path must contain 'tmp'." - echo " Current path: ${REMOTE}" - echo " Set DEMO_FTP_SUFFIX to a path under the server tmp/ directory." - exit 1 - fi - fi - - echo "โ„น๏ธ Remote path: ${REMOTE}" - echo "path=${REMOTE}" >> "$GITHUB_OUTPUT" - - - name: Detect SFTP authentication method - if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true' - id: auth - env: - HAS_KEY: ${{ secrets.DEMO_FTP_KEY }} - HAS_PASSWORD: ${{ secrets.DEMO_FTP_PASSWORD }} - run: | - if [ -n "$HAS_KEY" ] && [ -n "$HAS_PASSWORD" ]; then - # Both set: key auth with password as passphrase; falls back to password-only if key fails - echo "method=key" >> "$GITHUB_OUTPUT" - echo "use_passphrase=true" >> "$GITHUB_OUTPUT" - echo "has_password=true" >> "$GITHUB_OUTPUT" - echo "โ„น๏ธ Primary: SSH key + passphrase (DEMO_FTP_KEY / DEMO_FTP_PASSWORD)" - echo "โ„น๏ธ Fallback: password-only auth if key authentication fails" - elif [ -n "$HAS_KEY" ]; then - # Key only: no passphrase, no password fallback - echo "method=key" >> "$GITHUB_OUTPUT" - echo "use_passphrase=false" >> "$GITHUB_OUTPUT" - echo "has_password=false" >> "$GITHUB_OUTPUT" - echo "โ„น๏ธ Using SSH key authentication (DEMO_FTP_KEY, no passphrase, no fallback)" - elif [ -n "$HAS_PASSWORD" ]; then - # Password only: direct SFTP password auth - echo "method=password" >> "$GITHUB_OUTPUT" - echo "use_passphrase=false" >> "$GITHUB_OUTPUT" - echo "has_password=true" >> "$GITHUB_OUTPUT" - echo "โ„น๏ธ Using password authentication (DEMO_FTP_PASSWORD)" - else - echo "โŒ No SFTP credentials configured." - echo " Set DEMO_FTP_KEY (preferred) or DEMO_FTP_PASSWORD as an org-level secret." - exit 1 - fi - - - name: Setup PHP - if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true' - uses: shivammathur/setup-php@fcafdd6392932010c2bd5094439b8e33be2a8a09 # v2.37.0 - with: - php-version: '8.1' - tools: composer - - - name: Setup MokoStandards deploy tools - if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true' - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' - run: | - git clone --depth 1 --branch version/04 --quiet \ - "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ - /tmp/mokostandards - cd /tmp/mokostandards - composer install --no-dev --no-interaction --quiet - - - name: Clear remote destination folder (manual only) - if: >- - steps.source.outputs.skip == 'false' && - steps.remote.outputs.skip != 'true' && - inputs.clear_remote == true - env: - SFTP_HOST: ${{ steps.conn.outputs.host }} - SFTP_PORT: ${{ steps.conn.outputs.port }} - SFTP_USER: ${{ vars.DEMO_FTP_USERNAME }} - SFTP_KEY: ${{ secrets.DEMO_FTP_KEY }} - SFTP_PASSWORD: ${{ secrets.DEMO_FTP_PASSWORD }} - AUTH_METHOD: ${{ steps.auth.outputs.method }} - USE_PASSPHRASE: ${{ steps.auth.outputs.use_passphrase }} - HAS_PASSWORD: ${{ steps.auth.outputs.has_password }} - REMOTE_PATH: ${{ steps.remote.outputs.path }} - run: | - cat > /tmp/moko_clear.php << 'PHPEOF' - login($username, $key)) { - if ($password !== '') { - echo "โš ๏ธ Key auth failed โ€” falling back to password\n"; - if (!$sftp->login($username, $password)) { - fwrite(STDERR, "โŒ Both key and password authentication failed\n"); - exit(1); - } - echo "โœ… Connected via password authentication (key fallback)\n"; - } else { - fwrite(STDERR, "โŒ Key authentication failed and no password fallback is available\n"); - exit(1); - } - } else { - echo "โœ… Connected via SSH key authentication\n"; - } - } else { - if (!$sftp->login($username, (string) getenv('SFTP_PASSWORD'))) { - fwrite(STDERR, "โŒ Password authentication failed\n"); - exit(1); - } - echo "โœ… Connected via password authentication\n"; - } - - // โ”€โ”€ Recursive delete โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - function rmrf(SFTP $sftp, string $path): void - { - $entries = $sftp->nlist($path); - if ($entries === false) { - return; // path does not exist โ€” nothing to clear - } - foreach ($entries as $name) { - if ($name === '.' || $name === '..') { - continue; - } - $entry = "{$path}/{$name}"; - if ($sftp->is_dir($entry)) { - rmrf($sftp, $entry); - $sftp->rmdir($entry); - echo " ๐Ÿ—‘๏ธ Removed dir: {$entry}\n"; - } else { - $sftp->delete($entry); - echo " ๐Ÿ—‘๏ธ Removed file: {$entry}\n"; - } - } - } - - // โ”€โ”€ Create remote directory tree โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - function sftpMakedirs(SFTP $sftp, string $path): void - { - $parts = array_values(array_filter(explode('/', $path), fn(string $p) => $p !== '')); - $current = str_starts_with($path, '/') ? '' : ''; - foreach ($parts as $part) { - $current .= '/' . $part; - $sftp->mkdir($current); // silently returns false if already exists - } - } - - rmrf($sftp, $remotePath); - sftpMakedirs($sftp, $remotePath); - echo "โœ… Remote folder ready: {$remotePath}\n"; - PHPEOF - php /tmp/moko_clear.php - - - name: Deploy via SFTP - if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true' - env: - SFTP_HOST: ${{ steps.conn.outputs.host }} - SFTP_PORT: ${{ steps.conn.outputs.port }} - SFTP_USER: ${{ vars.DEMO_FTP_USERNAME }} - SFTP_KEY: ${{ secrets.DEMO_FTP_KEY }} - SFTP_PASSWORD: ${{ secrets.DEMO_FTP_PASSWORD }} - AUTH_METHOD: ${{ steps.auth.outputs.method }} - USE_PASSPHRASE: ${{ steps.auth.outputs.use_passphrase }} - REMOTE_PATH: ${{ steps.remote.outputs.path }} - SOURCE_DIR: ${{ steps.source.outputs.dir }} - run: | - # โ”€โ”€ Write SSH key to temp file (key auth only) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - if [ "$AUTH_METHOD" = "key" ]; then - printf '%s' "$SFTP_KEY" > /tmp/deploy_key - chmod 600 /tmp/deploy_key - fi - - # โ”€โ”€ Generate sftp-config.json safely via jq โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - if [ "$AUTH_METHOD" = "key" ]; then - jq -n \ - --arg host "$SFTP_HOST" \ - --argjson port "${SFTP_PORT:-22}" \ - --arg user "$SFTP_USER" \ - --arg path "$REMOTE_PATH" \ - --arg key "/tmp/deploy_key" \ - '{host:$host, port:$port, user:$user, remote_path:$path, ssh_key_file:$key}' \ - > /tmp/sftp-config.json - else - jq -n \ - --arg host "$SFTP_HOST" \ - --argjson port "${SFTP_PORT:-22}" \ - --arg user "$SFTP_USER" \ - --arg path "$REMOTE_PATH" \ - --arg pass "$SFTP_PASSWORD" \ - '{host:$host, port:$port, user:$user, remote_path:$path, password:$pass}' \ - > /tmp/sftp-config.json - fi - - # โ”€โ”€ Write update files (demo = stable) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true) - VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null || echo "unknown") - REPO="${{ github.repository }}" - - if [ "$PLATFORM" = "crm-module" ]; then - printf '%s' "$VERSION" > update.txt - fi - - if [ "$PLATFORM" = "waas-component" ]; then - MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1 || true) - if [ -n "$MANIFEST" ]; then - EXT_NAME=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "${{ github.event.repository.name }}") - EXT_TYPE=$(grep -oP ']+type="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "component") - EXT_ELEMENT=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || basename "$MANIFEST" .xml) - EXT_CLIENT=$(grep -oP ']+client="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "") - EXT_FOLDER=$(grep -oP ']+group="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "") - TARGET_PLATFORM=$(grep -oP '/dev/null | head -1 || true) - [ -n "$TARGET_PLATFORM" ] && TARGET_PLATFORM="${TARGET_PLATFORM}>" - [ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '' "/") - - CLIENT_TAG="" - if [ -n "$EXT_CLIENT" ]; then CLIENT_TAG="${EXT_CLIENT}"; elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then CLIENT_TAG="site"; fi - FOLDER_TAG="" - if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then FOLDER_TAG="${EXT_FOLDER}"; fi - - DOWNLOAD_URL="https://github.com/${REPO}/releases/download/v${VERSION}/${EXT_ELEMENT}-${VERSION}.zip" - { - printf '%s\n' '' - printf '%s\n' '' - printf '%s\n' ' ' - printf '%s\n' " ${EXT_NAME}" - printf '%s\n' " ${EXT_NAME} update" - printf '%s\n' " ${EXT_ELEMENT}" - printf '%s\n' " ${EXT_TYPE}" - printf '%s\n' " ${VERSION}" - [ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}" - [ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}" - printf '%s\n' ' ' - printf '%s\n' ' stable' - printf '%s\n' ' ' - printf '%s\n' " https://github.com/${REPO}" - printf '%s\n' ' ' - printf '%s\n' " ${DOWNLOAD_URL}" - printf '%s\n' ' ' - printf '%s\n' " ${TARGET_PLATFORM}" - printf '%s\n' ' Moko Consulting' - printf '%s\n' ' https://mokoconsulting.tech' - printf '%s\n' ' ' - printf '%s\n' '' - } > updates.xml - fi - fi - - # โ”€โ”€ Run deploy-sftp.php from MokoStandards โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json) - if [ "$USE_PASSPHRASE" = "true" ]; then - DEPLOY_ARGS+=(--key-passphrase "$SFTP_PASSWORD") - fi - - PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true) - if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards/api/deploy/deploy-joomla.php" ]; then - php /tmp/mokostandards/api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}" - else - php /tmp/mokostandards/api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}" - fi - # Remove temp files that should never be left behind - rm -f /tmp/deploy_key /tmp/sftp-config.json - - - name: Create or update failure issue - if: failure() && steps.remote.outputs.skip != 'true' && steps.conn.outputs.skip != 'true' - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - run: | - REPO="${{ github.repository }}" - RUN_URL="${{ github.server_url }}/${REPO}/actions/runs/${{ github.run_id }}" - ACTOR="${{ github.actor }}" - BRANCH="${{ github.ref_name }}" - EVENT="${{ github.event_name }}" - NOW=$(date -u '+%Y-%m-%d %H:%M:%S UTC') - LABEL="deploy-failure" - - TITLE="fix: Demo deployment failed โ€” ${REPO}" - BODY="## Demo Deployment Failed - - A deployment to the demo server failed and requires attention. - - | Field | Value | - |-------|-------| - | **Repository** | \`${REPO}\` | - | **Branch** | \`${BRANCH}\` | - | **Trigger** | ${EVENT} | - | **Actor** | @${ACTOR} | - | **Failed at** | ${NOW} | - | **Run** | [View workflow run](${RUN_URL}) | - - ### Next steps - 1. Review the [workflow run log](${RUN_URL}) for the specific error. - 2. Fix the underlying issue (credentials, SFTP connectivity, permissions). - 3. Re-trigger the deployment via **Actions โ†’ Deploy to Demo Server โ†’ Run workflow**. - - --- - *Auto-created by deploy-demo.yml โ€” close this issue once the deployment is resolved.*" - - # Ensure the label exists (idempotent โ€” no-op if already present) - gh label create "$LABEL" \ - --repo "$REPO" \ - --color "CC0000" \ - --description "Automated deploy failure tracking" \ - --force 2>/dev/null || true - - # Look for an existing open deploy-failure issue - EXISTING=$(gh api "repos/${REPO}/issues?labels=${LABEL}&state=all&per_page=1&sort=created&direction=desc" \ - --jq '.[0].number' 2>/dev/null) - - if [ -n "$EXISTING" ] && [ "$EXISTING" != "null" ]; then - gh api "repos/${REPO}/issues/${EXISTING}" \ - -X PATCH \ - -f title="$TITLE" \ - -f body="$BODY" \ - -f state="open" \ - --silent - echo "๐Ÿ“‹ Failure issue #${EXISTING} updated/reopened: ${REPO}" >> "$GITHUB_STEP_SUMMARY" - else - gh issue create \ - --repo "$REPO" \ - --title "$TITLE" \ - --body "$BODY" \ - --label "$LABEL" \ - --assignee "jmiller" \ - | tee -a "$GITHUB_STEP_SUMMARY" - fi - - - name: Deployment summary - if: always() - run: | - if [ "${{ steps.source.outputs.skip }}" == "true" ]; then - echo "### โญ๏ธ Deployment Skipped" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "No \`src/\` directory found in this repository." >> "$GITHUB_STEP_SUMMARY" - elif [ "${{ job.status }}" == "success" ]; then - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "### โœ… Demo Deployment Successful" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "| Field | Value |" >> "$GITHUB_STEP_SUMMARY" - echo "|-------|-------|" >> "$GITHUB_STEP_SUMMARY" - echo "| Host | \`${{ steps.conn.outputs.host }}:${{ steps.conn.outputs.port }}\` |" >> "$GITHUB_STEP_SUMMARY" - echo "| Remote path | \`${{ steps.remote.outputs.path }}\` |" >> "$GITHUB_STEP_SUMMARY" - echo "| Source | \`src/\` |" >> "$GITHUB_STEP_SUMMARY" - echo "| Trigger | ${{ github.event_name }} |" >> "$GITHUB_STEP_SUMMARY" - echo "| Auth | ${{ steps.auth.outputs.method }} |" >> "$GITHUB_STEP_SUMMARY" - echo "| Clear remote | ${{ inputs.clear_remote || 'false' }} |" >> "$GITHUB_STEP_SUMMARY" - else - echo "### โŒ Demo Deployment Failed" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "Check the job log above for error details." >> "$GITHUB_STEP_SUMMARY" - fi diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml deleted file mode 100644 index acc6d07..0000000 --- a/.github/workflows/deploy-dev.yml +++ /dev/null @@ -1,701 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# This file is part of a Moko Consulting project. -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# FILE INFORMATION -# DEFGROUP: GitHub.Workflow -# INGROUP: MokoStandards.Deploy -# REPO: https://github.com/mokoconsulting-tech/MokoStandards -# PATH: /templates/workflows/shared/deploy-dev.yml.template -# VERSION: 04.06.00 -# BRIEF: SFTP deployment workflow for development server โ€” synced to all governed repos -# NOTE: Synced via bulk-repo-sync to .github/workflows/deploy-dev.yml in all governed repos. -# Port is resolved in order: DEV_FTP_PORT variable โ†’ :port suffix in DEV_FTP_HOST โ†’ 22. - -name: Deploy to Dev Server (SFTP) - -# Deploys the contents of the src/ directory to the development server via SFTP. -# Triggers on every pull_request to development branches (so the dev server always -# reflects the latest PR state) and on push/merge to main branches. -# -# Required org-level variables: DEV_FTP_HOST, DEV_FTP_PATH, DEV_FTP_USERNAME -# Optional org-level variable: DEV_FTP_PORT (auto-detected from host or defaults to 22) -# Optional org/repo variable: DEV_FTP_SUFFIX โ€” when set, appended to DEV_FTP_PATH to form the -# full remote destination: DEV_FTP_PATH/DEV_FTP_SUFFIX -# Ignore rules: Place a .ftpignore file in the src/ directory. Each non-empty, -# non-comment line is a glob pattern tested against the relative path -# of each file (e.g. "subdir/file.txt"). The .gitignore is NOT used. -# Required org-level secret: DEV_FTP_KEY (preferred) or DEV_FTP_PASSWORD -# -# Access control: only users with admin or maintain role on the repository may deploy. - -on: - push: - branches: - - 'dev/**' - - 'rc/**' - - develop - - development - paths: - - 'src/**' - - 'htdocs/**' - pull_request: - types: [opened, synchronize, reopened, closed] - branches: - - 'dev/**' - - 'rc/**' - - develop - - development - paths: - - 'src/**' - - 'htdocs/**' - workflow_dispatch: - inputs: - clear_remote: - description: 'Delete all files inside the remote destination folder before uploading' - required: false - default: false - type: boolean - -permissions: - contents: read - pull-requests: write - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - check-permission: - name: Verify Deployment Permission - runs-on: ubuntu-latest - steps: - - name: Check actor permission - env: - # Prefer the org-scoped GH_TOKEN secret (needed for the org membership - # fallback). Falls back to the built-in github.token so the collaborator - # endpoint still works even if GH_TOKEN is not configured. - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - run: | - ACTOR="${{ github.actor }}" - REPO="${{ github.repository }}" - ORG="${{ github.repository_owner }}" - - METHOD="" - AUTHORIZED="false" - - # Hardcoded authorized users โ€” always allowed to deploy - AUTHORIZED_USERS="jmiller github-actions[bot]" - for user in $AUTHORIZED_USERS; do - if [ "$ACTOR" = "$user" ]; then - AUTHORIZED="true" - METHOD="hardcoded allowlist" - PERMISSION="admin" - break - fi - done - - # For other actors, check repo/org permissions via API - if [ "$AUTHORIZED" != "true" ]; then - PERMISSION=$(gh api "repos/${REPO}/collaborators/${ACTOR}/permission" \ - --jq '.permission' 2>/dev/null) - METHOD="repo collaborator API" - - if [ -z "$PERMISSION" ]; then - ORG_ROLE=$(gh api "orgs/${ORG}/memberships/${ACTOR}" \ - --jq '.role' 2>/dev/null) - METHOD="org membership API" - if [ "$ORG_ROLE" = "owner" ]; then - PERMISSION="admin" - else - PERMISSION="none" - fi - fi - - case "$PERMISSION" in - admin|maintain) AUTHORIZED="true" ;; - esac - fi - - # Write detailed summary - { - echo "## ๐Ÿ” Deploy Authorization" - echo "" - echo "| Field | Value |" - echo "|-------|-------|" - echo "| **Actor** | \`${ACTOR}\` |" - echo "| **Repository** | \`${REPO}\` |" - echo "| **Permission** | \`${PERMISSION}\` |" - echo "| **Method** | ${METHOD} |" - echo "| **Authorized** | ${AUTHORIZED} |" - echo "| **Trigger** | \`${{ github.event_name }}\` |" - echo "| **Branch** | \`${{ github.ref_name }}\` |" - echo "" - } >> "$GITHUB_STEP_SUMMARY" - - if [ "$AUTHORIZED" = "true" ]; then - echo "โœ… ${ACTOR} authorized to deploy (${METHOD})" >> "$GITHUB_STEP_SUMMARY" - else - echo "โŒ ${ACTOR} is NOT authorized to deploy." >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "Deployment requires one of:" >> "$GITHUB_STEP_SUMMARY" - echo "- Being in the hardcoded allowlist" >> "$GITHUB_STEP_SUMMARY" - echo "- Having \`admin\` or \`maintain\` role on the repository" >> "$GITHUB_STEP_SUMMARY" - exit 1 - fi - - deploy: - name: SFTP Deploy โ†’ Dev - runs-on: ubuntu-latest - needs: [check-permission] - if: >- - !startsWith(github.head_ref || github.ref_name, 'chore/') && - (github.event_name == 'workflow_dispatch' || - github.event_name == 'push' || - (github.event_name == 'pull_request' && - (github.event.action == 'opened' || - github.event.action == 'synchronize' || - github.event.action == 'reopened' || - github.event.pull_request.merged == true))) - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Resolve source directory - id: source - run: | - # Resolve source directory: src/ preferred, htdocs/ as fallback - if [ -d "src" ]; then - SRC="src" - elif [ -d "htdocs" ]; then - SRC="htdocs" - else - echo "โš ๏ธ No src/ or htdocs/ directory found โ€” skipping deployment" - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - COUNT=$(find "$SRC" -type f | wc -l) - echo "โœ… Source: ${SRC}/ (${COUNT} file(s))" - echo "skip=false" >> "$GITHUB_OUTPUT" - echo "dir=${SRC}" >> "$GITHUB_OUTPUT" - - - name: Preview files to deploy - if: steps.source.outputs.skip == 'false' - env: - SOURCE_DIR: ${{ steps.source.outputs.dir }} - run: | - # โ”€โ”€ Convert a ftpignore-style glob line to an ERE pattern โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - ftpignore_to_regex() { - local line="$1" - local anchored=false - # Strip inline comments and whitespace - line=$(printf '%s' "$line" | sed 's/[[:space:]]*#.*$//' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') - [ -z "$line" ] && return - # Skip negation patterns (not supported) - [[ "$line" == !* ]] && return - # Trailing slash = directory marker; strip it - line="${line%/}" - # Leading slash = anchored to root; strip it - if [[ "$line" == /* ]]; then - anchored=true - line="${line#/}" - fi - # Escape ERE special chars, then restore glob semantics - local regex - regex=$(printf '%s' "$line" \ - | sed 's/[.+^${}()|[\\]/\\&/g' \ - | sed 's/\\\*\\\*/\x01/g' \ - | sed 's/\\\*/[^\/]*/g' \ - | sed 's/\x01/.*/g' \ - | sed 's/\\\?/[^\/]/g') - if $anchored; then - printf '^%s(/|$)' "$regex" - else - printf '(^|/)%s(/|$)' "$regex" - fi - } - - # โ”€โ”€ Read .ftpignore (ftpignore-style globs) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - IGNORE_PATTERNS=() - IGNORE_SOURCES=() - if [ -f "${SOURCE_DIR}/.ftpignore" ]; then - while IFS= read -r line; do - [[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]] && continue - regex=$(ftpignore_to_regex "$line") - [ -n "$regex" ] && IGNORE_PATTERNS+=("$regex") && IGNORE_SOURCES+=("$line") - done < "${SOURCE_DIR}/.ftpignore" - fi - - # โ”€โ”€ Walk src/ and classify every file โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - WILL_UPLOAD=() - IGNORED_FILES=() - while IFS= read -r -d '' file; do - rel="${file#${SOURCE_DIR}/}" - SKIP=false - for i in "${!IGNORE_PATTERNS[@]}"; do - if echo "$rel" | grep -qE "${IGNORE_PATTERNS[$i]}" 2>/dev/null; then - IGNORED_FILES+=("$rel | .ftpignore \`${IGNORE_SOURCES[$i]}\`") - SKIP=true; break - fi - done - $SKIP && continue - WILL_UPLOAD+=("$rel") - done < <(find "$SOURCE_DIR" -type f -print0 | sort -z) - - UPLOAD_COUNT="${#WILL_UPLOAD[@]}" - IGNORE_COUNT="${#IGNORED_FILES[@]}" - - echo "โ„น๏ธ ${UPLOAD_COUNT} file(s) will be uploaded, ${IGNORE_COUNT} ignored" - - # โ”€โ”€ Write deployment preview to step summary โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - { - echo "## ๐Ÿ“‹ Deployment Preview" - echo "" - echo "| Field | Value |" - echo "|---|---|" - echo "| Source | \`${SOURCE_DIR}/\` |" - echo "| Files to upload | **${UPLOAD_COUNT}** |" - echo "| Files ignored | **${IGNORE_COUNT}** |" - echo "" - if [ "${UPLOAD_COUNT}" -gt 0 ]; then - echo "### ๐Ÿ“‚ Files that will be uploaded" - echo '```' - printf '%s\n' "${WILL_UPLOAD[@]}" - echo '```' - echo "" - fi - if [ "${IGNORE_COUNT}" -gt 0 ]; then - echo "### โญ๏ธ Files excluded" - echo "| File | Reason |" - echo "|---|---|" - for entry in "${IGNORED_FILES[@]}"; do - f="${entry% | *}"; r="${entry##* | }" - echo "| \`${f}\` | ${r} |" - done - echo "" - fi - } >> "$GITHUB_STEP_SUMMARY" - - - name: Resolve SFTP host and port - if: steps.source.outputs.skip == 'false' - id: conn - env: - HOST_RAW: ${{ vars.DEV_FTP_HOST }} - PORT_VAR: ${{ vars.DEV_FTP_PORT }} - run: | - HOST="$HOST_RAW" - PORT="$PORT_VAR" - - # Priority 1 โ€” explicit DEV_FTP_PORT variable - if [ -n "$PORT" ]; then - echo "โ„น๏ธ Using explicit DEV_FTP_PORT=${PORT}" - - # Priority 2 โ€” port embedded in DEV_FTP_HOST (host:port) - elif [[ "$HOST" == *:* ]]; then - PORT="${HOST##*:}" - HOST="${HOST%:*}" - echo "โ„น๏ธ Extracted port ${PORT} from DEV_FTP_HOST" - - # Priority 3 โ€” SFTP default - else - PORT="22" - echo "โ„น๏ธ No port specified โ€” defaulting to SFTP port 22" - fi - - echo "host=${HOST}" >> "$GITHUB_OUTPUT" - echo "port=${PORT}" >> "$GITHUB_OUTPUT" - echo "SFTP target: ${HOST}:${PORT}" - - - name: Build remote path - if: steps.source.outputs.skip == 'false' - id: remote - env: - DEV_FTP_PATH: ${{ vars.DEV_FTP_PATH }} - DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }} - run: | - BASE="$DEV_FTP_PATH" - - if [ -z "$BASE" ]; then - echo "โญ๏ธ DEV_FTP_PATH is not set โ€” skipping deployment." - echo " Configure it as a repo or org-level variable to enable deploy-dev." - echo "skip=true" >> "$GITHUB_OUTPUT" - echo "path=" >> "$GITHUB_OUTPUT" - exit 0 - fi - - # DEV_FTP_SUFFIX is required โ€” it identifies the remote subdirectory for this repo. - # Without it we cannot safely determine the deployment target. - if [ -z "$DEV_FTP_SUFFIX" ]; then - echo "โญ๏ธ DEV_FTP_SUFFIX variable is not set โ€” skipping deployment." - echo " Set DEV_FTP_SUFFIX as a repo or org variable to enable deploy-dev." - echo "skip=true" >> "$GITHUB_OUTPUT" - echo "path=" >> "$GITHUB_OUTPUT" - exit 0 - fi - - REMOTE="${BASE%/}/${DEV_FTP_SUFFIX#/}" - - # โ”€โ”€ Platform-specific path safety guards โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - PLATFORM="" - MOKO_FILE=".github/.mokostandards"; [ ! -f "$MOKO_FILE" ] && MOKO_FILE=".mokostandards"; if [ -f "$MOKO_FILE" ]; then - PLATFORM=$(grep -oP '^platform:.*' "$MOKO_FILE" 2>/dev/null || true) - fi - - if [ "$PLATFORM" = "crm-module" ]; then - # Dolibarr modules must deploy under htdocs/custom/ โ€” guard against - # accidentally overwriting server root or unrelated directories. - if [[ "$REMOTE" != *custom* ]]; then - echo "โŒ Safety check failed: Dolibarr (crm-module) remote path must contain 'custom'." - echo " Current path: ${REMOTE}" - echo " Set DEV_FTP_SUFFIX to the module's htdocs/custom/ subdirectory." - exit 1 - fi - fi - - if [ "$PLATFORM" = "waas-component" ]; then - # Joomla extensions may only deploy to the server's tmp/ directory. - if [[ "$REMOTE" != *tmp* ]]; then - echo "โŒ Safety check failed: Joomla (waas-component) remote path must contain 'tmp'." - echo " Current path: ${REMOTE}" - echo " Set DEV_FTP_SUFFIX to a path under the server tmp/ directory." - exit 1 - fi - fi - - echo "โ„น๏ธ Remote path: ${REMOTE}" - echo "path=${REMOTE}" >> "$GITHUB_OUTPUT" - - - name: Detect SFTP authentication method - if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true' - id: auth - env: - HAS_KEY: ${{ secrets.DEV_FTP_KEY }} - HAS_PASSWORD: ${{ secrets.DEV_FTP_PASSWORD }} - run: | - if [ -n "$HAS_KEY" ] && [ -n "$HAS_PASSWORD" ]; then - # Both set: key auth with password as passphrase; falls back to password-only if key fails - echo "method=key" >> "$GITHUB_OUTPUT" - echo "use_passphrase=true" >> "$GITHUB_OUTPUT" - echo "has_password=true" >> "$GITHUB_OUTPUT" - echo "โ„น๏ธ Primary: SSH key + passphrase (DEV_FTP_KEY / DEV_FTP_PASSWORD)" - echo "โ„น๏ธ Fallback: password-only auth if key authentication fails" - elif [ -n "$HAS_KEY" ]; then - # Key only: no passphrase, no password fallback - echo "method=key" >> "$GITHUB_OUTPUT" - echo "use_passphrase=false" >> "$GITHUB_OUTPUT" - echo "has_password=false" >> "$GITHUB_OUTPUT" - echo "โ„น๏ธ Using SSH key authentication (DEV_FTP_KEY, no passphrase, no fallback)" - elif [ -n "$HAS_PASSWORD" ]; then - # Password only: direct SFTP password auth - echo "method=password" >> "$GITHUB_OUTPUT" - echo "use_passphrase=false" >> "$GITHUB_OUTPUT" - echo "has_password=true" >> "$GITHUB_OUTPUT" - echo "โ„น๏ธ Using password authentication (DEV_FTP_PASSWORD)" - else - echo "โŒ No SFTP credentials configured." - echo " Set DEV_FTP_KEY (preferred) or DEV_FTP_PASSWORD as an org-level secret." - exit 1 - fi - - - name: Setup PHP - if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true' - uses: shivammathur/setup-php@fcafdd6392932010c2bd5094439b8e33be2a8a09 # v2.37.0 - with: - php-version: '8.1' - tools: composer - - - name: Setup MokoStandards deploy tools - if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true' - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' - run: | - git clone --depth 1 --branch version/04 --quiet \ - "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ - /tmp/mokostandards - cd /tmp/mokostandards - composer install --no-dev --no-interaction --quiet - - - name: Clear remote destination folder (manual only) - if: >- - steps.source.outputs.skip == 'false' && - steps.remote.outputs.skip != 'true' && - inputs.clear_remote == true - env: - SFTP_HOST: ${{ steps.conn.outputs.host }} - SFTP_PORT: ${{ steps.conn.outputs.port }} - SFTP_USER: ${{ vars.DEV_FTP_USERNAME }} - SFTP_KEY: ${{ secrets.DEV_FTP_KEY }} - SFTP_PASSWORD: ${{ secrets.DEV_FTP_PASSWORD }} - AUTH_METHOD: ${{ steps.auth.outputs.method }} - USE_PASSPHRASE: ${{ steps.auth.outputs.use_passphrase }} - HAS_PASSWORD: ${{ steps.auth.outputs.has_password }} - REMOTE_PATH: ${{ steps.remote.outputs.path }} - run: | - cat > /tmp/moko_clear.php << 'PHPEOF' - login($username, $key)) { - if ($password !== '') { - echo "โš ๏ธ Key auth failed โ€” falling back to password\n"; - if (!$sftp->login($username, $password)) { - fwrite(STDERR, "โŒ Both key and password authentication failed\n"); - exit(1); - } - echo "โœ… Connected via password authentication (key fallback)\n"; - } else { - fwrite(STDERR, "โŒ Key authentication failed and no password fallback is available\n"); - exit(1); - } - } else { - echo "โœ… Connected via SSH key authentication\n"; - } - } else { - if (!$sftp->login($username, (string) getenv('SFTP_PASSWORD'))) { - fwrite(STDERR, "โŒ Password authentication failed\n"); - exit(1); - } - echo "โœ… Connected via password authentication\n"; - } - - // โ”€โ”€ Recursive delete โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - function rmrf(SFTP $sftp, string $path): void - { - $entries = $sftp->nlist($path); - if ($entries === false) { - return; // path does not exist โ€” nothing to clear - } - foreach ($entries as $name) { - if ($name === '.' || $name === '..') { - continue; - } - $entry = "{$path}/{$name}"; - if ($sftp->is_dir($entry)) { - rmrf($sftp, $entry); - $sftp->rmdir($entry); - echo " ๐Ÿ—‘๏ธ Removed dir: {$entry}\n"; - } else { - $sftp->delete($entry); - echo " ๐Ÿ—‘๏ธ Removed file: {$entry}\n"; - } - } - } - - // โ”€โ”€ Create remote directory tree โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - function sftpMakedirs(SFTP $sftp, string $path): void - { - $parts = array_values(array_filter(explode('/', $path), fn(string $p) => $p !== '')); - $current = str_starts_with($path, '/') ? '' : ''; - foreach ($parts as $part) { - $current .= '/' . $part; - $sftp->mkdir($current); // silently returns false if already exists - } - } - - rmrf($sftp, $remotePath); - sftpMakedirs($sftp, $remotePath); - echo "โœ… Remote folder ready: {$remotePath}\n"; - PHPEOF - php /tmp/moko_clear.php - - - name: Deploy via SFTP - if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true' - env: - SFTP_HOST: ${{ steps.conn.outputs.host }} - SFTP_PORT: ${{ steps.conn.outputs.port }} - SFTP_USER: ${{ vars.DEV_FTP_USERNAME }} - SFTP_KEY: ${{ secrets.DEV_FTP_KEY }} - SFTP_PASSWORD: ${{ secrets.DEV_FTP_PASSWORD }} - AUTH_METHOD: ${{ steps.auth.outputs.method }} - USE_PASSPHRASE: ${{ steps.auth.outputs.use_passphrase }} - REMOTE_PATH: ${{ steps.remote.outputs.path }} - SOURCE_DIR: ${{ steps.source.outputs.dir }} - run: | - # โ”€โ”€ Write SSH key to temp file (key auth only) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - if [ "$AUTH_METHOD" = "key" ]; then - printf '%s' "$SFTP_KEY" > /tmp/deploy_key - chmod 600 /tmp/deploy_key - fi - - # โ”€โ”€ Generate sftp-config.json safely via jq โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - if [ "$AUTH_METHOD" = "key" ]; then - jq -n \ - --arg host "$SFTP_HOST" \ - --argjson port "${SFTP_PORT:-22}" \ - --arg user "$SFTP_USER" \ - --arg path "$REMOTE_PATH" \ - --arg key "/tmp/deploy_key" \ - '{host:$host, port:$port, user:$user, remote_path:$path, ssh_key_file:$key}' \ - > /tmp/sftp-config.json - else - jq -n \ - --arg host "$SFTP_HOST" \ - --argjson port "${SFTP_PORT:-22}" \ - --arg user "$SFTP_USER" \ - --arg path "$REMOTE_PATH" \ - --arg pass "$SFTP_PASSWORD" \ - '{host:$host, port:$port, user:$user, remote_path:$path, password:$pass}' \ - > /tmp/sftp-config.json - fi - - # Dev deploys skip minified files โ€” use unminified sources for debugging - echo "*.min.js" >> "${SOURCE_DIR}/.ftpignore" - echo "*.min.css" >> "${SOURCE_DIR}/.ftpignore" - - # โ”€โ”€ Run deploy-sftp.php from MokoStandards โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json) - if [ "$USE_PASSPHRASE" = "true" ]; then - DEPLOY_ARGS+=(--key-passphrase "$SFTP_PASSWORD") - fi - - # Set platform version to "development" before deploy (Dolibarr + Joomla) - php /tmp/mokostandards/api/cli/version_set_platform.php --path . --version development - - # Write update files โ€” dev/** = development, rc/** = rc - PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true) - REPO="${{ github.repository }}" - BRANCH="${{ github.ref_name }}" - - # Determine stability tag from branch prefix - STABILITY="development" - VERSION_LABEL="development" - if [[ "$BRANCH" == rc/* ]]; then - STABILITY="rc" - VERSION_LABEL=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null || echo "${BRANCH#rc/}")-rc - fi - - if [ "$PLATFORM" = "crm-module" ]; then - printf '%s' "$VERSION_LABEL" > update.txt - fi - - if [ "$PLATFORM" = "waas-component" ]; then - MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1 || true) - if [ -n "$MANIFEST" ]; then - EXT_NAME=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "${{ github.event.repository.name }}") - EXT_TYPE=$(grep -oP ']+type="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "component") - EXT_ELEMENT=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || basename "$MANIFEST" .xml) - EXT_CLIENT=$(grep -oP ']+client="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "") - EXT_FOLDER=$(grep -oP ']+group="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "") - TARGET_PLATFORM=$(grep -oP '/dev/null | head -1 || true) - [ -n "$TARGET_PLATFORM" ] && TARGET_PLATFORM="${TARGET_PLATFORM}>" - [ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '' "/") - - CLIENT_TAG="" - if [ -n "$EXT_CLIENT" ]; then - CLIENT_TAG="${EXT_CLIENT}" - elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then - CLIENT_TAG="site" - fi - - FOLDER_TAG="" - if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then - FOLDER_TAG="${EXT_FOLDER}" - fi - - DOWNLOAD_URL="https://github.com/${REPO}/archive/refs/heads/${BRANCH}.zip" - - { - printf '%s\n' '' - printf '%s\n' '' - printf '%s\n' ' ' - printf '%s\n' " ${EXT_NAME}" - printf '%s\n' " ${EXT_NAME} ${STABILITY} build" - printf '%s\n' " ${EXT_ELEMENT}" - printf '%s\n' " ${EXT_TYPE}" - printf '%s\n' " ${VERSION_LABEL}" - [ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}" - [ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}" - printf '%s\n' ' ' - printf '%s\n' " ${STABILITY}" - printf '%s\n' ' ' - printf '%s\n' " https://github.com/${REPO}/tree/${BRANCH}" - printf '%s\n' ' ' - printf '%s\n' " ${DOWNLOAD_URL}" - printf '%s\n' ' ' - printf '%s\n' " ${TARGET_PLATFORM}" - printf '%s\n' ' Moko Consulting' - printf '%s\n' ' https://mokoconsulting.tech' - printf '%s\n' ' ' - printf '%s\n' '' - } > updates.xml - sed -i '/^[[:space:]]*$/d' updates.xml - fi - fi - - # Use Joomla-aware deploy for waas-component (routes files to correct Joomla dirs) - # Use standard SFTP deploy for everything else - PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true) - if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards/api/deploy/deploy-joomla.php" ]; then - php /tmp/mokostandards/api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}" - else - php /tmp/mokostandards/api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}" - fi - # (both scripts handle dotfile skipping and .ftpignore natively) - # Remove temp files that should never be left behind - rm -f /tmp/deploy_key /tmp/sftp-config.json - - # Dev deploys fail silently โ€” no issue creation. - # Demo and RS deploys create failure issues (production-facing). - - - name: Deployment summary - if: always() - run: | - if [ "${{ steps.source.outputs.skip }}" == "true" ]; then - echo "### โญ๏ธ Deployment Skipped" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "No \`src/\` directory found in this repository." >> "$GITHUB_STEP_SUMMARY" - elif [ "${{ job.status }}" == "success" ]; then - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "### โœ… Dev Deployment Successful" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "| Field | Value |" >> "$GITHUB_STEP_SUMMARY" - echo "|-------|-------|" >> "$GITHUB_STEP_SUMMARY" - echo "| Host | \`${{ steps.conn.outputs.host }}:${{ steps.conn.outputs.port }}\` |" >> "$GITHUB_STEP_SUMMARY" - echo "| Remote path | \`${{ steps.remote.outputs.path }}\` |" >> "$GITHUB_STEP_SUMMARY" - echo "| Source | \`src/\` |" >> "$GITHUB_STEP_SUMMARY" - echo "| Trigger | ${{ github.event_name }} |" >> "$GITHUB_STEP_SUMMARY" - echo "| Auth | ${{ steps.auth.outputs.method }} |" >> "$GITHUB_STEP_SUMMARY" - echo "| Clear remote | ${{ inputs.clear_remote || 'false' }} |" >> "$GITHUB_STEP_SUMMARY" - else - echo "### โŒ Dev Deployment Failed" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "Check the job log above for error details." >> "$GITHUB_STEP_SUMMARY" - fi diff --git a/.github/workflows/enterprise-firewall-setup.yml b/.github/workflows/enterprise-firewall-setup.yml deleted file mode 100644 index 1a533fb..0000000 --- a/.github/workflows/enterprise-firewall-setup.yml +++ /dev/null @@ -1,758 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# This file is part of a Moko Consulting project. -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -# FILE INFORMATION -# DEFGROUP: GitHub.Workflow -# INGROUP: MokoStandards.Firewall -# REPO: https://github.com/mokoconsulting-tech/MokoStandards -# PATH: /templates/workflows/shared/enterprise-firewall-setup.yml.template -# VERSION: 04.06.00 -# BRIEF: Enterprise firewall configuration โ€” generates outbound allow-rules including SFTP deployment server -# NOTE: Reads DEV_FTP_HOST / DEV_FTP_PORT variables to include SFTP egress rules alongside HTTPS rules. - -name: Enterprise Firewall Configuration - -# This workflow provides firewall configuration guidance for enterprise-ready sites -# It generates firewall rules for allowing outbound access to trusted domains -# including license providers, documentation sources, package registries, -# and the SFTP deployment server (DEV_FTP_HOST / DEV_FTP_PORT). -# -# Runs automatically when: -# - Coding agent workflows are triggered (pull requests with copilot/ prefix) -# - Manual workflow dispatch for custom configurations - -on: - workflow_dispatch: - inputs: - firewall_type: - description: 'Target firewall type' - required: true - type: choice - options: - - 'iptables' - - 'ufw' - - 'firewalld' - - 'aws-security-group' - - 'azure-nsg' - - 'gcp-firewall' - - 'cloudflare' - - 'all' - default: 'all' - output_format: - description: 'Output format' - required: true - type: choice - options: - - 'shell-script' - - 'json' - - 'yaml' - - 'markdown' - - 'all' - default: 'markdown' - - # Auto-run when coding agent creates or updates PRs - pull_request: - branches: - - 'copilot/**' - - 'agent/**' - types: [opened, synchronize, reopened] - - # Auto-run on push to coding agent branches - push: - branches: - - 'copilot/**' - - 'agent/**' - -permissions: - contents: read - actions: read - -jobs: - generate-firewall-rules: - name: Generate Firewall Rules - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: '3.11' - - - name: Apply Firewall Rules to Runner (Auto-run only) - if: github.event_name != 'workflow_dispatch' - env: - DEV_FTP_HOST: ${{ vars.DEV_FTP_HOST }} - DEV_FTP_PORT: ${{ vars.DEV_FTP_PORT }} - run: | - echo "๐Ÿ”ฅ Applying firewall rules for coding agent environment..." - echo "" - echo "This step ensures the GitHub Actions runner can access trusted domains" - echo "including license providers, package registries, and documentation sources." - echo "" - - # Note: GitHub Actions runners are ephemeral and run in controlled environments - # This step documents what domains are being accessed during the workflow - # Actual firewall configuration is managed by GitHub - - cat > /tmp/trusted-domains.txt << 'EOF' - # Trusted domains for coding agent environment - # License Providers - www.gnu.org - opensource.org - choosealicense.com - spdx.org - creativecommons.org - apache.org - fsf.org - - # Documentation & Standards - semver.org - keepachangelog.com - conventionalcommits.org - - # GitHub & Related - github.com - api.github.com - docs.github.com - raw.githubusercontent.com - ghcr.io - - # Package Registries - npmjs.com - registry.npmjs.org - pypi.org - files.pythonhosted.org - packagist.org - repo.packagist.org - rubygems.org - - # Platform-Specific - joomla.org - downloads.joomla.org - docs.joomla.org - php.net - getcomposer.org - dolibarr.org - wiki.dolibarr.org - docs.dolibarr.org - - # Moko Consulting - mokoconsulting.tech - - # SFTP Deployment Server (DEV_FTP_HOST) - ${DEV_FTP_HOST:-} - - # Google Services - drive.google.com - docs.google.com - sheets.google.com - accounts.google.com - storage.googleapis.com - fonts.googleapis.com - fonts.gstatic.com - - # GitHub Extended - upload.github.com - objects.githubusercontent.com - user-images.githubusercontent.com - codeload.github.com - pkg.github.com - - # Developer Reference - developer.mozilla.org - stackoverflow.com - git-scm.com - - # CDN & Infrastructure - cdn.jsdelivr.net - unpkg.com - cdnjs.cloudflare.com - img.shields.io - - # Container Registries - hub.docker.com - registry-1.docker.io - - # CI & Code Quality - codecov.io - sonarcloud.io - - # Terraform & Infrastructure - registry.terraform.io - releases.hashicorp.com - checkpoint-api.hashicorp.com - EOF - - echo "โœ“ Trusted domains documented for this runner" - echo "โœ“ GitHub Actions runners have network access to these domains" - echo "" - - # Test connectivity to key domains - echo "Testing connectivity to key domains..." - for domain in "github.com" "www.gnu.org" "npmjs.com" "pypi.org"; do - if curl -s --max-time 3 -o /dev/null -w "%{http_code}" "https://$domain" | grep -q "200\|301\|302"; then - echo " โœ“ $domain is accessible" - else - echo " โš ๏ธ $domain connectivity check failed (may be expected)" - fi - done - - # Test SFTP server connectivity (TCP port check) - SFTP_HOST="${DEV_FTP_HOST:-}" - SFTP_PORT="${DEV_FTP_PORT:-22}" - if [ -n "$SFTP_HOST" ]; then - # Strip any embedded :port suffix - SFTP_HOST="${SFTP_HOST%%:*}" - echo "" - echo "Testing SFTP deployment server connectivity..." - if timeout 5 bash -c "echo >/dev/tcp/${SFTP_HOST}/${SFTP_PORT}" 2>/dev/null; then - echo " โœ“ SFTP server ${SFTP_HOST}:${SFTP_PORT} is reachable" - else - echo " โš ๏ธ SFTP server ${SFTP_HOST}:${SFTP_PORT} is not reachable from runner (firewall rule needed)" - fi - else - echo "" - echo " โ„น๏ธ DEV_FTP_HOST not configured โ€” skipping SFTP connectivity check" - fi - - - name: Generate Firewall Configuration - id: generate - env: - DEV_FTP_HOST: ${{ vars.DEV_FTP_HOST }} - DEV_FTP_PORT: ${{ vars.DEV_FTP_PORT }} - run: | - cat > generate_firewall_config.py << 'PYTHON_EOF' - #!/usr/bin/env python3 - """ - Enterprise Firewall Configuration Generator - - Generates firewall rules for enterprise-ready deployments allowing - access to trusted domains including license providers, documentation - sources, package registries, and platform-specific sites. - """ - - import json - import os - import yaml - import sys - from typing import List, Dict - - # SFTP deployment server from org variables - _sftp_host_raw = os.environ.get("DEV_FTP_HOST", "").strip() - _sftp_port = os.environ.get("DEV_FTP_PORT", "").strip() or "22" - # Strip embedded :port suffix if present - _sftp_host = _sftp_host_raw.split(":")[0] if _sftp_host_raw else "" - if ":" in _sftp_host_raw and not _sftp_port: - _sftp_port = _sftp_host_raw.split(":")[1] - - SFTP_HOST = _sftp_host - SFTP_PORT = int(_sftp_port) if _sftp_port.isdigit() else 22 - - # Trusted domains from .github/copilot.yml - TRUSTED_DOMAINS = { - "license_providers": [ - "www.gnu.org", - "opensource.org", - "choosealicense.com", - "spdx.org", - "creativecommons.org", - "apache.org", - "fsf.org", - ], - "documentation_standards": [ - "semver.org", - "keepachangelog.com", - "conventionalcommits.org", - ], - "github_related": [ - "github.com", - "api.github.com", - "docs.github.com", - "raw.githubusercontent.com", - "ghcr.io", - ], - "package_registries": [ - "npmjs.com", - "registry.npmjs.org", - "pypi.org", - "files.pythonhosted.org", - "packagist.org", - "repo.packagist.org", - "rubygems.org", - ], - "standards_organizations": [ - "json-schema.org", - "w3.org", - "ietf.org", - ], - "platform_specific": [ - "joomla.org", - "downloads.joomla.org", - "docs.joomla.org", - "php.net", - "getcomposer.org", - "dolibarr.org", - "wiki.dolibarr.org", - "docs.dolibarr.org", - ], - "moko_consulting": [ - "mokoconsulting.tech", - ], - "google_services": [ - "drive.google.com", - "docs.google.com", - "sheets.google.com", - "accounts.google.com", - "storage.googleapis.com", - "fonts.googleapis.com", - "fonts.gstatic.com", - ], - "github_extended": [ - "upload.github.com", - "objects.githubusercontent.com", - "user-images.githubusercontent.com", - "codeload.github.com", - "pkg.github.com", - ], - "developer_reference": [ - "developer.mozilla.org", - "stackoverflow.com", - "git-scm.com", - ], - "cdn_and_infrastructure": [ - "cdn.jsdelivr.net", - "unpkg.com", - "cdnjs.cloudflare.com", - "img.shields.io", - ], - "container_registries": [ - "hub.docker.com", - "registry-1.docker.io", - ], - "ci_code_quality": [ - "codecov.io", - "sonarcloud.io", - ], - "terraform_infrastructure": [ - "registry.terraform.io", - "releases.hashicorp.com", - "checkpoint-api.hashicorp.com", - ], - } - - # Inject SFTP deployment server as a separate category (port 22, not 443) - if SFTP_HOST: - TRUSTED_DOMAINS["sftp_deployment_server"] = [SFTP_HOST] - print(f"โ„น๏ธ SFTP deployment server: {SFTP_HOST}:{SFTP_PORT}") - - def generate_sftp_iptables_rules(host: str, port: int) -> str: - """Generate iptables rules specifically for SFTP egress""" - return ( - f"# Allow SFTP to deployment server {host}:{port}\n" - f"iptables -A OUTPUT -p tcp -d $(dig +short {host} | head -1)" - f" --dport {port} -j ACCEPT # SFTP deploy\n" - ) - - def generate_sftp_ufw_rules(host: str, port: int) -> str: - """Generate UFW rules for SFTP egress""" - return ( - f"# Allow SFTP to deployment server\n" - f"ufw allow out to $(dig +short {host} | head -1)" - f" port {port} proto tcp comment 'SFTP deploy to {host}'\n" - ) - - def generate_sftp_firewalld_rules(host: str, port: int) -> str: - """Generate firewalld rules for SFTP egress""" - return ( - f"# Allow SFTP to deployment server\n" - f"firewall-cmd --permanent --add-rich-rule='" - f"rule family=ipv4 destination address=$(dig +short {host} | head -1)" - f" port port={port} protocol=tcp accept' # SFTP deploy\n" - ) - - def generate_iptables_rules(domains: List[str]) -> str: - """Generate iptables firewall rules""" - rules = ["#!/bin/bash", "", "# Enterprise Firewall Rules - iptables", ""] - rules.append("# Allow outbound HTTPS to trusted domains") - rules.append("") - - for domain in domains: - rules.append(f"# Allow {domain}") - rules.append(f"iptables -A OUTPUT -p tcp -d $(dig +short {domain} | head -1) --dport 443 -j ACCEPT") - - rules.append("") - rules.append("# Allow DNS lookups") - rules.append("iptables -A OUTPUT -p udp --dport 53 -j ACCEPT") - rules.append("iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT") - - return "\n".join(rules) - - def generate_ufw_rules(domains: List[str]) -> str: - """Generate UFW firewall rules""" - rules = ["#!/bin/bash", "", "# Enterprise Firewall Rules - UFW", ""] - rules.append("# Allow outbound HTTPS to trusted domains") - rules.append("") - - for domain in domains: - rules.append(f"# Allow {domain}") - rules.append(f"ufw allow out to $(dig +short {domain} | head -1) port 443 proto tcp comment 'Allow {domain}'") - - rules.append("") - rules.append("# Allow DNS") - rules.append("ufw allow out 53/udp comment 'Allow DNS UDP'") - rules.append("ufw allow out 53/tcp comment 'Allow DNS TCP'") - - return "\n".join(rules) - - def generate_firewalld_rules(domains: List[str]) -> str: - """Generate firewalld rules""" - rules = ["#!/bin/bash", "", "# Enterprise Firewall Rules - firewalld", ""] - rules.append("# Add trusted domains to firewall") - rules.append("") - - for domain in domains: - rules.append(f"# Allow {domain}") - rules.append(f"firewall-cmd --permanent --add-rich-rule='rule family=ipv4 destination address=$(dig +short {domain} | head -1) port port=443 protocol=tcp accept'") - - rules.append("") - rules.append("# Reload firewall") - rules.append("firewall-cmd --reload") - - return "\n".join(rules) - - def generate_aws_security_group(domains: List[str]) -> Dict: - """Generate AWS Security Group rules (JSON format)""" - rules = { - "SecurityGroupRules": { - "Egress": [] - } - } - - for domain in domains: - rules["SecurityGroupRules"]["Egress"].append({ - "Description": f"Allow HTTPS to {domain}", - "IpProtocol": "tcp", - "FromPort": 443, - "ToPort": 443, - "CidrIp": "0.0.0.0/0", # In practice, resolve to specific IPs - "Tags": [{ - "Key": "Domain", - "Value": domain - }] - }) - - # Add DNS - rules["SecurityGroupRules"]["Egress"].append({ - "Description": "Allow DNS", - "IpProtocol": "udp", - "FromPort": 53, - "ToPort": 53, - "CidrIp": "0.0.0.0/0" - }) - - return rules - - def generate_markdown_documentation(domains_by_category: Dict[str, List[str]]) -> str: - """Generate markdown documentation""" - md = ["# Enterprise Firewall Configuration Guide", ""] - md.append("## Overview") - md.append("") - md.append("This document provides firewall configuration guidance for enterprise-ready deployments.") - md.append("It lists trusted domains that should be whitelisted for outbound access to ensure") - md.append("proper functionality of license validation, package management, and documentation access.") - md.append("") - - md.append("## Trusted Domains by Category") - md.append("") - - all_domains = [] - for category, domains in domains_by_category.items(): - category_name = category.replace("_", " ").title() - md.append(f"### {category_name}") - md.append("") - md.append("| Domain | Purpose |") - md.append("|--------|---------|") - - for domain in domains: - all_domains.append(domain) - purpose = get_domain_purpose(domain) - md.append(f"| `{domain}` | {purpose} |") - - md.append("") - - md.append("## Implementation Examples") - md.append("") - - md.append("### iptables Example") - md.append("") - md.append("```bash") - md.append("# Allow HTTPS to trusted domain") - md.append(f"iptables -A OUTPUT -p tcp -d $(dig +short {all_domains[0]}) --dport 443 -j ACCEPT") - md.append("```") - md.append("") - - md.append("### UFW Example") - md.append("") - md.append("```bash") - md.append("# Allow HTTPS to trusted domain") - md.append(f"ufw allow out to {all_domains[0]} port 443 proto tcp") - md.append("```") - md.append("") - - md.append("### AWS Security Group Example") - md.append("") - md.append("```json") - md.append("{") - md.append(' "IpPermissions": [{') - md.append(' "IpProtocol": "tcp",') - md.append(' "FromPort": 443,') - md.append(' "ToPort": 443,') - md.append(' "IpRanges": [{"CidrIp": "0.0.0.0/0", "Description": "HTTPS to trusted domains"}]') - md.append(" }]") - md.append("}") - md.append("```") - md.append("") - - md.append("## Ports Required") - md.append("") - md.append("| Port | Protocol | Purpose |") - md.append("|------|----------|---------|") - md.append("| 443 | TCP | HTTPS (secure web access) |") - md.append("| 80 | TCP | HTTP (redirects to HTTPS) |") - md.append("| 53 | UDP/TCP | DNS resolution |") - md.append("") - - md.append("## Security Considerations") - md.append("") - md.append("1. **DNS Resolution**: Ensure DNS queries are allowed (port 53 UDP/TCP)") - md.append("2. **Certificate Validation**: HTTPS requires ability to reach certificate authorities") - md.append("3. **Dynamic IPs**: Some domains use CDNs with dynamic IPs - consider using FQDNs in rules") - md.append("4. **Regular Updates**: Review and update whitelist as services change") - md.append("5. **Logging**: Enable logging for blocked connections to identify missing rules") - md.append("") - - md.append("## Compliance Notes") - md.append("") - md.append("- All listed domains provide read-only access to public information") - md.append("- License providers enable GPL compliance verification") - md.append("- Package registries support dependency security scanning") - md.append("- No authentication credentials are transmitted to these domains") - md.append("") - - return "\n".join(md) - - def get_domain_purpose(domain: str) -> str: - """Get human-readable purpose for a domain""" - purposes = { - "www.gnu.org": "GNU licenses and documentation", - "opensource.org": "Open Source Initiative resources", - "choosealicense.com": "GitHub license selection tool", - "spdx.org": "Software Package Data Exchange identifiers", - "creativecommons.org": "Creative Commons licenses", - "apache.org": "Apache Software Foundation licenses", - "fsf.org": "Free Software Foundation resources", - "semver.org": "Semantic versioning specification", - "keepachangelog.com": "Changelog format standards", - "conventionalcommits.org": "Commit message conventions", - "github.com": "GitHub platform access", - "api.github.com": "GitHub API access", - "docs.github.com": "GitHub documentation", - "raw.githubusercontent.com": "GitHub raw content access", - "npmjs.com": "npm package registry", - "pypi.org": "Python Package Index", - "packagist.org": "PHP Composer package registry", - "rubygems.org": "Ruby gems registry", - "joomla.org": "Joomla CMS platform", - "php.net": "PHP documentation and downloads", - "dolibarr.org": "Dolibarr ERP/CRM platform", - } - return purposes.get(domain, "Trusted resource") - - def main(): - # Use inputs if provided (manual dispatch), otherwise use defaults (auto-run) - firewall_type = "${{ github.event.inputs.firewall_type }}" or "all" - output_format = "${{ github.event.inputs.output_format }}" or "markdown" - - print(f"Running in {'manual' if '${{ github.event.inputs.firewall_type }}' else 'automatic'} mode") - print(f"Firewall type: {firewall_type}") - print(f"Output format: {output_format}") - print("") - - # Collect all domains - all_domains = [] - for domains in TRUSTED_DOMAINS.values(): - all_domains.extend(domains) - - # Remove duplicates and sort - all_domains = sorted(set(all_domains)) - - print(f"Generating firewall rules for {len(all_domains)} trusted domains...") - print("") - - # Exclude SFTP server from HTTPS rule generation (different port) - https_domains = [d for d in all_domains if d != SFTP_HOST] - - # Generate based on firewall type - if firewall_type in ["iptables", "all"]: - rules = generate_iptables_rules(https_domains) - if SFTP_HOST: - rules += "\n# โ”€โ”€ SFTP Deployment Server โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n" - rules += generate_sftp_iptables_rules(SFTP_HOST, SFTP_PORT) - with open("firewall-rules-iptables.sh", "w") as f: - f.write(rules) - print("โœ“ Generated iptables rules: firewall-rules-iptables.sh") - - if firewall_type in ["ufw", "all"]: - rules = generate_ufw_rules(https_domains) - if SFTP_HOST: - rules += "\n# โ”€โ”€ SFTP Deployment Server โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n" - rules += generate_sftp_ufw_rules(SFTP_HOST, SFTP_PORT) - with open("firewall-rules-ufw.sh", "w") as f: - f.write(rules) - print("โœ“ Generated UFW rules: firewall-rules-ufw.sh") - - if firewall_type in ["firewalld", "all"]: - rules = generate_firewalld_rules(https_domains) - if SFTP_HOST: - rules += "\n# โ”€โ”€ SFTP Deployment Server โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n" - rules += generate_sftp_firewalld_rules(SFTP_HOST, SFTP_PORT) - with open("firewall-rules-firewalld.sh", "w") as f: - f.write(rules) - print("โœ“ Generated firewalld rules: firewall-rules-firewalld.sh") - - if firewall_type in ["aws-security-group", "all"]: - rules = generate_aws_security_group(all_domains) - with open("firewall-rules-aws-sg.json", "w") as f: - json.dump(rules, f, indent=2) - print("โœ“ Generated AWS Security Group rules: firewall-rules-aws-sg.json") - - if output_format in ["yaml", "all"]: - with open("trusted-domains.yml", "w") as f: - yaml.dump(TRUSTED_DOMAINS, f, default_flow_style=False) - print("โœ“ Generated YAML domain list: trusted-domains.yml") - - if output_format in ["json", "all"]: - with open("trusted-domains.json", "w") as f: - json.dump(TRUSTED_DOMAINS, f, indent=2) - print("โœ“ Generated JSON domain list: trusted-domains.json") - - if output_format in ["markdown", "all"]: - md = generate_markdown_documentation(TRUSTED_DOMAINS) - with open("FIREWALL_CONFIGURATION.md", "w") as f: - f.write(md) - print("โœ“ Generated documentation: FIREWALL_CONFIGURATION.md") - - print("") - print("Domain Categories:") - for category, domains in TRUSTED_DOMAINS.items(): - print(f" - {category}: {len(domains)} domains") - - print("") - print("Total unique domains: ", len(all_domains)) - - if __name__ == "__main__": - main() - PYTHON_EOF - - chmod +x generate_firewall_config.py - pip install PyYAML - python3 generate_firewall_config.py - - - name: Upload Firewall Configuration Artifacts - uses: actions/upload-artifact@v6 - with: - name: firewall-configurations - path: | - firewall-rules-*.sh - firewall-rules-*.json - trusted-domains.* - FIREWALL_CONFIGURATION.md - retention-days: 90 - - - name: Display Summary - run: | - echo "## Firewall Configuration" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - echo "**Mode**: Manual Execution" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Firewall rules have been generated for enterprise-ready deployments." >> $GITHUB_STEP_SUMMARY - else - echo "**Mode**: Automatic Execution (Coding Agent Active)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "This workflow ran automatically because a coding agent (GitHub Copilot) is active." >> $GITHUB_STEP_SUMMARY - echo "Firewall configuration has been validated for the coding agent environment." >> $GITHUB_STEP_SUMMARY - fi - - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Files Generated" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - if ls firewall-rules-* trusted-domains.* FIREWALL_CONFIGURATION.md 2>/dev/null; then - ls -lh firewall-rules-* trusted-domains.* FIREWALL_CONFIGURATION.md 2>/dev/null | awk '{print "- " $9 " (" $5 ")"}' >> $GITHUB_STEP_SUMMARY - else - echo "- Documentation generated" >> $GITHUB_STEP_SUMMARY - fi - echo "" >> $GITHUB_STEP_SUMMARY - - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - echo "### Download Artifacts" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Download the generated firewall configurations from the workflow artifacts." >> $GITHUB_STEP_SUMMARY - else - echo "### Trusted Domains Active" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "The coding agent has access to:" >> $GITHUB_STEP_SUMMARY - echo "- License providers (GPL, OSI, SPDX, Apache, etc.)" >> $GITHUB_STEP_SUMMARY - echo "- Package registries (npm, PyPI, Packagist, RubyGems)" >> $GITHUB_STEP_SUMMARY - echo "- Documentation sources (GitHub, Joomla, Dolibarr, PHP)" >> $GITHUB_STEP_SUMMARY - echo "- Standards organizations (W3C, IETF, JSON Schema)" >> $GITHUB_STEP_SUMMARY - fi - -# Usage Instructions: -# -# This workflow runs in two modes: -# -# 1. AUTOMATIC MODE (Coding Agent): -# - Triggers when coding agent branches (copilot/**, agent/**) are pushed or PR'd -# - Validates firewall configuration for the coding agent environment -# - Documents accessible domains for compliance -# - Ensures license sources and package registries are available -# -# 2. MANUAL MODE (Enterprise Configuration): -# - Manually trigger from the Actions tab -# - Select desired firewall type and output format -# - Download generated artifacts -# - Apply firewall rules to your enterprise environment -# -# Configuration: -# - Trusted domains are sourced from .github/copilot.yml -# - Modify copilot.yml to add/remove trusted domains -# - Changes automatically propagate to firewall rules -# -# Important Notes: -# - Review generated rules before applying to production -# - Some domains may use CDNs with dynamic IPs -# - Consider using FQDN-based rules where supported -# - Test thoroughly in staging environment first -# - Monitor logs for blocked connections -# - Update rules as domains/services change diff --git a/.github/workflows/repository-cleanup.yml b/.github/workflows/repository-cleanup.yml deleted file mode 100644 index 96c2a8c..0000000 --- a/.github/workflows/repository-cleanup.yml +++ /dev/null @@ -1,525 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# This file is part of a Moko Consulting project. -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: GitHub.Workflow -# INGROUP: MokoStandards.Maintenance -# REPO: https://github.com/mokoconsulting-tech/MokoStandards -# PATH: /templates/workflows/shared/repository-cleanup.yml.template -# VERSION: 04.06.00 -# BRIEF: Recurring repository maintenance โ€” labels, branches, workflows, logs, doc indexes -# NOTE: Synced via bulk-repo-sync to .github/workflows/repository-cleanup.yml in all governed repos. -# Runs on the 1st and 15th of each month at 6:00 AM UTC, and on manual dispatch. - -name: Repository Cleanup - -on: - schedule: - - cron: '0 6 1,15 * *' - workflow_dispatch: - inputs: - reset_labels: - description: 'Delete ALL existing labels and recreate the standard set' - type: boolean - default: false - clean_branches: - description: 'Delete old chore/sync-mokostandards-* branches' - type: boolean - default: true - clean_workflows: - description: 'Delete orphaned workflow runs (cancelled, stale)' - type: boolean - default: true - clean_logs: - description: 'Delete workflow run logs older than 30 days' - type: boolean - default: true - fix_templates: - description: 'Strip copyright comment blocks from issue templates' - type: boolean - default: true - rebuild_indexes: - description: 'Rebuild docs/ index files' - type: boolean - default: true - delete_closed_issues: - description: 'Delete issues that have been closed for more than 30 days' - type: boolean - default: false - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -permissions: - contents: write - issues: write - actions: write - -jobs: - cleanup: - name: Repository Maintenance - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.GH_TOKEN || github.token }} - fetch-depth: 0 - - - name: Check actor permission - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - run: | - ACTOR="${{ github.actor }}" - # Schedule triggers use github-actions[bot] - if [ "${{ github.event_name }}" = "schedule" ]; then - echo "โœ… Scheduled run โ€” authorized" - exit 0 - fi - AUTHORIZED_USERS="jmiller github-actions[bot]" - for user in $AUTHORIZED_USERS; do - if [ "$ACTOR" = "$user" ]; then - echo "โœ… ${ACTOR} authorized" - exit 0 - fi - done - PERMISSION=$(gh api "repos/${{ github.repository }}/collaborators/${ACTOR}/permission" \ - --jq '.permission' 2>/dev/null) - case "$PERMISSION" in - admin|maintain) echo "โœ… ${ACTOR} has ${PERMISSION}" ;; - *) echo "โŒ Admin or maintain required"; exit 1 ;; - esac - - # โ”€โ”€ Determine which tasks to run โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - # On schedule: run all tasks with safe defaults (labels NOT reset) - # On dispatch: use input toggles - - name: Set task flags - id: tasks - run: | - if [ "${{ github.event_name }}" = "schedule" ]; then - echo "reset_labels=false" >> $GITHUB_OUTPUT - echo "clean_branches=true" >> $GITHUB_OUTPUT - echo "clean_workflows=true" >> $GITHUB_OUTPUT - echo "clean_logs=true" >> $GITHUB_OUTPUT - echo "fix_templates=true" >> $GITHUB_OUTPUT - echo "rebuild_indexes=true" >> $GITHUB_OUTPUT - echo "delete_closed_issues=false" >> $GITHUB_OUTPUT - else - echo "reset_labels=${{ inputs.reset_labels }}" >> $GITHUB_OUTPUT - echo "clean_branches=${{ inputs.clean_branches }}" >> $GITHUB_OUTPUT - echo "clean_workflows=${{ inputs.clean_workflows }}" >> $GITHUB_OUTPUT - echo "clean_logs=${{ inputs.clean_logs }}" >> $GITHUB_OUTPUT - echo "fix_templates=${{ inputs.fix_templates }}" >> $GITHUB_OUTPUT - echo "rebuild_indexes=${{ inputs.rebuild_indexes }}" >> $GITHUB_OUTPUT - echo "delete_closed_issues=${{ inputs.delete_closed_issues }}" >> $GITHUB_OUTPUT - fi - - # โ”€โ”€ DELETE RETIRED WORKFLOWS (always runs) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - - name: Delete retired workflow files - run: | - echo "## ๐Ÿ—‘๏ธ Retired Workflow Cleanup" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - RETIRED=( - ".github/workflows/build.yml" - ".github/workflows/code-quality.yml" - ".github/workflows/release-cycle.yml" - ".github/workflows/release-pipeline.yml" - ".github/workflows/branch-cleanup.yml" - ".github/workflows/auto-update-changelog.yml" - ".github/workflows/enterprise-issue-manager.yml" - ".github/workflows/flush-actions-cache.yml" - ".github/workflows/mokostandards-script-runner.yml" - ".github/workflows/unified-ci.yml" - ".github/workflows/unified-platform-testing.yml" - ".github/workflows/reusable-build.yml" - ".github/workflows/reusable-ci-validation.yml" - ".github/workflows/reusable-deploy.yml" - ".github/workflows/reusable-php-quality.yml" - ".github/workflows/reusable-platform-testing.yml" - ".github/workflows/reusable-project-detector.yml" - ".github/workflows/reusable-release.yml" - ".github/workflows/reusable-script-executor.yml" - ".github/workflows/rebuild-docs-indexes.yml" - ".github/workflows/setup-project-v2.yml" - ".github/workflows/sync-docs-to-project.yml" - ".github/workflows/release.yml" - ".github/workflows/sync-changelogs.yml" - ".github/workflows/version_branch.yml" - "update.json" - ".github/workflows/auto-version-branch.yml" - ".github/workflows/publish-to-mokodolibarr.yml" - ".github/workflows/ci.yml" - ".github/workflows/deploy-rs.yml" - "sftp-config.json" - "sftp-config.json.template" - "scripts/sftp-config" - ) - - DELETED=0 - for wf in "${RETIRED[@]}"; do - if [ -f "$wf" ]; then - git rm "$wf" 2>/dev/null || rm -f "$wf" - echo " Deleted: \`$(basename $wf)\`" >> $GITHUB_STEP_SUMMARY - DELETED=$((DELETED+1)) - fi - done - - if [ "$DELETED" -gt 0 ]; then - git config --local user.email "github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" - git add -A - git commit -m "chore: delete ${DELETED} retired workflow file(s) [skip ci]" \ - --author="github-actions[bot] " - git push - echo "โœ… ${DELETED} retired workflow(s) deleted" >> $GITHUB_STEP_SUMMARY - else - echo "โœ… No retired workflows found" >> $GITHUB_STEP_SUMMARY - fi - - # โ”€โ”€ LABEL RESET โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - - name: Reset labels to standard set - if: steps.tasks.outputs.reset_labels == 'true' - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - run: | - REPO="${{ github.repository }}" - echo "## ๐Ÿท๏ธ Label Reset" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - gh api "repos/${REPO}/labels?per_page=100" --paginate --jq '.[].name' | while read -r label; do - ENCODED=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$label', safe=''))") - gh api -X DELETE "repos/${REPO}/labels/${ENCODED}" --silent 2>/dev/null || true - done - - while IFS='|' read -r name color description; do - [ -z "$name" ] && continue - gh api "repos/${REPO}/labels" \ - -f name="$name" -f color="$color" -f description="$description" \ - --silent 2>/dev/null || true - done << 'LABELS' - joomla|7F52FF|Joomla extension or component - dolibarr|FF6B6B|Dolibarr module or extension - generic|808080|Generic project or library - php|4F5D95|PHP code changes - javascript|F7DF1E|JavaScript code changes - typescript|3178C6|TypeScript code changes - python|3776AB|Python code changes - css|1572B6|CSS/styling changes - html|E34F26|HTML template changes - documentation|0075CA|Documentation changes - ci-cd|000000|CI/CD pipeline changes - docker|2496ED|Docker configuration changes - tests|00FF00|Test suite changes - security|FF0000|Security-related changes - dependencies|0366D6|Dependency updates - config|F9D0C4|Configuration file changes - build|FFA500|Build system changes - automation|8B4513|Automated processes or scripts - mokostandards|B60205|MokoStandards compliance - needs-review|FBCA04|Awaiting code review - work-in-progress|D93F0B|Work in progress, not ready for merge - breaking-change|D73A4A|Breaking API or functionality change - priority: critical|B60205|Critical priority, must be addressed immediately - priority: high|D93F0B|High priority - priority: medium|FBCA04|Medium priority - priority: low|0E8A16|Low priority - type: bug|D73A4A|Something isn't working - type: feature|A2EEEF|New feature or request - type: enhancement|84B6EB|Enhancement to existing feature - type: refactor|F9D0C4|Code refactoring - type: chore|FEF2C0|Maintenance tasks - type: version|0E8A16|Version-related change - status: pending|FBCA04|Pending action or decision - status: in-progress|0E8A16|Currently being worked on - status: blocked|B60205|Blocked by another issue or dependency - status: on-hold|D4C5F9|Temporarily on hold - status: wontfix|FFFFFF|This will not be worked on - size/xs|C5DEF5|Extra small change (1-10 lines) - size/s|6FD1E2|Small change (11-30 lines) - size/m|F9DD72|Medium change (31-100 lines) - size/l|FFA07A|Large change (101-300 lines) - size/xl|FF6B6B|Extra large change (301-1000 lines) - size/xxl|B60205|Extremely large change (1000+ lines) - health: excellent|0E8A16|Health score 90-100 - health: good|FBCA04|Health score 70-89 - health: fair|FFA500|Health score 50-69 - health: poor|FF6B6B|Health score below 50 - standards-update|B60205|MokoStandards sync update - standards-drift|FBCA04|Repository drifted from MokoStandards - sync-report|0075CA|Bulk sync run report - sync-failure|D73A4A|Bulk sync failure requiring attention - push-failure|D73A4A|File push failure requiring attention - health-check|0E8A16|Repository health check results - version-drift|FFA500|Version mismatch detected - deploy-failure|CC0000|Automated deploy failure tracking - template-validation-failure|D73A4A|Template workflow validation failure - version|0E8A16|Version bump or release - LABELS - - echo "โœ… Standard labels created" >> $GITHUB_STEP_SUMMARY - - # โ”€โ”€ BRANCH CLEANUP โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - - name: Delete old sync branches - if: steps.tasks.outputs.clean_branches == 'true' - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - run: | - REPO="${{ github.repository }}" - CURRENT="chore/sync-mokostandards-v04.05" - echo "## ๐ŸŒฟ Branch Cleanup" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - FOUND=false - gh api "repos/${REPO}/branches?per_page=100" --jq '.[].name' | \ - grep "^chore/sync-mokostandards" | \ - grep -v "^${CURRENT}$" | while read -r branch; do - gh pr list --repo "$REPO" --head "$branch" --state open --json number --jq '.[].number' 2>/dev/null | while read -r pr; do - gh pr close "$pr" --repo "$REPO" --comment "Superseded by \`${CURRENT}\`" 2>/dev/null || true - echo " Closed PR #${pr}" >> $GITHUB_STEP_SUMMARY - done - gh api -X DELETE "repos/${REPO}/git/refs/heads/${branch}" --silent 2>/dev/null || true - echo " Deleted: \`${branch}\`" >> $GITHUB_STEP_SUMMARY - FOUND=true - done - - if [ "$FOUND" != "true" ]; then - echo "โœ… No old sync branches found" >> $GITHUB_STEP_SUMMARY - fi - - # โ”€โ”€ WORKFLOW RUN CLEANUP โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - - name: Clean up workflow runs - if: steps.tasks.outputs.clean_workflows == 'true' - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - run: | - REPO="${{ github.repository }}" - echo "## ๐Ÿ”„ Workflow Run Cleanup" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - DELETED=0 - # Delete cancelled and stale workflow runs - for status in cancelled stale; do - gh api "repos/${REPO}/actions/runs?status=${status}&per_page=100" \ - --jq '.workflow_runs[].id' 2>/dev/null | while read -r run_id; do - gh api -X DELETE "repos/${REPO}/actions/runs/${run_id}" --silent 2>/dev/null || true - DELETED=$((DELETED+1)) - done - done - - echo "โœ… Cleaned cancelled/stale workflow runs" >> $GITHUB_STEP_SUMMARY - - # โ”€โ”€ LOG CLEANUP โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - - name: Delete old workflow run logs - if: steps.tasks.outputs.clean_logs == 'true' - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - run: | - REPO="${{ github.repository }}" - CUTOFF=$(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-30d +%Y-%m-%dT%H:%M:%SZ) - echo "## ๐Ÿ“‹ Log Cleanup" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Deleting logs older than: ${CUTOFF}" >> $GITHUB_STEP_SUMMARY - - DELETED=0 - gh api "repos/${REPO}/actions/runs?created=<${CUTOFF}&per_page=100" \ - --jq '.workflow_runs[].id' 2>/dev/null | while read -r run_id; do - gh api -X DELETE "repos/${REPO}/actions/runs/${run_id}/logs" --silent 2>/dev/null || true - DELETED=$((DELETED+1)) - done - - echo "โœ… Cleaned old workflow run logs" >> $GITHUB_STEP_SUMMARY - - # โ”€โ”€ ISSUE TEMPLATE FIX โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - - name: Strip copyright headers from issue templates - if: steps.tasks.outputs.fix_templates == 'true' - run: | - echo "## ๐Ÿ“‹ Issue Template Cleanup" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - FIXED=0 - for f in .github/ISSUE_TEMPLATE/*.md; do - [ -f "$f" ] || continue - if grep -q '^$/d' "$f" - echo " Cleaned: \`$(basename $f)\`" >> $GITHUB_STEP_SUMMARY - FIXED=$((FIXED+1)) - fi - done - - if [ "$FIXED" -gt 0 ]; then - git config --local user.email "github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" - git add .github/ISSUE_TEMPLATE/ - git commit -m "fix: strip copyright comment blocks from issue templates [skip ci]" \ - --author="github-actions[bot] " - git push - echo "โœ… ${FIXED} template(s) cleaned and committed" >> $GITHUB_STEP_SUMMARY - else - echo "โœ… No templates need cleaning" >> $GITHUB_STEP_SUMMARY - fi - - # โ”€โ”€ REBUILD DOC INDEXES โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - - name: Rebuild docs/ index files - if: steps.tasks.outputs.rebuild_indexes == 'true' - run: | - echo "## ๐Ÿ“š Documentation Index Rebuild" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ ! -d "docs" ]; then - echo "โญ๏ธ No docs/ directory โ€” skipping" >> $GITHUB_STEP_SUMMARY - exit 0 - fi - - UPDATED=0 - # Generate index.md for each docs/ subdirectory - find docs -type d | while read -r dir; do - INDEX="${dir}/index.md" - FILES=$(find "$dir" -maxdepth 1 -name "*.md" ! -name "index.md" -printf "- [%f](./%f)\n" 2>/dev/null | sort) - if [ -z "$FILES" ]; then - continue - fi - - cat > "$INDEX" << INDEXEOF - # $(basename "$dir") - - ## Documents - - ${FILES} - - --- - *Auto-generated by repository-cleanup workflow* - INDEXEOF - # Dedent - sed -i 's/^ //' "$INDEX" - UPDATED=$((UPDATED+1)) - done - - if [ "$UPDATED" -gt 0 ]; then - git config --local user.email "github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" - git add docs/ - if ! git diff --cached --quiet; then - git commit -m "docs: rebuild documentation indexes [skip ci]" \ - --author="github-actions[bot] " - git push - echo "โœ… ${UPDATED} index file(s) rebuilt and committed" >> $GITHUB_STEP_SUMMARY - else - echo "โœ… All indexes already up to date" >> $GITHUB_STEP_SUMMARY - fi - else - echo "โœ… No indexes to rebuild" >> $GITHUB_STEP_SUMMARY - fi - - # โ”€โ”€ VERSION DRIFT DETECTION โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - - name: Check for version drift - run: | - echo "## ๐Ÿ“ฆ Version Drift Check" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ ! -f "README.md" ]; then - echo "โญ๏ธ No README.md โ€” skipping" >> $GITHUB_STEP_SUMMARY - exit 0 - fi - - README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md 2>/dev/null | head -1) - if [ -z "$README_VERSION" ]; then - echo "โš ๏ธ No VERSION found in README.md FILE INFORMATION block" >> $GITHUB_STEP_SUMMARY - exit 0 - fi - - echo "**README version:** \`${README_VERSION}\`" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - DRIFT=0 - CHECKED=0 - - # Check all files with FILE INFORMATION blocks - while IFS= read -r -d '' file; do - FILE_VERSION=$(grep -oP '^\s*\*?\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' "$file" 2>/dev/null | head -1) - [ -z "$FILE_VERSION" ] && continue - CHECKED=$((CHECKED+1)) - if [ "$FILE_VERSION" != "$README_VERSION" ]; then - echo " โš ๏ธ \`${file}\`: \`${FILE_VERSION}\` (expected \`${README_VERSION}\`)" >> $GITHUB_STEP_SUMMARY - DRIFT=$((DRIFT+1)) - fi - done < <(find . -maxdepth 4 -type f \( -name "*.php" -o -name "*.md" -o -name "*.yml" \) ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" -print0 2>/dev/null) - - echo "" >> $GITHUB_STEP_SUMMARY - if [ "$DRIFT" -gt 0 ]; then - echo "โš ๏ธ **${DRIFT}** file(s) out of ${CHECKED} have version drift" >> $GITHUB_STEP_SUMMARY - echo "Run \`sync-version-on-merge\` workflow or update manually" >> $GITHUB_STEP_SUMMARY - else - echo "โœ… All ${CHECKED} file(s) match README version \`${README_VERSION}\`" >> $GITHUB_STEP_SUMMARY - fi - - # โ”€โ”€ PROTECT CUSTOM WORKFLOWS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - - name: Ensure custom workflow directory exists - run: | - echo "## ๐Ÿ”ง Custom Workflows" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ ! -d ".github/workflows/custom" ]; then - mkdir -p .github/workflows/custom - cat > .github/workflows/custom/README.md << 'CWEOF' - # Custom Workflows - - Place repo-specific workflows here. Files in this directory are: - - **Never overwritten** by MokoStandards bulk sync - - **Never deleted** by the repository-cleanup workflow - - Safe for custom CI, notifications, or repo-specific automation - - Synced workflows live in `.github/workflows/` (parent directory). - CWEOF - sed -i 's/^ //' .github/workflows/custom/README.md - git config --local user.email "github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" - git add .github/workflows/custom/ - if ! git diff --cached --quiet; then - git commit -m "chore: create .github/workflows/custom/ for repo-specific workflows [skip ci]" \ - --author="github-actions[bot] " - git push - echo "โœ… Created \`.github/workflows/custom/\` directory" >> $GITHUB_STEP_SUMMARY - fi - else - CUSTOM_COUNT=$(find .github/workflows/custom -name "*.yml" -o -name "*.yaml" 2>/dev/null | wc -l) - echo "โœ… Custom workflow directory exists (${CUSTOM_COUNT} workflow(s))" >> $GITHUB_STEP_SUMMARY - fi - - # โ”€โ”€ DELETE CLOSED ISSUES โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - - name: Delete old closed issues - if: steps.tasks.outputs.delete_closed_issues == 'true' - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - run: | - REPO="${{ github.repository }}" - CUTOFF=$(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-30d +%Y-%m-%dT%H:%M:%SZ) - echo "## ๐Ÿ—‘๏ธ Closed Issue Cleanup" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Deleting issues closed before: ${CUTOFF}" >> $GITHUB_STEP_SUMMARY - - DELETED=0 - gh api "repos/${REPO}/issues?state=closed&since=1970-01-01T00:00:00Z&per_page=100&sort=updated&direction=asc" \ - --jq ".[] | select(.closed_at < \"${CUTOFF}\") | .number" 2>/dev/null | while read -r num; do - # Lock and close with "not_planned" to mark as cleaned up - gh api "repos/${REPO}/issues/${num}/lock" -X PUT -f lock_reason="resolved" --silent 2>/dev/null || true - echo " Locked issue #${num}" >> $GITHUB_STEP_SUMMARY - DELETED=$((DELETED+1)) - done - - if [ "$DELETED" -eq 0 ] 2>/dev/null; then - echo "โœ… No old closed issues found" >> $GITHUB_STEP_SUMMARY - else - echo "โœ… Locked ${DELETED} old closed issue(s)" >> $GITHUB_STEP_SUMMARY - fi - - - name: Summary - if: always() - run: | - echo "" >> $GITHUB_STEP_SUMMARY - echo "---" >> $GITHUB_STEP_SUMMARY - echo "*Run by @${{ github.actor }} โ€” trigger: ${{ github.event_name }}*" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/standards-compliance.yml b/.github/workflows/standards-compliance.yml deleted file mode 100644 index 44ab47d..0000000 --- a/.github/workflows/standards-compliance.yml +++ /dev/null @@ -1,2614 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# SPDX-License-Identifier: GPL-3.0-or-later -# FILE INFORMATION -# DEFGROUP: GitHub.Workflow -# INGROUP: MokoStandards.Compliance -# REPO: https://github.com/mokoconsulting-tech/MokoStandards -# PATH: /.github/workflows/standards-compliance.yml -# VERSION: 04.06.00 -# BRIEF: MokoStandards compliance validation workflow -# NOTE: Validates repository structure, documentation, and coding standards - -name: Standards Compliance - -# โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— -# โ•‘ MOKOSTANDARDS COMPLIANCE WORKFLOW โ•‘ -# โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ -# โ•‘ โ•‘ -# โ•‘ 28 checks across 4 priority tiers: โ•‘ -# โ•‘ โ•‘ -# โ•‘ TIER 1 โ€” CRITICAL (must pass) โ•‘ -# โ•‘ secret-scanning, license-compliance, repository-structure, โ•‘ -# โ•‘ coding-standards, version-consistency โ•‘ -# โ•‘ โ•‘ -# โ•‘ TIER 2 โ€” IMPORTANT (should pass) โ•‘ -# โ•‘ workflow-validation, documentation-quality, readme-completeness, โ•‘ -# โ•‘ git-hygiene, script-integrity โ•‘ -# โ•‘ โ•‘ -# โ•‘ TIER 3 โ€” QUALITY (code metrics) โ•‘ -# โ•‘ line-length, file-naming, insecure-patterns, complexity, โ•‘ -# โ•‘ duplication, dead-code โ•‘ -# โ•‘ โ•‘ -# โ•‘ TIER 4 โ€” SUPPLEMENTARY (informational) โ•‘ -# โ•‘ file-size, binary, todo, deps, links, api-docs, accessibility, โ•‘ -# โ•‘ performance, enterprise, health, terraform โ•‘ -# โ•‘ โ•‘ -# โ•‘ File size: warning >15MB, critical >20MB โ•‘ -# โ•‘ Exempt: .mmdb, .woff2, .woff, .ttf, .otf โ•‘ -# โ•‘ โ•‘ -# โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - -env: - WORKFLOW_VERSION: "04.04.01" - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -# MokoStandards Policy Compliance: -# - File formatting: Enforces organizational coding standards -# - Reference: docs/policy/file-formatting.md - -# โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -# โ”‚ WORKFLOW FLOW DIAGRAM โ”‚ -# โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -# -# TRIGGER: Push/PR to main/dev/rc branches -# โ”‚ -# โ–ผ -# โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -# โ”‚ PARALLEL VALIDATION CHECKS โ”‚ -# โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -# โ”‚ -# โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -# โ–ผ โ–ผ โ–ผ โ–ผ โ–ผ -# โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -# โ”‚Repository โ”‚File Header โ”‚Code Styleโ”‚ โ”‚ Docs โ”‚ โ”‚ License โ”‚ -# โ”‚Structureโ”‚ โ”‚ Validationโ”‚ โ”‚ Check โ”‚ โ”‚ Check โ”‚ โ”‚ Check โ”‚ -# โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -# โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ -# โ–ผ โ–ผ โ–ผ โ–ผ โ–ผ -# โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -# โ”‚ Check โ”‚ โ”‚ Verify โ”‚ โ”‚ Run โ”‚ โ”‚ Check โ”‚ โ”‚ Verify โ”‚ -# โ”‚Required โ”‚ โ”‚Copyright โ”‚ โ”‚ Linters โ”‚ โ”‚README โ”‚ โ”‚SPDX-ID โ”‚ -# โ”‚ Dirs โ”‚ โ”‚ Header โ”‚ โ”‚(Python, โ”‚ โ”‚ Exists โ”‚ โ”‚ Present โ”‚ -# โ”‚ โ”‚ โ”‚ Format โ”‚ โ”‚PHP,YAML) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ -# โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -# โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ -# โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -# โ”‚ -# โ–ผ -# โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -# โ”‚ All Checks Pass?โ”‚ -# โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -# โ”‚ โ”‚ -# YES โ”‚ โ”‚ NO -# โ–ผ โ–ผ -# โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -# โ”‚ SUCCESS โ”‚ โ”‚ CREATE ISSUE โ”‚ -# โ”‚ Summary โ”‚ โ”‚ with Failure โ”‚ -# โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ Details โ”‚ -# โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - -on: - push: - branches: [main, dev/**, rc/**, version/**] - pull_request: - branches: [main, dev/**, rc/**] - workflow_dispatch: - -permissions: - contents: read - pull-requests: write - issues: write - -jobs: - # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - # TIER 1 โ€” CRITICAL (must pass, blocks merge) - # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - secret-scanning: - name: Secret Scanning - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Scan for Secrets - run: | - set -x - echo "## ๐Ÿ”’ Secret Scanning" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Scanning for hardcoded secrets and credentials." >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Define secret patterns - VIOLATIONS=0 - - # Check for common secret patterns - echo "### Secret Patterns" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Helper: scan with a pattern, show results with file:line, return count - scan_pattern() { - local label="$1" icon="$2" tmpfile="$3" - local count=0 - if [ -f "$tmpfile" ]; then - count=$(wc -l < "$tmpfile") - fi - if [ "$count" -gt 0 ]; then - echo "${icon} **${label}**: ${count} finding(s)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "View locations" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| File | Line | Match |" >> $GITHUB_STEP_SUMMARY - echo "|------|------|-------|" >> $GITHUB_STEP_SUMMARY - head -20 "$tmpfile" | while IFS= read -r line; do - FILE=$(echo "$line" | cut -d: -f1 | sed 's|^\./||') - LINENO=$(echo "$line" | cut -d: -f2) - MATCH=$(echo "$line" | cut -d: -f3- | head -c 80 | sed 's/|/\\|/g') - echo "| \`${FILE}\` | ${LINENO} | \`${MATCH}\` |" >> $GITHUB_STEP_SUMMARY - done - if [ "$count" -gt 20 ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "*... and $((count - 20)) more*" >> $GITHUB_STEP_SUMMARY - fi - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - VIOLATIONS=$((VIOLATIONS + count)) - fi - } - - # Pattern 1: password/secret assignments - grep -r -n -E "(password|passwd|pwd|secret|api[_-]?key|token).*=.*['\"]" . \ - --include="*.php" --include="*.py" --include="*.js" --include="*.ts" \ - --exclude-dir=".git" --exclude-dir="vendor" --exclude-dir="node_modules" 2>/dev/null | \ - grep -v -E '(test|example|sample|getenv|getString|getArgument|config\[|/\.\*/|^\s*//|^\s*\*|CREDENTIAL_PATTERNS|SecurityValidator|SECRET_PATTERN|===|!==|ApiClient|str_contains|gen_wrappers)' | \ - grep -v "= ''" | grep -v '= ""' | grep -v '\$this->config' | \ - grep -v 'type="password"' | grep -v 'type="text"' | grep -v 'name="password"' | grep -v 'name="secretkey"' | \ - grep -v '/dev/null > /tmp/secrets2.txt || true - scan_pattern "Private keys" "โŒ" /tmp/secrets2.txt - - # Pattern 3: AWS keys - grep -r -n -E "AKIA[0-9A-Z]{16}" . \ - --include="*.php" --include="*.py" --include="*.js" --include="*.txt" --include="*.env" \ - --exclude-dir=".git" --exclude-dir="vendor" --exclude-dir="node_modules" 2>/dev/null > /tmp/secrets3.txt || true - scan_pattern "AWS access keys" "โŒ" /tmp/secrets3.txt - - # Pattern 4: GitHub tokens - grep -r -n -E "gh[ps]_[a-zA-Z0-9]{36}" . \ - --include="*.php" --include="*.py" --include="*.js" --include="*.txt" --include="*.env" \ - --exclude-dir=".git" --exclude-dir="vendor" --exclude-dir="node_modules" 2>/dev/null > /tmp/secrets4.txt || true - scan_pattern "GitHub tokens" "โŒ" /tmp/secrets4.txt - - echo "" >> $GITHUB_STEP_SUMMARY - - if [ "$VIOLATIONS" -gt 0 ]; then - echo "**Total Violations**: $VIOLATIONS" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "View detected secrets (file paths only)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - cat /tmp/secrets*.txt 2>/dev/null | cut -d: -f1 | sort -u >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Action Required**: Remove hardcoded secrets immediately!" >> $GITHUB_STEP_SUMMARY - echo "Use environment variables or secrets management instead." >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "โœ… No hardcoded secrets detected" >> $GITHUB_STEP_SUMMARY - fi - - license-compliance: - name: License Header Validation - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Check SPDX Headers - run: | - set -x - echo "### SPDX License Header Check" >> $GITHUB_STEP_SUMMARY - - # Count source files with and without SPDX headers - TOTAL_PHP=0 - WITH_SPDX_PHP=0 - - if find . -name "*.php" -type f ! -path "./vendor/*" | head -1 | grep -q .; then - TOTAL_PHP=$(find . -name "*.php" -type f ! -path "./vendor/*" | wc -l) - WITH_SPDX_PHP=$(find . -name "*.php" -type f ! -path "./vendor/*" -exec grep -l "SPDX-License-Identifier" {} \; | wc -l) - fi - - if [ "$TOTAL_PHP" -gt 0 ]; then - PERCENT=$((WITH_SPDX_PHP * 100 / TOTAL_PHP)) - echo "- PHP files: $WITH_SPDX_PHP/$TOTAL_PHP ($PERCENT%) with SPDX headers" >> $GITHUB_STEP_SUMMARY - - if [ "$PERCENT" -lt 80 ]; then - echo "โš ๏ธ Less than 80% of PHP files have SPDX headers" >> $GITHUB_STEP_SUMMARY - else - echo "โœ… Good SPDX header coverage" >> $GITHUB_STEP_SUMMARY - fi - fi - - - name: Validate License File - run: | - set -x - echo "" >> $GITHUB_STEP_SUMMARY - echo "### License File Validation" >> $GITHUB_STEP_SUMMARY - - if [ ! -f "LICENSE" ]; then - echo "โŒ LICENSE file not found" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### โŒ Validation Failed: LICENSE File Missing" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Error:** LICENSE file is required for all MokoStandards-compliant repositories" >> $GITHUB_STEP_SUMMARY - echo "**Action Required:** Add LICENSE file with appropriate open-source license (GPL-3.0-or-later recommended)" >> $GITHUB_STEP_SUMMARY - echo "" - echo "โŒ ERROR: LICENSE file not found - This is a critical requirement" - exit 1 - fi - - # Check license type - if grep -qi "GNU GENERAL PUBLIC LICENSE" LICENSE; then - VERSION=$(grep -i "Version 3" LICENSE || echo "") - if [ -n "$VERSION" ]; then - echo "โœ… GPL-3.0-or-later license detected" >> $GITHUB_STEP_SUMMARY - else - echo "โš ๏ธ GPL license detected but version unclear" >> $GITHUB_STEP_SUMMARY - fi - elif grep -qi "MIT License" LICENSE; then - echo "โœ… MIT license detected" >> $GITHUB_STEP_SUMMARY - elif grep -qi "Apache License" LICENSE; then - echo "โœ… Apache license detected" >> $GITHUB_STEP_SUMMARY - else - echo "โ„น๏ธ License type could not be automatically detected" >> $GITHUB_STEP_SUMMARY - fi - - repository-structure: - name: Repository Structure Validation - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Check Required Directories - run: | - set -x - echo "## ๐Ÿ“ Repository Structure Validation" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - MISSING=0 - PRESENT=0 - TOTAL=2 - - echo "### Required Directories" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Directory | Status | Files | Size | Notes |" >> $GITHUB_STEP_SUMMARY - echo "|-----------|--------|-------|------|-------|" >> $GITHUB_STEP_SUMMARY - - # Check required directories - for dir in docs .github; do - if [ -d "$dir" ]; then - FILE_COUNT=$(find "$dir" -type f 2>/dev/null | wc -l) - DIR_SIZE=$(du -sh "$dir" 2>/dev/null | cut -f1) - echo "| $dir/ | โœ… Pass | $FILE_COUNT files | $DIR_SIZE | Complete |" >> $GITHUB_STEP_SUMMARY - PRESENT=$((PRESENT + 1)) - else - echo "| $dir/ | โŒ **Missing** | - | - | **Action Required** |" >> $GITHUB_STEP_SUMMARY - MISSING=$((MISSING + 1)) - fi - done - - echo "" >> $GITHUB_STEP_SUMMARY - PERCENT=$((PRESENT * 100 / TOTAL)) - echo "**Compliance Score:** $PERCENT% ($PRESENT/$TOTAL directories present)" >> $GITHUB_STEP_SUMMARY - - if [ "$MISSING" -gt 0 ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "### ๐Ÿ”ด Critical Issues: $MISSING" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Remediation Steps:**" >> $GITHUB_STEP_SUMMARY - [ ! -d "docs" ] && echo "- Create docs directory: \`mkdir docs && echo '# Documentation' > docs/README.md\`" >> $GITHUB_STEP_SUMMARY - [ ! -d ".github" ] && echo "- Create .github directory: \`mkdir -p .github/workflows\`" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "๐Ÿ“š Reference: [MokoStandards Repository Structure](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/docs/policy/core-structure.md)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### โŒ Validation Failed: Required Directories Missing" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Status:** Repository structure does not meet MokoStandards requirements" >> $GITHUB_STEP_SUMMARY - echo "**Missing:** $MISSING required director(y|ies)" >> $GITHUB_STEP_SUMMARY - echo "**Compliance:** $PERCENT% ($PRESENT/$TOTAL directories present)" >> $GITHUB_STEP_SUMMARY - echo "" - echo "โŒ ERROR: Required directories missing - See job summary for remediation steps" - exit 1 - fi - - - name: Check Required Files - run: | - set -x - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Required Files" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - MISSING=0 - PRESENT=0 - TOTAL=5 - - echo "| File | Status | Size | Last Modified | Notes |" >> $GITHUB_STEP_SUMMARY - echo "|------|--------|------|---------------|-------|" >> $GITHUB_STEP_SUMMARY - - # Check required files (CHANGELOG handled separately via find -iname to support src/ChangeLog.md) - for file in README.md LICENSE CONTRIBUTING.md SECURITY.md .editorconfig; do - if [ -f "$file" ]; then - FILE_SIZE=$(wc -c < "$file" 2>/dev/null | awk '{printf "%.1f KB", $1/1024}') - LAST_MOD=$(stat -c %y "$file" 2>/dev/null | cut -d' ' -f1 || echo "Unknown") - CONTENT_CHECK="" - - # Basic content validation - case "$file" in - "README.md") - LINES=$(wc -l < "$file") - [ "$LINES" -lt 10 ] && CONTENT_CHECK="โš ๏ธ Too short" - ;; - "LICENSE") - [ $(wc -c < "$file") -lt 100 ] && CONTENT_CHECK="โš ๏ธ Incomplete?" - ;; - esac - - echo "| $file | โœ… Pass | $FILE_SIZE | $LAST_MOD | Complete $CONTENT_CHECK |" >> $GITHUB_STEP_SUMMARY - PRESENT=$((PRESENT + 1)) - else - echo "| $file | โŒ **Missing** | - | - | **Required** |" >> $GITHUB_STEP_SUMMARY - MISSING=$((MISSING + 1)) - fi - done - - echo "" >> $GITHUB_STEP_SUMMARY - PERCENT=$((PRESENT * 100 / TOTAL)) - echo "**Compliance Score:** $PERCENT% ($PRESENT/$TOTAL files present)" >> $GITHUB_STEP_SUMMARY - - if [ "$MISSING" -gt 0 ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "### ๐Ÿ”ด Critical Issues: $MISSING" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Remediation Steps:**" >> $GITHUB_STEP_SUMMARY - [ ! -f "README.md" ] && echo "- Create README.md: Use [template](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/templates/docs/required/README.md)" >> $GITHUB_STEP_SUMMARY - [ ! -f "LICENSE" ] && echo "- Add LICENSE file: Choose from [OSI-approved licenses](https://opensource.org/licenses)" >> $GITHUB_STEP_SUMMARY - [ ! -f "CONTRIBUTING.md" ] && echo "- Create CONTRIBUTING.md: Use [template](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/templates/docs/required/CONTRIBUTING.md)" >> $GITHUB_STEP_SUMMARY - [ ! -f "SECURITY.md" ] && echo "- Create SECURITY.md: Use [template](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/templates/docs/required/SECURITY.md)" >> $GITHUB_STEP_SUMMARY - [ ! -f ".editorconfig" ] && echo "- Add .editorconfig: Use [template](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/templates/.editorconfig)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "๐Ÿ“š Reference: [MokoStandards File Requirements](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/docs/policy/file-header-standards.md)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### โŒ Validation Failed: Required Files Missing" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Status:** Repository files do not meet MokoStandards requirements" >> $GITHUB_STEP_SUMMARY - echo "**Missing:** $MISSING required file(s)" >> $GITHUB_STEP_SUMMARY - echo "**Compliance:** $PERCENT% ($PRESENT/$TOTAL files present)" >> $GITHUB_STEP_SUMMARY - echo "" - echo "โŒ ERROR: Required files missing - See job summary for remediation steps" - exit 1 - fi - - coding-standards: - name: Coding Standards Check - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Check for Tab Characters - run: | - set -x - echo "### Tab Character Detection" >> $GITHUB_STEP_SUMMARY - - # Policy: Tabs are DEFAULT. Only check for tabs in files that REQUIRE spaces. - # Languages requiring spaces: YAML, Python, Haskell, F#, CoffeeScript, Nim, JSON, RST - TABS_IN_SPACES_FILES=$(find . -type f \ - \( -name "*.yml" -o -name "*.yaml" \ - -o -name "*.py" \ - -o -name "*.hs" -o -name "*.lhs" \ - -o -name "*.fs" -o -name "*.fsx" -o -name "*.fsi" \ - -o -name "*.coffee" -o -name "*.litcoffee" \ - -o -name "*.nim" -o -name "*.nims" -o -name "*.nimble" \ - -o -name "*.json" \ - -o -name "*.rst" \) \ - ! -path "./vendor/*" \ - ! -path "./node_modules/*" \ - ! -path "./.git/*" \ - -exec grep -l $'\t' {} \; 2>/dev/null | head -10) - - if [ -n "$TABS_IN_SPACES_FILES" ]; then - echo "โš ๏ธ Tab characters found in files that require spaces:" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "$TABS_IN_SPACES_FILES" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "These languages require spaces (tabs will break): YAML, Python, Haskell, F#, CoffeeScript, Nim, JSON, RST" >> $GITHUB_STEP_SUMMARY - echo "All other files (including .md, .ps1, LICENSE, etc.) may use tabs per MokoStandards policy" >> $GITHUB_STEP_SUMMARY - else - echo "โœ… No tabs found in files requiring spaces" >> $GITHUB_STEP_SUMMARY - echo "Note: Tabs are allowed in most files (policy default). Only checked files requiring spaces." >> $GITHUB_STEP_SUMMARY - fi - - - name: Check File Encoding - run: | - set -x - echo "" >> $GITHUB_STEP_SUMMARY - echo "### File Encoding Check" >> $GITHUB_STEP_SUMMARY - - # Check for UTF-8 encoding (ASCII is a subset of UTF-8 and is acceptable) - NON_UTF8=$(find . -type f \( -name "*.php" -o -name "*.js" -o -name "*.md" \) \ - ! -path "./vendor/*" \ - ! -path "./node_modules/*" \ - ! -path "./.git/*" \ - -exec file {} \; | grep -v "UTF-8" | grep -v "ASCII" | head -5) - - if [ -n "$NON_UTF8" ]; then - echo "โš ๏ธ Non-UTF-8 files detected:" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "$NON_UTF8" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - else - echo "โœ… All source files appear to be UTF-8 encoded" >> $GITHUB_STEP_SUMMARY - fi - - - name: Check Line Endings - run: | - set -x - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Line Ending Check" >> $GITHUB_STEP_SUMMARY - - # Check for CRLF line endings - CRLF_FILES=$(find . -type f \( -name "*.php" -o -name "*.js" -o -name "*.md" \) \ - ! -path "./vendor/*" \ - ! -path "./node_modules/*" \ - ! -path "./.git/*" \ - -exec file {} \; | grep "CRLF" | head -5) - - if [ -n "$CRLF_FILES" ]; then - echo "โš ๏ธ Files with CRLF line endings found:" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "$CRLF_FILES" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "MokoStandards requires LF line endings" >> $GITHUB_STEP_SUMMARY - else - echo "โœ… Line endings are consistent (LF)" >> $GITHUB_STEP_SUMMARY - fi - - version-consistency: - name: Version Consistency Check - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Set up PHP - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 - with: - php-version: '8.1' - extensions: json - tools: composer - coverage: none - - - name: Setup MokoStandards tools - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' - run: | - git clone --depth 1 --branch version/04 --quiet \ - "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ - /tmp/mokostandards 2>/dev/null || true - if [ -d "/tmp/mokostandards" ] && [ -f "/tmp/mokostandards/composer.json" ]; then - cd /tmp/mokostandards - composer install --no-dev --no-interaction --quiet 2>/dev/null || true - fi - - - name: Run Version Consistency Check - id: version_check - run: | - set -x - echo "## ๐Ÿ”ข Version Consistency Validation" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Use MokoStandards tools (no Composer needed on the governed repo) - if [ -f "/tmp/mokostandards/api/validate/check_version_consistency.php" ]; then - php /tmp/mokostandards/api/validate/check_version_consistency.php --path . --verbose 2>&1 | tee /tmp/version-check.log - EXIT_CODE=${PIPESTATUS[0]} - elif [ -f "api/validate/check_version_consistency.php" ]; then - php api/validate/check_version_consistency.php --path . --verbose 2>&1 | tee /tmp/version-check.log - EXIT_CODE=${PIPESTATUS[0]} - else - echo "โญ๏ธ MokoStandards tools not available โ€” skipping version check" >> $GITHUB_STEP_SUMMARY - exit 0 - fi - - echo '```' >> $GITHUB_STEP_SUMMARY - cat /tmp/version-check.log >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - - if [ "$EXIT_CODE" -eq 0 ]; then - echo "โœ… All version numbers are consistent" >> $GITHUB_STEP_SUMMARY - else - echo "โŒ Version drift detected" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - - # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - # TIER 2 โ€” IMPORTANT (should pass) - # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - workflow-validation: - name: Workflow Configuration Check - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Check Required Workflows - run: | - set -x - echo "### GitHub Actions Workflows" >> $GITHUB_STEP_SUMMARY - - WORKFLOWS_DIR=".github/workflows" - - if [ ! -d "$WORKFLOWS_DIR" ]; then - echo "โŒ No workflows directory found" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### โŒ Validation Failed: Workflows Directory Missing" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Error:** .github/workflows directory is required for CI/CD automation" >> $GITHUB_STEP_SUMMARY - echo "**Action Required:** Create .github/workflows directory and add GitHub Actions workflows" >> $GITHUB_STEP_SUMMARY - echo "" - echo "โŒ ERROR: .github/workflows directory not found" - exit 1 - fi - - # Check for recommended workflows - CI_FOUND=false - for wf in ci.yml build.yml ci-dolibarr.yml ci-joomla.yml; do - if [ -f "$WORKFLOWS_DIR/$wf" ]; then - echo "โœ… CI workflow present ($wf)" >> $GITHUB_STEP_SUMMARY - CI_FOUND=true - break - fi - done - if [ "$CI_FOUND" = "false" ]; then - echo "โš ๏ธ No CI workflow found (ci.yml, build.yml, ci-dolibarr.yml, or ci-joomla.yml)" >> $GITHUB_STEP_SUMMARY - fi - - if [ -f "$WORKFLOWS_DIR/codeql-analysis.yml" ]; then - echo "โœ… CodeQL security scanning present" >> $GITHUB_STEP_SUMMARY - else - echo "โš ๏ธ CodeQL workflow not found" >> $GITHUB_STEP_SUMMARY - fi - - # Check for MokoStandards-synced workflows - for wf in deploy-dev.yml deploy-demo.yml deploy-rs.yml sync-version-on-merge.yml auto-release.yml standards-compliance.yml enterprise-firewall-setup.yml; do - if [ -f "$WORKFLOWS_DIR/$wf" ]; then - echo "โœ… ${wf}" >> $GITHUB_STEP_SUMMARY - else - echo "โš ๏ธ ${wf} not found (synced from MokoStandards)" >> $GITHUB_STEP_SUMMARY - fi - done - - - name: Validate Workflow Syntax - run: | - set -x - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Workflow YAML Syntax" >> $GITHUB_STEP_SUMMARY - - INVALID=0 - for workflow in $(find .github/workflows -maxdepth 1 -type f \( -name "*.yml" -o -name "*.yaml" \) 2>/dev/null); do - if [ -f "$workflow" ]; then - if python3 -c "import yaml, sys; yaml.safe_load(open(sys.argv[1]))" "$workflow" 2>/dev/null; then - echo "โœ… $(basename $workflow)" >> $GITHUB_STEP_SUMMARY - else - echo "โŒ $(basename $workflow) - invalid YAML" >> $GITHUB_STEP_SUMMARY - INVALID=$((INVALID + 1)) - fi - fi - done - - if [ "$INVALID" -gt 0 ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "### โŒ Validation Failed: Invalid Workflow YAML Syntax" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Error:** $INVALID workflow file(s) have invalid YAML syntax" >> $GITHUB_STEP_SUMMARY - echo "**Action Required:** Fix YAML syntax errors in the marked workflow files" >> $GITHUB_STEP_SUMMARY - echo "**Tool:** Run \`python3 -c \"import yaml; yaml.safe_load(open('.github/workflows/FILE.yml'))\"\` locally" >> $GITHUB_STEP_SUMMARY - echo "" - echo "โŒ ERROR: $INVALID workflow file(s) with invalid YAML syntax" - exit 1 - fi - - echo "" >> $GITHUB_STEP_SUMMARY - echo "### โœ… All Workflow Files Have Valid YAML Syntax" >> $GITHUB_STEP_SUMMARY - echo "" - echo "โœ… SUCCESS: All workflow files passed YAML validation" - - - name: Validate CodeQL Configuration - if: hashFiles('.github/workflows/codeql-analysis.yml') != '' - run: | - set -e - echo "" >> $GITHUB_STEP_SUMMARY - echo "### CodeQL Language Configuration" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Inline validation (rewritten from Python to bash for PHP-only architecture) - CODEQL_FILE=".github/workflows/codeql-analysis.yml" - - if [ ! -f "$CODEQL_FILE" ]; then - echo "โš ๏ธ CodeQL workflow file not found" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### โš ๏ธ CodeQL Workflow Not Found" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Status:** CodeQL workflow file not present - skipping language validation" >> $GITHUB_STEP_SUMMARY - echo "" - echo "โš ๏ธ INFO: CodeQL workflow not found - Skipping validation" - exit 0 - fi - - echo "**CodeQL Configuration Analysis**" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Extract configured languages from workflow - LANGUAGES=$(grep -A5 "language:" "$CODEQL_FILE" | grep -oP "(?<=')[^']+(?=')" | tr '\n' ' ' || echo "") - - # Check if this is a configuration-only scan (no languages specified) - if grep -q "category.*language:config" "$CODEQL_FILE"; then - echo "**Scan Type:** Configuration-only (no language matrix)" >> $GITHUB_STEP_SUMMARY - echo "**Status:** โœ… Valid configuration for PHP-only repository" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "This CodeQL workflow scans YAML, JSON, shell scripts for security issues." >> $GITHUB_STEP_SUMMARY - echo "PHP security is handled by SecurityValidator enterprise library." >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "โœ… SUCCESS: CodeQL configuration-only scan properly configured" - exit 0 - fi - - if [ -z "$LANGUAGES" ]; then - echo "โŒ No languages configured in CodeQL workflow" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### โŒ Validation Failed: CodeQL Languages Not Configured" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Error:** CodeQL workflow exists but has no languages configured" >> $GITHUB_STEP_SUMMARY - echo "**Action Required:** Configure appropriate languages in codeql-analysis.yml" >> $GITHUB_STEP_SUMMARY - echo "" - echo "โŒ ERROR: No languages configured in CodeQL workflow" - exit 1 - fi - - echo "**Configured Languages:** $LANGUAGES" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Validate language presence in repository - INVALID_LANGS="" - VALID_LANGS="" - - for LANG in $LANGUAGES; do - case "$LANG" in - python) - # Check for Python files (should be none in v04.00.04) - if find . -name "*.py" -type f ! -path "./.git/*" | grep -q .; then - VALID_LANGS="$VALID_LANGS python" - echo "โœ… Python: Found Python files" >> $GITHUB_STEP_SUMMARY - else - INVALID_LANGS="$INVALID_LANGS python" - echo "โŒ Python: No Python files found (PHP-only repository)" >> $GITHUB_STEP_SUMMARY - fi - ;; - javascript|typescript) - # Check for JS/TS files - if find . \( -name "*.js" -o -name "*.ts" -o -name "*.json" \) -type f ! -path "./.git/*" ! -path "./node_modules/*" | grep -q .; then - VALID_LANGS="$VALID_LANGS $LANG" - echo "โœ… $LANG: Found JavaScript/TypeScript/JSON files" >> $GITHUB_STEP_SUMMARY - else - INVALID_LANGS="$INVALID_LANGS $LANG" - echo "โš ๏ธ $LANG: No JavaScript/TypeScript files found" >> $GITHUB_STEP_SUMMARY - fi - ;; - java) - if find . -name "*.java" -type f ! -path "./.git/*" | grep -q .; then - VALID_LANGS="$VALID_LANGS java" - echo "โœ… Java: Found Java files" >> $GITHUB_STEP_SUMMARY - else - INVALID_LANGS="$INVALID_LANGS java" - echo "โš ๏ธ Java: No Java files found" >> $GITHUB_STEP_SUMMARY - fi - ;; - go) - if find . -name "*.go" -type f ! -path "./.git/*" | grep -q .; then - VALID_LANGS="$VALID_LANGS go" - echo "โœ… Go: Found Go files" >> $GITHUB_STEP_SUMMARY - else - INVALID_LANGS="$INVALID_LANGS go" - echo "โš ๏ธ Go: No Go files found" >> $GITHUB_STEP_SUMMARY - fi - ;; - cpp|c) - if find . \( -name "*.cpp" -o -name "*.c" -o -name "*.h" \) -type f ! -path "./.git/*" | grep -q .; then - VALID_LANGS="$VALID_LANGS $LANG" - echo "โœ… $LANG: Found C/C++ files" >> $GITHUB_STEP_SUMMARY - else - INVALID_LANGS="$INVALID_LANGS $LANG" - echo "โš ๏ธ $LANG: No C/C++ files found" >> $GITHUB_STEP_SUMMARY - fi - ;; - ruby) - if find . -name "*.rb" -type f ! -path "./.git/*" | grep -q .; then - VALID_LANGS="$VALID_LANGS ruby" - echo "โœ… Ruby: Found Ruby files" >> $GITHUB_STEP_SUMMARY - else - INVALID_LANGS="$INVALID_LANGS ruby" - echo "โš ๏ธ Ruby: No Ruby files found" >> $GITHUB_STEP_SUMMARY - fi - ;; - *) - echo "โš ๏ธ $LANG: Unknown language, skipping validation" >> $GITHUB_STEP_SUMMARY - ;; - esac - done - - echo "" >> $GITHUB_STEP_SUMMARY - - # Report results - if [ -n "$INVALID_LANGS" ]; then - echo "**โš ๏ธ Warning:** Some configured languages may not have corresponding files:" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "Invalid languages: $INVALID_LANGS" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Note:** This is informational. CodeQL will skip languages without source files." >> $GITHUB_STEP_SUMMARY - echo "For PHP repository (v04.00.04), JavaScript language covers JSON/YAML/shell scripts." >> $GITHUB_STEP_SUMMARY - else - echo "โœ… **All configured CodeQL languages have corresponding source files**" >> $GITHUB_STEP_SUMMARY - fi - - # Always succeed - this is informational only - echo "" >> $GITHUB_STEP_SUMMARY - echo "### โœ… CodeQL Configuration Validation Complete" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Status:** CodeQL language configuration reviewed successfully" >> $GITHUB_STEP_SUMMARY - echo "" - echo "โœ… SUCCESS: CodeQL validation complete" - exit 0 - - documentation-quality: - name: Documentation Quality Check - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Validate README.md - run: | - set -x - echo "## ๐Ÿ“š Documentation Quality Check" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### README.md Analysis" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ ! -f "README.md" ]; then - echo "โŒ **Critical:** README.md not found" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### โŒ Validation Failed: README.md Missing" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Error:** README.md is required for all MokoStandards-compliant repositories" >> $GITHUB_STEP_SUMMARY - echo "**Action Required:** Create README.md with project description, setup instructions, and usage examples" >> $GITHUB_STEP_SUMMARY - echo "" - echo "โŒ ERROR: README.md not found - This is a critical requirement" - exit 1 - fi - - # Detailed content analysis - SIZE=$(wc -c < README.md) - LINES=$(wc -l < README.md) - WORDS=$(wc -w < README.md) - HEADINGS=$(grep -c "^#" README.md || echo 0) - LINKS=$(grep -c "\[.*\](.*)" README.md || echo 0) - CODE_BLOCKS=$(grep -c '```' README.md || echo 0) - - echo "| Metric | Value | Status | Recommendation |" >> $GITHUB_STEP_SUMMARY - echo "|--------|-------|--------|----------------|" >> $GITHUB_STEP_SUMMARY - - # Size check - SIZE_STATUS="โœ… Good" - SIZE_REC="Adequate length" - if [ "$SIZE" -lt 500 ]; then - SIZE_STATUS="โš ๏ธ Warning" - SIZE_REC="Add more content (min 500 bytes)" - elif [ "$SIZE" -gt 50000 ]; then - SIZE_STATUS="โš ๏ธ Warning" - SIZE_REC="Consider splitting into multiple docs" - fi - echo "| Size | $SIZE bytes | $SIZE_STATUS | $SIZE_REC |" >> $GITHUB_STEP_SUMMARY - - # Line count - LINES_STATUS="โœ… Good" - LINES_REC="Good size" - if [ "$LINES" -lt 20 ]; then - LINES_STATUS="โš ๏ธ Warning" - LINES_REC="Add more sections (min 20 lines)" - fi - echo "| Lines | $LINES | $LINES_STATUS | $LINES_REC |" >> $GITHUB_STEP_SUMMARY - - # Word count - WORDS_STATUS="โœ… Good" - WORDS_REC="Good detail" - if [ "$WORDS" -lt 100 ]; then - WORDS_STATUS="โš ๏ธ Warning" - WORDS_REC="Add more description (min 100 words)" - fi - echo "| Words | $WORDS | $WORDS_STATUS | $WORDS_REC |" >> $GITHUB_STEP_SUMMARY - - # Headings - HEADINGS_STATUS="โœ… Good" - HEADINGS_REC="Well structured" - if [ "$HEADINGS" -lt 3 ]; then - HEADINGS_STATUS="โš ๏ธ Warning" - HEADINGS_REC="Add more sections (min 3 headings)" - fi - echo "| Headings | $HEADINGS | $HEADINGS_STATUS | $HEADINGS_REC |" >> $GITHUB_STEP_SUMMARY - - # Links - LINKS_STATUS="โœ… Good" - LINKS_REC="Includes references" - if [ "$LINKS" -lt 1 ]; then - LINKS_STATUS="โ„น๏ธ Info" - LINKS_REC="Consider adding useful links" - fi - echo "| Links | $LINKS | $LINKS_STATUS | $LINKS_REC |" >> $GITHUB_STEP_SUMMARY - - # Code blocks - CODE_STATUS="โœ… Good" - CODE_REC="Includes examples" - if [ "$CODE_BLOCKS" -eq 0 ]; then - CODE_STATUS="โ„น๏ธ Info" - CODE_REC="Consider adding code examples" - fi - echo "| Code blocks | $CODE_BLOCKS | $CODE_STATUS | $CODE_REC |" >> $GITHUB_STEP_SUMMARY - - echo "" >> $GITHUB_STEP_SUMMARY - - # Check for key sections - echo "**Section Coverage:**" >> $GITHUB_STEP_SUMMARY - MISSING_COUNT=0 - grep -qi "install\|setup\|getting started" README.md && echo "- โœ… Installation/Setup instructions" >> $GITHUB_STEP_SUMMARY || { echo "- โš ๏ธ Missing: Installation/Setup" >> $GITHUB_STEP_SUMMARY; MISSING_COUNT=$((MISSING_COUNT + 1)); } - grep -qi "usage\|example\|how to" README.md && echo "- โœ… Usage examples" >> $GITHUB_STEP_SUMMARY || { echo "- โš ๏ธ Missing: Usage examples" >> $GITHUB_STEP_SUMMARY; MISSING_COUNT=$((MISSING_COUNT + 1)); } - grep -qi "license" README.md && echo "- โœ… License information" >> $GITHUB_STEP_SUMMARY || { echo "- โš ๏ธ Missing: License information" >> $GITHUB_STEP_SUMMARY; MISSING_COUNT=$((MISSING_COUNT + 1)); } - grep -qi "contribut" README.md && echo "- โœ… Contributing guidelines" >> $GITHUB_STEP_SUMMARY || echo "- โ„น๏ธ Optional: Contributing section" >> $GITHUB_STEP_SUMMARY - - if [ "$MISSING_COUNT" -gt 0 ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "**โš ๏ธ $MISSING_COUNT important sections missing**" >> $GITHUB_STEP_SUMMARY - fi - - - name: Validate CHANGELOG.md - run: | - set -x - echo "" >> $GITHUB_STEP_SUMMARY - echo "### CHANGELOG.md Analysis" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Locate changelog case-insensitively; accepted at root, src/, or docs/ - CHANGELOG_PATH=$(find . -maxdepth 3 \( -path ./.git -o -path ./node_modules \) -prune \ - -o -iname "changelog.md" -print | head -1 | sed 's|^\./||') - - if [ -z "$CHANGELOG_PATH" ]; then - echo "โŒ **Critical:** CHANGELOG.md not found (checked root, src/, docs/)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### โŒ Validation Failed: CHANGELOG.md Missing" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Error:** CHANGELOG.md is required for all MokoStandards-compliant repositories" >> $GITHUB_STEP_SUMMARY - echo "**Action Required:** Create CHANGELOG.md following [Keep a Changelog](https://keepachangelog.com/) format" >> $GITHUB_STEP_SUMMARY - echo "" - echo "โŒ ERROR: CHANGELOG.md not found - This is a critical requirement" - exit 1 - fi - - echo "๐Ÿ“„ Found: $CHANGELOG_PATH" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Analyze changelog structure - VERSIONS=$(grep -c "## \[" "$CHANGELOG_PATH" || echo 0) - UNRELEASED=$(grep -c "## \[Unreleased\]" "$CHANGELOG_PATH" || echo 0) - DATES=$(grep -c "[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}" "$CHANGELOG_PATH" || echo 0) - SIZE=$(wc -c < "$CHANGELOG_PATH") - - echo "| Metric | Value | Status | Notes |" >> $GITHUB_STEP_SUMMARY - echo "|--------|-------|--------|-------|" >> $GITHUB_STEP_SUMMARY - - # Check format - if grep -qi "## \[.*\]" "$CHANGELOG_PATH"; then - echo "| Format | Keep a Changelog | โœ… Pass | Standard format |" >> $GITHUB_STEP_SUMMARY - else - echo "| Format | Custom | โš ๏ธ Warning | Consider [Keep a Changelog](https://keepachangelog.com/) |" >> $GITHUB_STEP_SUMMARY - fi - - # Version count - VERSIONS_STATUS="โœ… Good" - VERSIONS_NOTE="Well maintained" - if [ "$VERSIONS" -lt 1 ]; then - VERSIONS_STATUS="โš ๏ธ Warning" - VERSIONS_NOTE="Add version entries" - fi - echo "| Versions | $VERSIONS | $VERSIONS_STATUS | $VERSIONS_NOTE |" >> $GITHUB_STEP_SUMMARY - - # Unreleased section - if [ "$UNRELEASED" -gt 0 ]; then - echo "| Unreleased | Yes | โœ… Good | Active development tracked |" >> $GITHUB_STEP_SUMMARY - else - echo "| Unreleased | No | โ„น๏ธ Info | Consider adding [Unreleased] section |" >> $GITHUB_STEP_SUMMARY - fi - - # Dates - DATES_STATUS="โœ… Good" - if [ "$DATES" -lt 1 ]; then - DATES_STATUS="โš ๏ธ Warning" - DATES_NOTE="Add release dates" - else - DATES_NOTE="Dates present" - fi - echo "| Release dates | $DATES | $DATES_STATUS | $DATES_NOTE |" >> $GITHUB_STEP_SUMMARY - - # Check for standard sections - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Changelog Sections:**" >> $GITHUB_STEP_SUMMARY - grep -qi "### Added" "$CHANGELOG_PATH" && echo "- โœ… Added section" >> $GITHUB_STEP_SUMMARY || echo "- โ„น๏ธ Added section (optional)" >> $GITHUB_STEP_SUMMARY - grep -qi "### Changed" "$CHANGELOG_PATH" && echo "- โœ… Changed section" >> $GITHUB_STEP_SUMMARY || echo "- โ„น๏ธ Changed section (optional)" >> $GITHUB_STEP_SUMMARY - grep -qi "### Fixed" "$CHANGELOG_PATH" && echo "- โœ… Fixed section" >> $GITHUB_STEP_SUMMARY || echo "- โ„น๏ธ Fixed section (optional)" >> $GITHUB_STEP_SUMMARY - - echo "" >> $GITHUB_STEP_SUMMARY - echo "๐Ÿ“š Reference: [Keep a Changelog](https://keepachangelog.com/)" >> $GITHUB_STEP_SUMMARY - - - name: Check Documentation Index - run: | - set -x - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Documentation Index" >> $GITHUB_STEP_SUMMARY - - if [ -f "docs/index.md" ] || [ -f "docs/README.md" ]; then - echo "โœ… Documentation index found" >> $GITHUB_STEP_SUMMARY - else - echo "โš ๏ธ No documentation index (docs/index.md or docs/README.md)" >> $GITHUB_STEP_SUMMARY - fi - - readme-completeness: - name: README Completeness Check - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Check README Sections - run: | - set -x - echo "## ๐Ÿ“„ README Completeness Check" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ ! -f "README.md" ]; then - echo "โŒ README.md not found" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - # Required sections - REQUIRED_SECTIONS=("Installation" "Usage" "Contributing" "License") - MISSING=0 - PRESENT=0 - - echo "### Required Sections" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - for section in "${REQUIRED_SECTIONS[@]}"; do - if grep -qi "##.*$section" README.md; then - echo "โœ… $section" >> $GITHUB_STEP_SUMMARY - PRESENT=$((PRESENT + 1)) - else - echo "โŒ $section" >> $GITHUB_STEP_SUMMARY - MISSING=$((MISSING + 1)) - fi - done - - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Completeness**: $PRESENT/${#REQUIRED_SECTIONS[@]} required sections present" >> $GITHUB_STEP_SUMMARY - - if [ "$MISSING" -gt 0 ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Action Required**: Add missing sections to README.md" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - # ============================================================================ - # PHASE 3: Future Enhancements - # ============================================================================ - - git-hygiene: - name: Git Repository Hygiene - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - fetch-depth: 0 - - - name: Check .gitignore - run: | - set -x - echo "### .gitignore Validation" >> $GITHUB_STEP_SUMMARY - - if [ ! -f ".gitignore" ]; then - echo "โš ๏ธ .gitignore file not found" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### โš ๏ธ Warning: .gitignore Not Found" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Status:** .gitignore file is recommended but not required" >> $GITHUB_STEP_SUMMARY - echo "**Recommendation:** Add .gitignore to exclude build artifacts, dependencies, and temporary files" >> $GITHUB_STEP_SUMMARY - echo "" - echo "โš ๏ธ WARNING: .gitignore file not found - Continuing validation" - exit 0 - fi - - # Check for common exclusions - MISSING="" - grep -q "vendor/" .gitignore || MISSING="${MISSING}vendor/ " - grep -q "node_modules/" .gitignore || MISSING="${MISSING}node_modules/ " - - if [ -n "$MISSING" ]; then - echo "โš ๏ธ .gitignore may be missing common exclusions: $MISSING" >> $GITHUB_STEP_SUMMARY - else - echo "โœ… .gitignore appears complete" >> $GITHUB_STEP_SUMMARY - fi - - - name: Check for Large Files - run: | - set -x - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Large File Detection" >> $GITHUB_STEP_SUMMARY - - # Find files larger than 1MB - LARGE_FILES=$(find . -type f -size +1M ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" | head -5) - - if [ -n "$LARGE_FILES" ]; then - echo "โš ๏ธ Large files detected (>1MB):" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "$LARGE_FILES" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "Consider using Git LFS for large binary files" >> $GITHUB_STEP_SUMMARY - else - echo "โœ… No unusually large files detected" >> $GITHUB_STEP_SUMMARY - fi - - script-integrity: - name: Script Integrity Validation - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: '3.x' - - - name: Validate Script Integrity - id: script_check - run: | - set -x - echo "## ๐Ÿ” Script Integrity Validation" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ -f "api/.script-registry.json" ]; then - echo "### Critical Scripts" >> $GITHUB_STEP_SUMMARY - php api/maintenance/update_sha_hashes.php \ - --dry-run --verbose | tee /tmp/script-validation.log - - EXIT_CODE=$? - - echo "" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - cat /tmp/script-validation.log >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - - if [ "$EXIT_CODE" -eq 0 ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "โœ… All critical scripts validated successfully!" >> $GITHUB_STEP_SUMMARY - exit 0 - else - echo "" >> $GITHUB_STEP_SUMMARY - echo "โŒ Script integrity violations detected" >> $GITHUB_STEP_SUMMARY - echo "**Action Required:** Review validation report and update registry" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - else - echo "โ„น๏ธ Script registry not found - skipping integrity check" >> $GITHUB_STEP_SUMMARY - exit 0 - fi - - - # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - # TIER 3 โ€” QUALITY (code quality metrics) - # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - line-length-validation: - name: Line Length Check - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Check Line Lengths - run: | - set -x - echo "## ๐Ÿ“ Line Length Validation" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Line length standards: - # - General source code: 120 characters (hard limit) - # - YAML workflows: 180 characters (exception for GitHub Actions) - # - Markdown files: No limit (content-focused) - - echo "### Line Length Standards" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| File Type | Soft Limit | Hard Limit |" >> $GITHUB_STEP_SUMMARY - echo "|-----------|------------|------------|" >> $GITHUB_STEP_SUMMARY - echo "| General source code | 80 chars | 120 chars |" >> $GITHUB_STEP_SUMMARY - echo "| YAML workflows | 80 chars | 180 chars |" >> $GITHUB_STEP_SUMMARY - echo "| Markdown files | N/A | No limit |" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Check YAML files (using yamllint which is already configured) - echo "### YAML Files (180 char limit)" >> $GITHUB_STEP_SUMMARY - - YAML_VIOLATIONS=0 - if command -v yamllint >/dev/null 2>&1; then - # Install yamllint if not present - : - else - pip install yamllint >/dev/null 2>&1 - fi - - # Run yamllint and count line-length warnings - YAML_OUTPUT=$(yamllint .github/workflows/*.yml 2>&1 | grep "line too long" || true) - if [ -n "$YAML_OUTPUT" ]; then - YAML_VIOLATIONS=$(echo "$YAML_OUTPUT" | wc -l) - echo "โš ๏ธ Found $YAML_VIOLATIONS lines exceeding 180 characters in YAML files" >> $GITHUB_STEP_SUMMARY - echo "
View warnings (informational only)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "$YAML_OUTPUT" | head -20 >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - else - echo "โœ… All YAML files comply with 180 character limit" >> $GITHUB_STEP_SUMMARY - fi - echo "" >> $GITHUB_STEP_SUMMARY - - # Check source code files (PHP, Python, JavaScript, etc.) for 120 char limit - echo "### Source Code Files (120 char limit)" >> $GITHUB_STEP_SUMMARY - - LONG_LINES=$(find . -type f \ - \( -name "*.php" -o -name "*.py" -o -name "*.js" -o -name "*.ts" \ - -o -name "*.go" -o -name "*.rs" -o -name "*.java" -o -name "*.c" \ - -o -name "*.cpp" -o -name "*.h" -o -name "*.sh" \) \ - ! -path "./vendor/*" \ - ! -path "./node_modules/*" \ - ! -path "./.git/*" \ - ! -path "./build/*" \ - ! -path "./dist/*" \ - -exec awk 'length > 120 { print FILENAME ":" NR ": " length " chars" }' {} \; 2>/dev/null | head -20) - - if [ -n "$LONG_LINES" ]; then - LINE_COUNT=$(echo "$LONG_LINES" | wc -l) - echo "โš ๏ธ Found $LINE_COUNT source code lines exceeding 120 characters" >> $GITHUB_STEP_SUMMARY - echo "
View violations (informational)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "$LONG_LINES" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - else - echo "โœ… All source code files comply with 120 character limit" >> $GITHUB_STEP_SUMMARY - fi - echo "" >> $GITHUB_STEP_SUMMARY - - # Confirm Markdown files are not checked - echo "### Markdown Files" >> $GITHUB_STEP_SUMMARY - echo "โœ… Markdown files have no line length limit per coding standards" >> $GITHUB_STEP_SUMMARY - echo "Rationale: Content-focused format, URLs, tables, and natural prose flow" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Summary - echo "### Summary" >> $GITHUB_STEP_SUMMARY - echo "This check is **informational only** and does not block merges." >> $GITHUB_STEP_SUMMARY - echo "Line length standards help maintain code readability." >> $GITHUB_STEP_SUMMARY - echo "Exceptions documented in: \`docs/policy/coding-style-guide.md\`" >> $GITHUB_STEP_SUMMARY - - file-naming-standards: - name: File Naming Standards - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Check File Naming - run: | - set -x - echo "## ๐Ÿ“ File Naming Standards" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - VIOLATIONS=0 - - # Check PHP files (should be PascalCase for classes) - INVALID_PHP=$(find . -name "*.php" ! -path "./vendor/*" ! -path "./.git/*" ! -regex ".*/[A-Z][a-zA-Z0-9]*\.php" ! -name "index.php" ! -name "functions.php" | wc -l || echo 0) - - # Check config files (should be kebab-case) - INVALID_CONFIG=$(find . -name "*.yml" -o -name "*.yaml" -o -name "*.json" ! -path "./vendor/*" ! -path "./.git/*" ! -path "./node_modules/*" | grep -E "[A-Z_]" | wc -l || echo 0) - - echo "### Naming Violations" >> $GITHUB_STEP_SUMMARY - echo "- **PHP files not PascalCase**: $INVALID_PHP" >> $GITHUB_STEP_SUMMARY - echo "- **Config files not kebab-case**: $INVALID_CONFIG" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - VIOLATIONS=$((INVALID_PHP + INVALID_CONFIG)) - - if [ "$VIOLATIONS" -gt 0 ]; then - echo "โš ๏ธ Found $VIOLATIONS naming convention violation(s)" >> $GITHUB_STEP_SUMMARY - echo "**Recommendation**: Follow naming conventions for consistency" >> $GITHUB_STEP_SUMMARY - else - echo "โœ… File naming conventions followed" >> $GITHUB_STEP_SUMMARY - fi - - insecure-patterns: - name: Insecure Code Pattern Detection - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Scan for Insecure Patterns - run: | - set -x - echo "## ๐Ÿ”’ Insecure Code Pattern Detection" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - VIOLATIONS=0 - - # PHP: SQL injection patterns - if grep -r -n "\\$_\(GET\|POST\|REQUEST\).*mysql_query\|mysqli_query" . --include="*.php" ! -path "./vendor/*" 2>/dev/null > /tmp/sql_inject.txt; then - COUNT=$(wc -l < /tmp/sql_inject.txt) - echo "โš ๏ธ Found $COUNT potential SQL injection pattern(s)" >> $GITHUB_STEP_SUMMARY - VIOLATIONS=$((VIOLATIONS + COUNT)) - fi - - # PHP: eval/exec usage - if grep -r -n "eval\|exec\|system\|passthru\|shell_exec" . --include="*.php" ! -path "./vendor/*" 2>/dev/null > /tmp/exec.txt; then - COUNT=$(wc -l < /tmp/exec.txt) - echo "โš ๏ธ Found $COUNT dangerous function call(s)" >> $GITHUB_STEP_SUMMARY - VIOLATIONS=$((VIOLATIONS + COUNT)) - fi - - # Python: eval usage - if grep -r -n "eval(" . --include="*.py" 2>/dev/null > /tmp/py_eval.txt; then - COUNT=$(wc -l < /tmp/py_eval.txt) - echo "โš ๏ธ Found $COUNT Python eval() usage(s)" >> $GITHUB_STEP_SUMMARY - VIOLATIONS=$((VIOLATIONS + COUNT)) - fi - - echo "" >> $GITHUB_STEP_SUMMARY - - if [ "$VIOLATIONS" -gt 0 ]; then - echo "**Total Violations**: $VIOLATIONS" >> $GITHUB_STEP_SUMMARY - echo "**Recommendation**: Review and secure flagged patterns" >> $GITHUB_STEP_SUMMARY - else - echo "โœ… No insecure patterns detected" >> $GITHUB_STEP_SUMMARY - fi - - code-complexity: - name: Code Complexity Analysis - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Setup PHP - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 - with: - php-version: '8.1' - - - name: Analyze Complexity - run: | - set -x - echo "## ๐Ÿ“Š Code Complexity Analysis" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - PHP_COUNT=$(find . -name "*.php" ! -path "./vendor/*" ! -path "./.git/*" | wc -l) - - if [ "$PHP_COUNT" -gt 0 ]; then - # Install phploc - wget https://phar.phpunit.de/phploc.phar 2>/dev/null - chmod +x phploc.phar - - echo "### PHP Code Metrics" >> $GITHUB_STEP_SUMMARY - if ./phploc.phar --exclude vendor --exclude .git . 2>&1 | tee /tmp/phploc.txt; then - COMPLEXITY=$(grep "Cyclomatic Complexity" /tmp/phploc.txt | grep "Average" | awk '{print $NF}' || echo "N/A") - echo "**Average Cyclomatic Complexity**: $COMPLEXITY" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ "$COMPLEXITY" != "N/A" ] && [ $(echo "$COMPLEXITY > 10" | bc -l) -eq 1 ]; then - echo "โš ๏ธ Average complexity exceeds recommended threshold (10)" >> $GITHUB_STEP_SUMMARY - echo "**Recommendation**: Refactor complex functions" >> $GITHUB_STEP_SUMMARY - else - echo "โœ… Code complexity within acceptable limits" >> $GITHUB_STEP_SUMMARY - fi - fi - else - echo "โ„น๏ธ No PHP files found for complexity analysis" >> $GITHUB_STEP_SUMMARY - fi - - code-duplication: - name: Code Duplication Detection - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Setup PHP - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 - with: - php-version: '8.1' - - - name: Detect Duplicates - run: | - set -x - echo "## ๐Ÿ” Code Duplication Detection" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Check if PHP files exist - PHP_COUNT=$(find . -name "*.php" ! -path "./vendor/*" ! -path "./.git/*" | wc -l) - - if [ "$PHP_COUNT" -gt 0 ]; then - echo "### PHP Code Duplication" >> $GITHUB_STEP_SUMMARY - - # Install phpcpd - wget https://phar.phpunit.de/phpcpd.phar 2>/dev/null - chmod +x phpcpd.phar - - # Run duplication detection - if ./phpcpd.phar --exclude vendor --exclude .git . 2>&1 | tee /tmp/phpcpd.txt; then - DUPLICATION=$(grep "Found" /tmp/phpcpd.txt | grep -oE "[0-9]+\.[0-9]+%" | head -1 || echo "0.00%") - echo "๐Ÿ“Š **Duplication Rate**: $DUPLICATION" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - DUPLICATION_NUM=$(echo "$DUPLICATION" | sed 's/%//') - if [ $(echo "$DUPLICATION_NUM > 5.0" | bc -l) -eq 1 ]; then - echo "โš ๏ธ Code duplication exceeds 5% threshold" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "View duplication details" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - cat /tmp/phpcpd.txt >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - else - echo "โœ… Code duplication within acceptable limits (<5%)" >> $GITHUB_STEP_SUMMARY - fi - else - echo "โœ… No significant code duplication detected" >> $GITHUB_STEP_SUMMARY - fi - else - echo "โ„น๏ธ No PHP files found for duplication analysis" >> $GITHUB_STEP_SUMMARY - fi - - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Note**: This is an informational check to encourage DRY principles." >> $GITHUB_STEP_SUMMARY - - dead-code-detection: - name: Dead Code Detection - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Setup Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: '3.x' - - - name: Detect Dead Code - run: | - set -x - echo "## ๐Ÿ—‘๏ธ Dead Code Detection" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - PY_COUNT=$(find . -name "*.py" ! -path "./vendor/*" ! -path "./.git/*" ! -path "./venv/*" | wc -l) - - if [ "$PY_COUNT" -gt 0 ]; then - pip install vulture 2>/dev/null - echo "### Python Dead Code" >> $GITHUB_STEP_SUMMARY - - if vulture . --exclude vendor,venv,.git 2>&1 | tee /tmp/vulture.txt; then - DEAD_COUNT=$(wc -l < /tmp/vulture.txt || echo 0) - if [ "$DEAD_COUNT" -gt 0 ]; then - echo "โš ๏ธ Found $DEAD_COUNT potential dead code item(s)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "View dead code" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - head -50 /tmp/vulture.txt >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - else - echo "โœ… No dead code detected" >> $GITHUB_STEP_SUMMARY - fi - fi - else - echo "โ„น๏ธ No Python files found for dead code analysis" >> $GITHUB_STEP_SUMMARY - fi - - - # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - # TIER 4 โ€” SUPPLEMENTARY (informational) - # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - file-size-limits: - name: File Size Limits - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Check File Sizes - run: | - set -x - echo "## ๐Ÿ“ฆ File Size Validation" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Exempt file types (allowed to be large) - EXEMPT="! -name *.mmdb ! -name *.woff2 ! -name *.woff ! -name *.ttf ! -name *.otf" - - # Find large files (>15MB warning, >20MB critical) - LARGE_FILES=$(find . -type f -size +15M $EXEMPT ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" 2>/dev/null | wc -l) - HUGE_FILES=$(find . -type f -size +20M $EXEMPT ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" 2>/dev/null | wc -l) - - echo "### Size Thresholds" >> $GITHUB_STEP_SUMMARY - echo "- **Warning**: Files >15MB" >> $GITHUB_STEP_SUMMARY - echo "- **Critical**: Files >20MB" >> $GITHUB_STEP_SUMMARY - echo "- **Exempt**: .mmdb, .woff2, .woff, .ttf, .otf" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ "$HUGE_FILES" -gt 0 ]; then - echo "โŒ **Critical**: Found $HUGE_FILES file(s) exceeding 20MB" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "View files >20MB" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - find . -type f -size +20M $EXEMPT ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" -exec ls -lh {} + 2>/dev/null | awk '{print $5, $9}' >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Action Required**: Remove or optimize files >20MB" >> $GITHUB_STEP_SUMMARY - exit 1 - elif [ "$LARGE_FILES" -gt 0 ]; then - echo "โš ๏ธ **Warning**: Found $LARGE_FILES file(s) between 15MB and 20MB" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "View files >15MB" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - find . -type f -size +15M $EXEMPT ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" -exec ls -lh {} + 2>/dev/null | awk '{print $5, $9}' >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Recommendation**: Consider optimizing large files" >> $GITHUB_STEP_SUMMARY - else - echo "โœ… All files within acceptable size limits" >> $GITHUB_STEP_SUMMARY - fi - - binary-file-detection: - name: Binary File Detection - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Detect Binary Files - run: | - set -x - echo "## ๐Ÿ” Binary File Detection" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Find binary files excluding allowed types - BINARIES=$(find . -type f ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" \ - ! -name "*.png" ! -name "*.jpg" ! -name "*.jpeg" ! -name "*.gif" ! -name "*.svg" ! -name "*.ico" \ - ! -name "*.woff" ! -name "*.woff2" ! -name "*.ttf" ! -name "*.eot" \ - -exec file {} \; | grep -v "text" | grep -v "empty" | wc -l || echo 0) - - if [ "$BINARIES" -gt 0 ]; then - echo "โš ๏ธ Found $BINARIES non-image binary file(s)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "View binary files" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - find . -type f ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" \ - ! -name "*.png" ! -name "*.jpg" ! -name "*.jpeg" ! -name "*.gif" ! -name "*.svg" ! -name "*.ico" \ - ! -name "*.woff" ! -name "*.woff2" ! -name "*.ttf" ! -name "*.eot" \ - -exec file {} \; | grep -v "text" | grep -v "empty" | cut -d: -f1 >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Recommendation**: Source control should primarily contain text files" >> $GITHUB_STEP_SUMMARY - else - echo "โœ… No unexpected binary files detected" >> $GITHUB_STEP_SUMMARY - fi - - # ============================================================================ - # PHASE 4: Nice to Have Checks - # ============================================================================ - - todo-fixme-tracking: - name: TODO/FIXME Tracking - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Track Technical Debt - run: | - set -x - echo "## ๐Ÿ“ TODO/FIXME Tracking" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Tracking technical debt markers in source code." >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Search for technical debt markers - PATTERNS="TODO|FIXME|HACK|XXX" - EXTENSIONS="*.php *.py *.js *.ts *.go *.rs *.java *.c *.cpp *.h *.hpp *.sh" - - echo "### Technical Debt Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - TOTAL_COUNT=0 - for ext in $EXTENSIONS; do - COUNT=$(find . -type f -name "$ext" ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" -exec grep -n -E "($PATTERNS)" {} + 2>/dev/null | wc -l || echo 0) - TOTAL_COUNT=$((TOTAL_COUNT + COUNT)) - done - - if [ "$TOTAL_COUNT" -gt 0 ]; then - echo "โš ๏ธ Found **$TOTAL_COUNT** technical debt item(s)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "View technical debt items" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - for ext in $EXTENSIONS; do - find . -type f -name "$ext" ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" -exec grep -n -H -E "($PATTERNS)" {} + 2>/dev/null | head -100 || true - done >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - else - echo "โœ… No technical debt markers found" >> $GITHUB_STEP_SUMMARY - fi - - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Note**: This is an informational check. Technical debt items don't block compliance." >> $GITHUB_STEP_SUMMARY - - dependency-vulnerabilities: - name: Dependency Vulnerability Scanning - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Setup PHP - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 - with: - php-version: '8.1' - - - name: Setup Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: '3.x' - - - name: Scan Dependencies - run: | - set -x - echo "## ๐Ÿ›ก๏ธ Dependency Vulnerability Scanning" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - VULNERABILITIES=0 - - # PHP Dependencies - if [ -f "composer.json" ]; then - echo "### PHP Dependencies (composer)" >> $GITHUB_STEP_SUMMARY - if composer audit --no-dev 2>&1 | tee /tmp/php_audit.txt; then - echo "โœ… No PHP vulnerabilities detected" >> $GITHUB_STEP_SUMMARY - else - VULN_COUNT=$(grep -c "vulnerability" /tmp/php_audit.txt || echo 0) - echo "โš ๏ธ Found $VULN_COUNT PHP vulnerability/vulnerabilities" >> $GITHUB_STEP_SUMMARY - VULNERABILITIES=$((VULNERABILITIES + VULN_COUNT)) - fi - echo "" >> $GITHUB_STEP_SUMMARY - fi - - # Python Dependencies - if [ -f "requirements.txt" ]; then - echo "### Python Dependencies" >> $GITHUB_STEP_SUMMARY - pip install pip-audit 2>&1 > /dev/null - if pip-audit -r requirements.txt 2>&1 | tee /tmp/py_audit.txt; then - echo "โœ… No Python vulnerabilities detected" >> $GITHUB_STEP_SUMMARY - else - VULN_COUNT=$(grep -c "vulnerability" /tmp/py_audit.txt || echo 0) - echo "โš ๏ธ Found $VULN_COUNT Python vulnerability/vulnerabilities" >> $GITHUB_STEP_SUMMARY - VULNERABILITIES=$((VULNERABILITIES + VULN_COUNT)) - fi - echo "" >> $GITHUB_STEP_SUMMARY - fi - - # NPM Dependencies - if [ -f "package.json" ]; then - echo "### NPM Dependencies" >> $GITHUB_STEP_SUMMARY - if npm audit --production 2>&1 | tee /tmp/npm_audit.txt; then - echo "โœ… No NPM vulnerabilities detected" >> $GITHUB_STEP_SUMMARY - else - VULN_COUNT=$(grep -c "vulnerability" /tmp/npm_audit.txt || echo 0) - echo "โš ๏ธ Found $VULN_COUNT NPM vulnerability/vulnerabilities" >> $GITHUB_STEP_SUMMARY - VULNERABILITIES=$((VULNERABILITIES + VULN_COUNT)) - fi - echo "" >> $GITHUB_STEP_SUMMARY - fi - - if [ "$VULNERABILITIES" -gt 0 ]; then - echo "**Total Vulnerabilities**: $VULNERABILITIES" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Action Required**: Update vulnerable dependencies" >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "โœ… No dependency vulnerabilities detected" >> $GITHUB_STEP_SUMMARY - fi - - unused-dependencies: - name: Unused Dependencies Check - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Setup PHP - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 - with: - php-version: '8.1' - - - name: Check Unused Dependencies - run: | - set -x - echo "## ๐Ÿ“ฆ Unused Dependencies Check" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ -f "composer.json" ]; then - echo "### PHP Dependencies" >> $GITHUB_STEP_SUMMARY - - # Install composer-unused - composer global require icanhazstring/composer-unused 2>/dev/null || true - - if composer global exec composer-unused 2>&1 | tee /tmp/unused.txt; then - UNUSED_COUNT=$(grep "unused" /tmp/unused.txt | wc -l || echo 0) - if [ "$UNUSED_COUNT" -gt 0 ]; then - echo "โš ๏ธ Found $UNUSED_COUNT unused dependency/dependencies" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "View unused dependencies" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - cat /tmp/unused.txt >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - else - echo "โœ… No unused dependencies detected" >> $GITHUB_STEP_SUMMARY - fi - else - echo "โœ… All dependencies appear to be in use" >> $GITHUB_STEP_SUMMARY - fi - else - echo "โ„น๏ธ No composer.json found" >> $GITHUB_STEP_SUMMARY - fi - - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Recommendation**: Remove unused dependencies to reduce attack surface" >> $GITHUB_STEP_SUMMARY - - broken-link-detection: - name: Broken Link Detection - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Check Internal Links - run: | - set -x - echo "## ๐Ÿ”— Broken Link Detection" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Checking internal links in markdown files." >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - BROKEN_LINKS=0 - CHECKED_LINKS=0 - - # Find all markdown files - MD_FILES=$(find . -name "*.md" ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*") - - for file in $MD_FILES; do - # Extract markdown links [text](path) - while IFS= read -r line; do - # Extract path from [text](path) - link=$(echo "$line" | sed -n 's/.*\](\([^)]*\)).*/\1/p') - - # Skip external links (http/https) - if echo "$link" | grep -qE "^https?://"; then - continue - fi - - # Skip anchors only - if echo "$link" | grep -qE "^#"; then - continue - fi - - CHECKED_LINKS=$((CHECKED_LINKS + 1)) - - # Get directory of the markdown file - basedir=$(dirname "$file") - - # Resolve relative path - if [ -n "$link" ]; then - # Remove anchor if present - clean_link=$(echo "$link" | sed 's/#.*//') - - # Check if file exists - if [ ! -e "$basedir/$clean_link" ] && [ ! -e "$clean_link" ]; then - echo "Broken link in $file: $link" >> /tmp/broken_links.txt - BROKEN_LINKS=$((BROKEN_LINKS + 1)) - fi - fi - done < <(grep -o '\[.*\](.*)' "$file" 2>/dev/null || true) - done - - echo "### Link Validation Results" >> $GITHUB_STEP_SUMMARY - echo "- **Links Checked**: $CHECKED_LINKS" >> $GITHUB_STEP_SUMMARY - echo "- **Broken Links**: $BROKEN_LINKS" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ "$BROKEN_LINKS" -gt 0 ]; then - echo "โš ๏ธ Found $BROKEN_LINKS broken internal link(s)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "View broken links" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - cat /tmp/broken_links.txt 2>/dev/null >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Recommendation**: Fix or remove broken links to maintain documentation quality" >> $GITHUB_STEP_SUMMARY - else - if [ "$CHECKED_LINKS" -gt 0 ]; then - echo "โœ… All internal links are valid" >> $GITHUB_STEP_SUMMARY - else - echo "โ„น๏ธ No internal links found to check" >> $GITHUB_STEP_SUMMARY - fi - fi - - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Note**: This check validates internal file references only. External URLs are not validated." >> $GITHUB_STEP_SUMMARY - - # ============================================================================ - # PHASE 2: Medium Priority Checks - # ============================================================================ - - api-documentation: - name: API Documentation Coverage - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Check Documentation - run: | - set -x - echo "## ๐Ÿ“š API Documentation Coverage" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Count public functions/classes - PUBLIC_METHODS=$(grep -r "public function" . --include="*.php" ! -path "./vendor/*" | wc -l || echo 0) - DOCUMENTED=$(grep -B5 -r "public function" . --include="*.php" ! -path "./vendor/*" | grep -c "/\*\*" || echo 0) - - if [ "$PUBLIC_METHODS" -gt 0 ]; then - COVERAGE=$((DOCUMENTED * 100 / PUBLIC_METHODS)) - echo "**Documentation Coverage**: $COVERAGE% ($DOCUMENTED/$PUBLIC_METHODS)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ "$COVERAGE" -lt 80 ]; then - echo "โš ๏ธ Documentation coverage below 80% threshold" >> $GITHUB_STEP_SUMMARY - echo "**Recommendation**: Add PHPDoc blocks to public methods" >> $GITHUB_STEP_SUMMARY - else - echo "โœ… Good documentation coverage" >> $GITHUB_STEP_SUMMARY - fi - else - echo "โ„น๏ธ No public methods found for documentation check" >> $GITHUB_STEP_SUMMARY - fi - - accessibility-check: - name: Accessibility Check - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Check Accessibility - run: | - set -x - echo "## โ™ฟ Accessibility Check" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - HTML_COUNT=$(find . -name "*.html" ! -path "./vendor/*" ! -path "./.git/*" ! -path "./node_modules/*" | wc -l || echo 0) - MD_IMG_COUNT=$(find . -name "*.md" ! -path "./vendor/*" ! -path "./.git/*" -exec grep -l "!\[" {} + 2>/dev/null | wc -l || echo 0) - - if [ "$HTML_COUNT" -gt 0 ] || [ "$MD_IMG_COUNT" -gt 0 ]; then - # Check for images without alt text - MISSING_ALT=0 - - if [ "$HTML_COUNT" -gt 0 ]; then - MISSING_ALT=$(grep -r "> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ "$MISSING_ALT" -gt 0 ]; then - echo "โš ๏ธ Found images without alt text" >> $GITHUB_STEP_SUMMARY - echo "**Recommendation**: Add descriptive alt text for accessibility" >> $GITHUB_STEP_SUMMARY - else - echo "โœ… All images have alt text" >> $GITHUB_STEP_SUMMARY - fi - else - echo "โ„น๏ธ No HTML files found for accessibility check" >> $GITHUB_STEP_SUMMARY - fi - - performance-metrics: - name: Performance Metrics - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Check Performance Metrics - run: | - set -x - echo "## โšก Performance Metrics" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Check if JavaScript bundles exist - if [ -f "package.json" ]; then - echo "### Bundle Analysis" >> $GITHUB_STEP_SUMMARY - - # Check for common bundle files - BUNDLE_SIZE=0 - if [ -d "dist" ]; then - BUNDLE_SIZE=$(du -sb dist/ 2>/dev/null | cut -f1 || echo 0) - elif [ -d "build" ]; then - BUNDLE_SIZE=$(du -sb build/ 2>/dev/null | cut -f1 || echo 0) - fi - - if [ "$BUNDLE_SIZE" -gt 0 ]; then - BUNDLE_MB=$((BUNDLE_SIZE / 1024 / 1024)) - echo "**Bundle Size**: ${BUNDLE_MB}MB" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ "$BUNDLE_MB" -gt 5 ]; then - echo "โš ๏ธ Bundle size exceeds 5MB threshold" >> $GITHUB_STEP_SUMMARY - echo "**Recommendation**: Optimize bundle size" >> $GITHUB_STEP_SUMMARY - else - echo "โœ… Bundle size within acceptable limits" >> $GITHUB_STEP_SUMMARY - fi - else - echo "โ„น๏ธ No build artifacts found" >> $GITHUB_STEP_SUMMARY - fi - else - echo "โ„น๏ธ Not a JavaScript project" >> $GITHUB_STEP_SUMMARY - fi - - enterprise-readiness: - name: Enterprise Readiness Check - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Set up PHP - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 - with: - php-version: '8.1' - extensions: json, mbstring - tools: composer - coverage: none - - - name: Install API Package - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' - run: | - if [ -f "composer.json" ]; then - composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader - else - echo "No composer.json โ€” pulling MokoStandards tools" - if [ ! -d "/tmp/mokostandards" ]; then - git clone --depth 1 --branch version/04 --quiet \ - "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ - /tmp/mokostandards 2>/dev/null || true - if [ -f "/tmp/mokostandards/composer.json" ]; then - cd /tmp/mokostandards && composer install --no-dev --no-interaction --quiet 2>/dev/null || true - cd - - fi - fi - fi - - - name: Check Enterprise Readiness - id: enterprise_check - run: | - echo "" >> $GITHUB_STEP_SUMMARY - - SCRIPT="" - if [ -f "api/validate/check_enterprise_readiness.php" ]; then - SCRIPT="api/validate/check_enterprise_readiness.php" - elif [ -f "/tmp/mokostandards/api/validate/check_enterprise_readiness.php" ]; then - SCRIPT="/tmp/mokostandards/api/validate/check_enterprise_readiness.php" - fi - - if [ -n "$SCRIPT" ]; then - php "$SCRIPT" --verbose | tee /tmp/enterprise-check.log - EXIT_CODE=$? - - echo "" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - cat /tmp/enterprise-check.log >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - - if [ "$EXIT_CODE" -eq 0 ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "โœ… Repository meets enterprise readiness criteria!" >> $GITHUB_STEP_SUMMARY - exit 0 - else - echo "" >> $GITHUB_STEP_SUMMARY - echo "โš ๏ธ Enterprise readiness issues detected" >> $GITHUB_STEP_SUMMARY - echo "**Note:** This is informational - review recommendations to improve" >> $GITHUB_STEP_SUMMARY - exit 0 # Non-blocking - fi - else - echo "โ„น๏ธ Enterprise readiness check script not found - skipping" >> $GITHUB_STEP_SUMMARY - exit 0 - fi - - repository-health: - name: Repository Health Check - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Set up PHP - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 - with: - php-version: '8.1' - extensions: json, mbstring - tools: composer - coverage: none - - - name: Install API Package - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' - run: | - if [ -f "composer.json" ]; then - composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader - else - echo "No composer.json โ€” pulling MokoStandards tools" - if [ ! -d "/tmp/mokostandards" ]; then - git clone --depth 1 --branch version/04 --quiet \ - "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ - /tmp/mokostandards 2>/dev/null || true - if [ -f "/tmp/mokostandards/composer.json" ]; then - cd /tmp/mokostandards && composer install --no-dev --no-interaction --quiet 2>/dev/null || true - cd - - fi - fi - fi - - - name: Check Repository Health - id: health_check - run: | - echo "" >> $GITHUB_STEP_SUMMARY - - SCRIPT="" - if [ -f "api/validate/check_repo_health.php" ]; then - SCRIPT="api/validate/check_repo_health.php" - elif [ -f "/tmp/mokostandards/api/validate/check_repo_health.php" ]; then - SCRIPT="/tmp/mokostandards/api/validate/check_repo_health.php" - fi - - if [ -n "$SCRIPT" ]; then - php "$SCRIPT" --verbose | tee /tmp/health-check.log - EXIT_CODE=$? - - echo "" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - cat /tmp/health-check.log >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - - if [ "$EXIT_CODE" -eq 0 ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "โœ… Repository health check passed!" >> $GITHUB_STEP_SUMMARY - exit 0 - else - echo "" >> $GITHUB_STEP_SUMMARY - echo "โš ๏ธ Repository health issues detected" >> $GITHUB_STEP_SUMMARY - echo "**Note:** This is informational - review recommendations to improve" >> $GITHUB_STEP_SUMMARY - exit 0 # Non-blocking - fi - else - echo "โ„น๏ธ Repository health check script not found - skipping" >> $GITHUB_STEP_SUMMARY - exit 0 - fi - - terraform-validation: - name: Terraform Configuration Validation - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Setup Terraform - uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0 - with: - terraform_version: "1.0" - - - name: Validate Terraform Files - run: | - set -x - echo "## ๐Ÿ—๏ธ Terraform Configuration Validation" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Check if terraform files exist - TF_COUNT=$(find . -name "*.tf" -type f | wc -l || echo 0) - - if [ "$TF_COUNT" -eq 0 ]; then - echo "โ„น๏ธ No Terraform files found in repository" >> $GITHUB_STEP_SUMMARY - exit 0 - fi - - echo "**Terraform Files Found**: $TF_COUNT" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Validation Results - VALIDATION_PASSED=true - WARNINGS=0 - ERRORS=0 - - # 1. Check .github/config.tf location (not root override files) - echo "### Override Configuration Check" >> $GITHUB_STEP_SUMMARY - LEGACY_OVERRIDES=$(find . -maxdepth 1 -name "*override*.tf" -o -name "MokoStandards.override.tf" 2>/dev/null | wc -l || echo 0) - if [ "$LEGACY_OVERRIDES" -gt 0 ]; then - echo "โš ๏ธ Found legacy override files in root directory" >> $GITHUB_STEP_SUMMARY - echo "**Expected Location**: .github/config.tf" >> $GITHUB_STEP_SUMMARY - echo "**Legacy files found**: $LEGACY_OVERRIDES" >> $GITHUB_STEP_SUMMARY - WARNINGS=$((WARNINGS + 1)) - else - if [ -f ".github/config.tf" ]; then - echo "โœ… Override configuration in correct location (.github/config.tf)" >> $GITHUB_STEP_SUMMARY - else - echo "โ„น๏ธ No override configuration found" >> $GITHUB_STEP_SUMMARY - fi - fi - echo "" >> $GITHUB_STEP_SUMMARY - - # 2. Terraform Syntax Validation - echo "### Terraform Syntax Validation" >> $GITHUB_STEP_SUMMARY - SYNTAX_ERRORS=0 - - # Find all directories with terraform files - for dir in $(find . -name "*.tf" -type f -exec dirname {} \; | sort -u); do - cd "$dir" || continue - echo "Validating: $dir" >> $GITHUB_STEP_SUMMARY - - # Initialize without backend - terraform init -backend=false > /dev/null 2>&1 || true - - # Validate - if terraform validate -no-color > /tmp/tf_validate.txt 2>&1; then - echo " โœ… Syntax valid" >> $GITHUB_STEP_SUMMARY - else - echo " โŒ Syntax errors found" >> $GITHUB_STEP_SUMMARY - cat /tmp/tf_validate.txt >> $GITHUB_STEP_SUMMARY - SYNTAX_ERRORS=$((SYNTAX_ERRORS + 1)) - VALIDATION_PASSED=false - fi - cd - > /dev/null - done - echo "" >> $GITHUB_STEP_SUMMARY - - if [ "$SYNTAX_ERRORS" -eq 0 ]; then - echo "โœ… All Terraform files have valid syntax" >> $GITHUB_STEP_SUMMARY - else - echo "โŒ Found $SYNTAX_ERRORS directories with syntax errors" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS + SYNTAX_ERRORS)) - fi - echo "" >> $GITHUB_STEP_SUMMARY - - # 3. Terraform Formatting Check - echo "### Terraform Formatting Check" >> $GITHUB_STEP_SUMMARY - FORMAT_ISSUES=0 - - for tf_file in $(find . -name "*.tf" -type f); do - if ! terraform fmt -check=true -no-color "$tf_file" > /dev/null 2>&1; then - FORMAT_ISSUES=$((FORMAT_ISSUES + 1)) - fi - done - - if [ "$FORMAT_ISSUES" -eq 0 ]; then - echo "โœ… All Terraform files properly formatted" >> $GITHUB_STEP_SUMMARY - else - echo "โš ๏ธ Found $FORMAT_ISSUES files with formatting issues" >> $GITHUB_STEP_SUMMARY - echo "**Fix**: Run \`terraform fmt -recursive\`" >> $GITHUB_STEP_SUMMARY - WARNINGS=$((WARNINGS + 1)) - fi - echo "" >> $GITHUB_STEP_SUMMARY - - # 4. Check for file_metadata blocks - echo "### File Metadata Validation" >> $GITHUB_STEP_SUMMARY - MISSING_METADATA=0 - - for tf_file in $(find . -name "*.tf" -type f); do - if ! grep -q "file_metadata" "$tf_file"; then - MISSING_METADATA=$((MISSING_METADATA + 1)) - fi - done - - if [ "$MISSING_METADATA" -eq 0 ]; then - echo "โœ… All Terraform files contain file_metadata block" >> $GITHUB_STEP_SUMMARY - else - echo "โš ๏ธ Found $MISSING_METADATA files missing file_metadata block" >> $GITHUB_STEP_SUMMARY - echo "**Reference**: docs/policy/terraform-file-standards.md" >> $GITHUB_STEP_SUMMARY - WARNINGS=$((WARNINGS + 1)) - fi - echo "" >> $GITHUB_STEP_SUMMARY - - # 5. Version Consistency Check - echo "### Version Consistency Check" >> $GITHUB_STEP_SUMMARY - VERSION_MISMATCHES=0 - EXPECTED_VERSION="04.00.04" - - for tf_file in $(find . -name "*.tf" -type f); do - if grep -q "version.*=" "$tf_file"; then - if ! grep -q "version.*=.*\"$EXPECTED_VERSION\"" "$tf_file"; then - VERSION_MISMATCHES=$((VERSION_MISMATCHES + 1)) - fi - fi - done - - if [ "$VERSION_MISMATCHES" -eq 0 ]; then - echo "โœ… All Terraform file versions match $EXPECTED_VERSION" >> $GITHUB_STEP_SUMMARY - else - echo "โš ๏ธ Found $VERSION_MISMATCHES files with version mismatches" >> $GITHUB_STEP_SUMMARY - echo "**Expected Version**: $EXPECTED_VERSION" >> $GITHUB_STEP_SUMMARY - WARNINGS=$((WARNINGS + 1)) - fi - echo "" >> $GITHUB_STEP_SUMMARY - - # 6. Copyright Header Check - echo "### Copyright Header Check" >> $GITHUB_STEP_SUMMARY - MISSING_COPYRIGHT=0 - - for tf_file in $(find . -name "*.tf" -type f); do - if ! grep -q "Copyright (C)" "$tf_file"; then - MISSING_COPYRIGHT=$((MISSING_COPYRIGHT + 1)) - fi - done - - if [ "$MISSING_COPYRIGHT" -eq 0 ]; then - echo "โœ… All Terraform files have copyright headers" >> $GITHUB_STEP_SUMMARY - else - echo "โš ๏ธ Found $MISSING_COPYRIGHT files missing copyright headers" >> $GITHUB_STEP_SUMMARY - echo "**Reference**: docs/policy/terraform-file-standards.md" >> $GITHUB_STEP_SUMMARY - WARNINGS=$((WARNINGS + 1)) - fi - echo "" >> $GITHUB_STEP_SUMMARY - - # Summary - echo "---" >> $GITHUB_STEP_SUMMARY - echo "### Validation Summary" >> $GITHUB_STEP_SUMMARY - echo "**Total Files**: $TF_COUNT" >> $GITHUB_STEP_SUMMARY - echo "**Errors**: $ERRORS" >> $GITHUB_STEP_SUMMARY - echo "**Warnings**: $WARNINGS" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ "$VALIDATION_PASSED" = true ] && [ "$ERRORS" -eq 0 ]; then - echo "โœ… **Terraform Validation: PASSED**" >> $GITHUB_STEP_SUMMARY - exit 0 - elif [ "$ERRORS" -gt 0 ]; then - echo "โŒ **Terraform Validation: FAILED**" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Note**: This is an informational check and does not block merges" >> $GITHUB_STEP_SUMMARY - exit 0 # Informational only - else - echo "โš ๏ธ **Terraform Validation: PASSED WITH WARNINGS**" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Note**: This is an informational check and does not block merges" >> $GITHUB_STEP_SUMMARY - exit 0 # Informational only - fi - - summary: - name: Compliance Summary - runs-on: ubuntu-latest - needs: [ - repository-structure, documentation-quality, coding-standards, line-length-validation, license-compliance, git-hygiene, workflow-validation, version-consistency, script-integrity, enterprise-readiness, repository-health, - todo-fixme-tracking, file-size-limits, secret-scanning, broken-link-detection, - dependency-vulnerabilities, code-duplication, unused-dependencies, readme-completeness, - code-complexity, api-documentation, insecure-patterns, binary-file-detection, - dead-code-detection, file-naming-standards, accessibility-check, performance-metrics, terraform-validation - ] - if: always() - - steps: - - name: Generate Compliance Report - run: | - set -x - echo "# ๐Ÿ“Š MokoStandards Compliance Report" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Calculate overall status - REPO_STATUS="${{ needs.repository-structure.result }}" - DOCS_STATUS="${{ needs.documentation-quality.result }}" - CODE_STATUS="${{ needs.coding-standards.result }}" - LINE_LENGTH_STATUS="${{ needs.line-length-validation.result }}" - LICENSE_STATUS="${{ needs.license-compliance.result }}" - GIT_STATUS="${{ needs.git-hygiene.result }}" - WORKFLOW_STATUS="${{ needs.workflow-validation.result }}" - VERSION_STATUS="${{ needs.version-consistency.result }}" - SCRIPT_STATUS="${{ needs.script-integrity.result }}" - ENTERPRISE_STATUS="${{ needs.enterprise-readiness.result }}" - HEALTH_STATUS="${{ needs.repository-health.result }}" - TERRAFORM_STATUS="${{ needs.terraform-validation.result }}" - - PASSED=0 - FAILED=0 - WARNINGS=0 - TOTAL=28 - - # Critical checks (must pass) - [ "$REPO_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) - [ "$DOCS_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) - [ "$CODE_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) - [ "$LICENSE_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) - [ "$GIT_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) - [ "$WORKFLOW_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) - [ "$VERSION_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) - [ "$SCRIPT_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) - - # Informational checks (don't fail build) - if [ "$ENTERPRISE_STATUS" = "success" ]; then - PASSED=$((PASSED + 1)) - else - WARNINGS=$((WARNINGS + 1)) - fi - - if [ "$HEALTH_STATUS" = "success" ]; then - PASSED=$((PASSED + 1)) - else - WARNINGS=$((WARNINGS + 1)) - fi - - if [ "$TERRAFORM_STATUS" = "success" ]; then - PASSED=$((PASSED + 1)) - else - WARNINGS=$((WARNINGS + 1)) - fi - - # Adjust total to only count critical checks for compliance percentage - CRITICAL_TOTAL=8 - CRITICAL_PASSED=$((PASSED - WARNINGS)) - COMPLIANCE_PERCENT=$((CRITICAL_PASSED * 100 / CRITICAL_TOTAL)) - - # Overall status badge - if [ "$COMPLIANCE_PERCENT" -eq 100 ]; then - echo "## โœ… Overall Status: **COMPLIANT** ($COMPLIANCE_PERCENT%)" >> $GITHUB_STEP_SUMMARY - elif [ "$COMPLIANCE_PERCENT" -ge 80 ]; then - echo "## โš ๏ธ Overall Status: **MOSTLY COMPLIANT** ($COMPLIANCE_PERCENT%)" >> $GITHUB_STEP_SUMMARY - elif [ "$COMPLIANCE_PERCENT" -ge 50 ]; then - echo "## โš ๏ธ Overall Status: **PARTIALLY COMPLIANT** ($COMPLIANCE_PERCENT%)" >> $GITHUB_STEP_SUMMARY - else - echo "## โŒ Overall Status: **NON-COMPLIANT** ($COMPLIANCE_PERCENT%)" >> $GITHUB_STEP_SUMMARY - fi - - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Critical Checks:** $CRITICAL_PASSED/$CRITICAL_TOTAL passed" >> $GITHUB_STEP_SUMMARY - echo "**Total Checks:** $PASSED/$TOTAL passed" >> $GITHUB_STEP_SUMMARY - if [ "$WARNINGS" -gt 0 ]; then - echo "**Informational:** $WARNINGS warning(s)" >> $GITHUB_STEP_SUMMARY - fi - echo "" >> $GITHUB_STEP_SUMMARY - - # Progress bar - FILLED=$((COMPLIANCE_PERCENT / 5)) - EMPTY=$((20 - FILLED)) - BAR="" - for i in $(seq 1 $FILLED); do BAR="${BAR}โ–ˆ"; done - for i in $(seq 1 $EMPTY); do BAR="${BAR}โ–‘"; done - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "$BAR $COMPLIANCE_PERCENT%" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Detailed breakdown - echo "## Validation Results" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Area | Status | Result | Priority |" >> $GITHUB_STEP_SUMMARY - echo "|------|--------|--------|----------|" >> $GITHUB_STEP_SUMMARY - - # Repository Structure - if [ "$REPO_STATUS" = "success" ]; then - echo "| ๐Ÿ“ Repository Structure | โœ… Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY - else - echo "| ๐Ÿ“ Repository Structure | โŒ Fail | **Action Required** | ๐Ÿ”ด Critical |" >> $GITHUB_STEP_SUMMARY - fi - - # Documentation Quality - if [ "$DOCS_STATUS" = "success" ]; then - echo "| ๐Ÿ“š Documentation Quality | โœ… Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY - else - echo "| ๐Ÿ“š Documentation Quality | โŒ Fail | **Action Required** | ๐Ÿ”ด Critical |" >> $GITHUB_STEP_SUMMARY - fi - - # Coding Standards - if [ "$CODE_STATUS" = "success" ]; then - echo "| ๐Ÿ’ป Coding Standards | โœ… Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY - else - echo "| ๐Ÿ’ป Coding Standards | โš ๏ธ Warning | Review Recommended | ๐ŸŸก Medium |" >> $GITHUB_STEP_SUMMARY - fi - - # License Compliance - if [ "$LICENSE_STATUS" = "success" ]; then - echo "| โš–๏ธ License Compliance | โœ… Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY - else - echo "| โš–๏ธ License Compliance | โŒ Fail | **Action Required** | ๐Ÿ”ด Critical |" >> $GITHUB_STEP_SUMMARY - fi - - # Git Hygiene - if [ "$GIT_STATUS" = "success" ]; then - echo "| ๐Ÿงน Git Repository Hygiene | โœ… Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY - else - echo "| ๐Ÿงน Git Repository Hygiene | โš ๏ธ Warning | Review Recommended | ๐ŸŸก Medium |" >> $GITHUB_STEP_SUMMARY - fi - - # Workflow Configuration - if [ "$WORKFLOW_STATUS" = "success" ]; then - echo "| โš™๏ธ Workflow Configuration | โœ… Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY - else - echo "| โš™๏ธ Workflow Configuration | โš ๏ธ Warning | Review Recommended | ๐ŸŸก Medium |" >> $GITHUB_STEP_SUMMARY - fi - - # Version Consistency - if [ "$VERSION_STATUS" = "success" ]; then - echo "| ๐Ÿ”ข Version Consistency | โœ… Pass | All versions match | - |" >> $GITHUB_STEP_SUMMARY - else - echo "| ๐Ÿ”ข Version Consistency | โŒ Fail | **Action Required** | ๐Ÿ”ด Critical |" >> $GITHUB_STEP_SUMMARY - fi - - # Script Integrity - if [ "$SCRIPT_STATUS" = "success" ]; then - echo "| ๐Ÿ” Script Integrity | โœ… Pass | SHA hashes validated | - |" >> $GITHUB_STEP_SUMMARY - else - echo "| ๐Ÿ” Script Integrity | โŒ Fail | **Action Required** | ๐Ÿ”ด Critical |" >> $GITHUB_STEP_SUMMARY - fi - - # Enterprise Readiness (Informational) - if [ "$ENTERPRISE_STATUS" = "success" ]; then - echo "| ๐Ÿข Enterprise Readiness | โœ… Pass | Ready for enterprise | โ„น๏ธ Info |" >> $GITHUB_STEP_SUMMARY - else - echo "| ๐Ÿข Enterprise Readiness | โ„น๏ธ Info | Review suggestions | โ„น๏ธ Info |" >> $GITHUB_STEP_SUMMARY - fi - - # Repository Health (Informational) - if [ "$HEALTH_STATUS" = "success" ]; then - echo "| ๐Ÿฅ Repository Health | โœ… Pass | Health check passed | โ„น๏ธ Info |" >> $GITHUB_STEP_SUMMARY - else - echo "| ๐Ÿฅ Repository Health | โ„น๏ธ Info | Review recommendations | โ„น๏ธ Info |" >> $GITHUB_STEP_SUMMARY - fi - - echo "" >> $GITHUB_STEP_SUMMARY - - # Action items summary - if [ "$FAILED" -gt 0 ]; then - echo "## โšก Action Items" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**$FAILED validation area(s) require attention:**" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - [ "$REPO_STATUS" != "success" ] && echo "- ๐Ÿ”ด **Critical:** Fix repository structure issues" >> $GITHUB_STEP_SUMMARY - [ "$DOCS_STATUS" != "success" ] && echo "- ๐Ÿ”ด **Critical:** Improve documentation quality" >> $GITHUB_STEP_SUMMARY - [ "$LICENSE_STATUS" != "success" ] && echo "- ๐Ÿ”ด **Critical:** Resolve license compliance issues" >> $GITHUB_STEP_SUMMARY - [ "$CODE_STATUS" != "success" ] && echo "- ๐ŸŸก **Medium:** Review coding standards violations" >> $GITHUB_STEP_SUMMARY - [ "$GIT_STATUS" != "success" ] && echo "- ๐ŸŸก **Medium:** Address git repository hygiene items" >> $GITHUB_STEP_SUMMARY - [ "$WORKFLOW_STATUS" != "success" ] && echo "- ๐ŸŸก **Medium:** Review workflow configuration" >> $GITHUB_STEP_SUMMARY - - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Next Steps:**" >> $GITHUB_STEP_SUMMARY - echo "1. Review detailed results in individual job outputs above" >> $GITHUB_STEP_SUMMARY - echo "2. Follow remediation steps provided for each failure" >> $GITHUB_STEP_SUMMARY - echo "3. Re-run this workflow after making corrections" >> $GITHUB_STEP_SUMMARY - echo "4. Reach 100% compliance before merging" >> $GITHUB_STEP_SUMMARY - else - echo "## ๐ŸŽ‰ Excellent!" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Your repository is **fully compliant** with MokoStandards!" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Achievements:**" >> $GITHUB_STEP_SUMMARY - echo "- โœ… All required directories and files present" >> $GITHUB_STEP_SUMMARY - echo "- โœ… Documentation meets quality standards" >> $GITHUB_STEP_SUMMARY - echo "- โœ… Coding standards followed" >> $GITHUB_STEP_SUMMARY - echo "- โœ… License compliance verified" >> $GITHUB_STEP_SUMMARY - echo "- โœ… Git repository well-maintained" >> $GITHUB_STEP_SUMMARY - echo "- โœ… Workflows properly configured" >> $GITHUB_STEP_SUMMARY - fi - - echo "" >> $GITHUB_STEP_SUMMARY - echo "---" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "๐Ÿ“š **Resources:**" >> $GITHUB_STEP_SUMMARY - echo "- [MokoStandards Documentation](https://github.com/mokoconsulting-tech/MokoStandards)" >> $GITHUB_STEP_SUMMARY - echo "- [Repository Structure Guide](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/docs/policy/core-structure.md)" >> $GITHUB_STEP_SUMMARY - echo "- [Documentation Standards](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/docs/policy/document-formatting.md)" >> $GITHUB_STEP_SUMMARY - echo "- [Coding Standards](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/docs/policy/coding-style-guide.md)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "_Generated by MokoStandards Compliance Workflow v${WORKFLOW_VERSION}_" >> $GITHUB_STEP_SUMMARY - - # Create tracking issue for non-compliance if on push - if [ "$COMPLIANCE_PERCENT" -lt 100 ] && [ "${{ github.event_name }}" = "push" ]; then - echo "Creating tracking issue for standards violations..." - fi - - # Exit with error if not fully compliant - if [ "$COMPLIANCE_PERCENT" -lt 100 ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "### โŒ Standards Compliance Failed" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Overall Compliance:** $COMPLIANCE_PERCENT%" >> $GITHUB_STEP_SUMMARY - echo "**Status:** Repository does not meet 100% compliance requirement" >> $GITHUB_STEP_SUMMARY - echo "**Action Required:** Review and fix all validation failures above" >> $GITHUB_STEP_SUMMARY - echo "" - echo "โŒ ERROR: Standards compliance at $COMPLIANCE_PERCENT% - 100% required" - exit 1 - fi - - echo "" >> $GITHUB_STEP_SUMMARY - echo "### โœ… Full Standards Compliance Achieved" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Overall Compliance:** 100%" >> $GITHUB_STEP_SUMMARY - echo "**Status:** Repository meets all MokoStandards requirements" >> $GITHUB_STEP_SUMMARY - echo "" - echo "โœ… SUCCESS: Repository is fully MokoStandards compliant" - - - name: Create or reopen tracking issue for standards violations - if: failure() - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - run: | - REPO="${{ github.repository }}" - RUN_URL="${{ github.server_url }}/${REPO}/actions/runs/${{ github.run_id }}" - DATE=$(date -u '+%Y-%m-%d') - SHA="${{ github.sha }}" - ACTOR="${{ github.actor }}" - BRANCH="${{ github.ref_name }}" - - # Collect failed checks - FAILED="" - [ "${{ needs.repository-structure.result }}" != "success" ] && FAILED="${FAILED}\n- Repository Structure" - [ "${{ needs.documentation-quality.result }}" != "success" ] && FAILED="${FAILED}\n- Documentation Quality" - [ "${{ needs.coding-standards.result }}" != "success" ] && FAILED="${FAILED}\n- Coding Standards" - [ "${{ needs.license-compliance.result }}" != "success" ] && FAILED="${FAILED}\n- License Compliance" - [ "${{ needs.git-hygiene.result }}" != "success" ] && FAILED="${FAILED}\n- Git Hygiene" - [ "${{ needs.workflow-validation.result }}" != "success" ] && FAILED="${FAILED}\n- Workflow Validation" - [ "${{ needs.version-consistency.result }}" != "success" ] && FAILED="${FAILED}\n- Version Consistency" - [ "${{ needs.script-integrity.result }}" != "success" ] && FAILED="${FAILED}\n- Script Integrity" - [ "${{ needs.secret-scanning.result }}" != "success" ] && FAILED="${FAILED}\n- Secret Scanning" - [ "${{ needs.line-length-validation.result }}" != "success" ] && FAILED="${FAILED}\n- Line Length" - [ "${{ needs.file-size-limits.result }}" != "success" ] && FAILED="${FAILED}\n- File Size Limits" - [ "${{ needs.readme-completeness.result }}" != "success" ] && FAILED="${FAILED}\n- README Completeness" - - if [ -z "$FAILED" ]; then - echo "No failed checks to report" - exit 0 - fi - - TITLE="[Standards] Compliance violations โ€” ${DATE}" - BODY="## Standards Compliance Violations - - | Field | Value | - |-------|-------| - | **Branch** | \`${BRANCH}\` | - | **Commit** | \`${SHA:0:7}\` | - | **Actor** | @${ACTOR} | - | **Run** | [View workflow](${RUN_URL}) | - - ### Failed Checks - $(printf '%b' "$FAILED") - - ### Required Actions - 1. Review the [workflow run](${RUN_URL}) for details - 2. Fix each failed check - 3. Push to trigger a new scan - - --- - *Auto-created by standards-compliance workflow*" - - BODY=$(echo "$BODY" | sed 's/^ //') - LABEL="standards-violation" - - gh label create "$LABEL" --repo "$REPO" --color "D73A4A" --description "Standards compliance failure" --force 2>/dev/null || true - - EXISTING=$(gh api "repos/${REPO}/issues?labels=${LABEL}&state=all&per_page=1&sort=created&direction=desc" \ - --jq '.[0].number' 2>/dev/null) - - if [ -n "$EXISTING" ] && [ "$EXISTING" != "null" ]; then - gh api "repos/${REPO}/issues/${EXISTING}" -X PATCH \ - -f title="$TITLE" -f body="$BODY" -f state="open" --silent - echo "Updated issue #${EXISTING}" - else - gh issue create --repo "$REPO" --title "$TITLE" --body "$BODY" \ - --label "$LABEL" --assignee "jmiller" - fi - -# CUSTOMIZATION: -# -# 1. Adjust severity of checks (convert warnings to errors or vice versa) -# 2. Add project-specific validation rules -# 3. Integrate with custom linting tools -# 4. Add notification steps for compliance failures -# 5. Customize required files/directories for your project type - diff --git a/.github/workflows/sync-version-on-merge.yml b/.github/workflows/sync-version-on-merge.yml deleted file mode 100644 index 60715f6..0000000 --- a/.github/workflows/sync-version-on-merge.yml +++ /dev/null @@ -1,133 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# This file is part of a Moko Consulting project. -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: GitHub.Workflow -# INGROUP: MokoStandards.Automation -# REPO: https://github.com/mokoconsulting-tech/MokoStandards -# PATH: /templates/workflows/shared/sync-version-on-merge.yml.template -# VERSION: 04.06.00 -# BRIEF: Auto-bump patch version on every push to main and propagate to all file headers -# NOTE: Synced via bulk-repo-sync to .github/workflows/sync-version-on-merge.yml in all governed repos. -# README.md is the single source of truth for the repository version. - -name: Sync Version from README - -on: - push: - branches: - - main - - master - workflow_dispatch: - inputs: - dry_run: - description: 'Dry run (preview only, no commit)' - type: boolean - default: false - -permissions: - contents: write - issues: write - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - sync-version: - name: Propagate README version - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.GH_TOKEN || github.token }} - fetch-depth: 0 - - - name: Set up PHP - uses: shivammathur/setup-php@fcafdd6392932010c2bd5094439b8e33be2a8a09 # v2.37.0 - with: - php-version: '8.1' - tools: composer - - - name: Setup MokoStandards tools - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' - run: | - git clone --depth 1 --branch version/04 --quiet \ - "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ - /tmp/mokostandards - cd /tmp/mokostandards - composer install --no-dev --no-interaction --quiet - - - name: Auto-bump patch version - if: ${{ github.event_name == 'push' && github.actor != 'github-actions[bot]' }} - run: | - if git diff --name-only HEAD~1 HEAD 2>/dev/null | grep -q '^README\.md$'; then - echo "README.md changed in this push โ€” skipping auto-bump" - exit 0 - fi - - RESULT=$(php /tmp/mokostandards/api/cli/version_bump.php --path .) || { - echo "โš ๏ธ Could not bump version โ€” skipping" - exit 0 - } - echo "Auto-bumping patch: $RESULT" - git config --local user.email "github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" - git add README.md - git commit -m "chore(version): auto-bump patch ${RESULT} [skip ci]" \ - --author="github-actions[bot] " - git push - - - name: Extract version from README.md - id: readme_version - run: | - git pull --ff-only 2>/dev/null || true - VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null) - if [ -z "$VERSION" ]; then - echo "โš ๏ธ No VERSION in README.md โ€” skipping propagation" - echo "skip=true" >> $GITHUB_OUTPUT - exit 0 - fi - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "skip=false" >> $GITHUB_OUTPUT - echo "โœ… README.md version: $VERSION" - - - name: Run version sync - if: ${{ steps.readme_version.outputs.skip != 'true' && inputs.dry_run != true }} - run: | - php /tmp/mokostandards/api/maintenance/update_version_from_readme.php \ - --path . \ - --create-issue \ - --repo "${{ github.repository }}" - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - - - name: Commit updated files - if: ${{ steps.readme_version.outputs.skip != 'true' && inputs.dry_run != true }} - run: | - git pull --ff-only 2>/dev/null || true - if git diff --quiet; then - echo "โ„น๏ธ No version changes needed โ€” already up to date" - exit 0 - fi - VERSION="${{ steps.readme_version.outputs.version }}" - git config --local user.email "github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" - git add -A - git commit -m "chore(version): sync badges and headers to ${VERSION} [skip ci]" \ - --author="github-actions[bot] " - git push - - - name: Summary - run: | - VERSION="${{ steps.readme_version.outputs.version }}" - echo "## ๐Ÿ“ฆ Version Sync โ€” ${VERSION}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Source:** \`README.md\` FILE INFORMATION block" >> $GITHUB_STEP_SUMMARY - echo "**Version:** \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY -- 2.52.0 From 551934f76bf3d0e31d396df196d433aa0fbbbf32 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sat, 2 May 2026 18:06:11 -0500 Subject: [PATCH 13/16] chore: add XML .mokostandards manifest --- .gitea/.mokostandards | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/.gitea/.mokostandards b/.gitea/.mokostandards index 80141b0..e7f4155 100644 --- a/.gitea/.mokostandards +++ b/.gitea/.mokostandards @@ -1 +1,25 @@ -platform: default-repository + + + + + joomla-api-mcp + MokoConsulting + MCP server for Joomla Web Services API operations + GNU General Public License v3 + + + waas-component + 04.07.00 + https://git.mokoconsulting.tech/MokoConsulting/MokoStandards + 2026-05-02T23:06:09+00:00 + + + Markdown + joomla-extension + + -- 2.52.0 From 67ea51a2565c34a1db1452be6faddb3229a531b5 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sat, 2 May 2026 18:13:19 -0500 Subject: [PATCH 14/16] chore: enrich .mokostandards with build/deploy/scripts --- .gitea/.mokostandards | 53 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/.gitea/.mokostandards b/.gitea/.mokostandards index e7f4155..5899fe8 100644 --- a/.gitea/.mokostandards +++ b/.gitea/.mokostandards @@ -22,4 +22,57 @@ Markdown joomla-extension + + + ${{ secrets.DEV_HOST }} + ${{ secrets.DEV_PATH }} + sftp + dev/** + src/ + + + ${{ secrets.DEMO_HOST }} + ${{ secrets.DEMO_PATH }} + sftp + main + src/ + + + + + + + + + + + -- 2.52.0 From a97a072be487173b108e37bef228d9b4e7b07b16 Mon Sep 17 00:00:00 2001 From: jmiller Date: Tue, 5 May 2026 16:55:56 -0500 Subject: [PATCH 15/16] =?UTF-8?q?ci:=20add=20cascade=20main=20=E2=86=92=20?= =?UTF-8?q?dev=20workflow=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/cascade-dev.yml | 184 +++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 .gitea/workflows/cascade-dev.yml diff --git a/.gitea/workflows/cascade-dev.yml b/.gitea/workflows/cascade-dev.yml new file mode 100644 index 0000000..54f9c37 --- /dev/null +++ b/.gitea/workflows/cascade-dev.yml @@ -0,0 +1,184 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Maintenance +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# PATH: /templates/workflows/cascade-dev.yml.template +# VERSION: 01.00.00 +# BRIEF: Forward-merge main โ†’ dev after every push to main +# +# +========================================================================+ +# | CASCADE MAIN โ†’ DEV | +# +========================================================================+ +# | | +# | Triggers on every push to main (PR merges, bot commits, etc.) | +# | | +# | 1. Check if a 'dev' branch exists | +# | 2. Create a PR (main โ†’ dev) via Gitea API | +# | 3. Auto-merge if clean; leave open for manual resolution on conflict | +# | | +# +========================================================================+ + +name: Cascade Main โ†’ Dev + +on: + push: + branches: + - main + workflow_dispatch: + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +permissions: + contents: write + pull-requests: write + +jobs: + cascade: + name: Merge main โ†’ dev + runs-on: ubuntu-latest + if: >- + !contains(github.event.head_commit.message, '[skip ci]') && + !contains(github.event.head_commit.message, '[skip cascade]') + + steps: + - name: Check dev branch exists + id: check + env: + GA_TOKEN: ${{ secrets.GA_TOKEN }} + run: | + API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + STATUS=$(curl -sS -o /dev/null -w "%{http_code}" \ + -H "Authorization: token ${GA_TOKEN}" \ + "${API}/branches/dev") + + if [ "$STATUS" = "200" ]; then + echo "exists=true" >> "$GITHUB_OUTPUT" + echo "โœ… dev branch exists" + else + echo "exists=false" >> "$GITHUB_OUTPUT" + echo "โ„น๏ธ No dev branch found (HTTP ${STATUS}) โ€” skipping cascade" + fi + + - name: Check if dev is already up to date + if: steps.check.outputs.exists == 'true' + id: diff + env: + GA_TOKEN: ${{ secrets.GA_TOKEN }} + run: | + API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + # Compare main..dev โ€” if ahead_by is 0 there's nothing to cascade + RESPONSE=$(curl -sS \ + -H "Authorization: token ${GA_TOKEN}" \ + "${API}/compare/dev...main") + + AHEAD=$(echo "$RESPONSE" | jq '.total_commits // 0') + + if [ "$AHEAD" -eq 0 ]; then + echo "needs_merge=false" >> "$GITHUB_OUTPUT" + echo "โœ… dev is already up to date with main" + else + echo "needs_merge=true" >> "$GITHUB_OUTPUT" + echo "โ„น๏ธ main is ${AHEAD} commit(s) ahead of dev" + fi + + - name: Create cascade PR + if: steps.diff.outputs.needs_merge == 'true' + id: pr + env: + GA_TOKEN: ${{ secrets.GA_TOKEN }} + run: | + API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + SHORT_SHA="${GITHUB_SHA:0:7}" + + # Check if a cascade PR already exists (main โ†’ dev) + EXISTING=$(curl -sS \ + -H "Authorization: token ${GA_TOKEN}" \ + "${API}/pulls?state=open&head=${GITEA_ORG}:main&base=dev&limit=1") + + EXISTING_COUNT=$(echo "$EXISTING" | jq 'length') + + if [ "$EXISTING_COUNT" -gt 0 ]; then + PR_NUMBER=$(echo "$EXISTING" | jq -r '.[0].number') + PR_URL=$(echo "$EXISTING" | jq -r '.[0].html_url') + echo "pr_number=${PR_NUMBER}" >> "$GITHUB_OUTPUT" + echo "pr_exists=true" >> "$GITHUB_OUTPUT" + echo "โ„น๏ธ Cascade PR already exists: ${PR_URL}" + else + RESPONSE=$(curl -sS -w "\n%{http_code}" \ + -X POST \ + -H "Authorization: token ${GA_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ + \"title\": \"chore: cascade main โ†’ dev (${SHORT_SHA}) [skip ci]\", + \"body\": \"## Automatic cascade\n\nForward-merging \`main\` (${SHORT_SHA}) into \`dev\` to keep branches in sync.\n\nIf this PR has conflicts, please resolve them manually and merge.\n\n> Auto-created by the **Cascade Main โ†’ Dev** workflow.\", + \"head\": \"main\", + \"base\": \"dev\" + }" \ + "${API}/pulls") + + HTTP_CODE=$(echo "$RESPONSE" | tail -1) + BODY=$(echo "$RESPONSE" | sed '$d') + PR_NUMBER=$(echo "$BODY" | jq -r '.number // empty') + PR_URL=$(echo "$BODY" | jq -r '.html_url // empty') + + if [ "$HTTP_CODE" = "201" ] && [ -n "$PR_NUMBER" ]; then + echo "pr_number=${PR_NUMBER}" >> "$GITHUB_OUTPUT" + echo "pr_exists=false" >> "$GITHUB_OUTPUT" + echo "โœ… Created cascade PR #${PR_NUMBER}: ${PR_URL}" + else + echo "โŒ Failed to create PR (HTTP ${HTTP_CODE}): ${BODY}" + exit 1 + fi + fi + + - name: Auto-merge cascade PR + if: steps.pr.outputs.pr_number != '' + env: + GA_TOKEN: ${{ secrets.GA_TOKEN }} + run: | + API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + PR_NUMBER="${{ steps.pr.outputs.pr_number }}" + + # Check if PR is mergeable + PR_DATA=$(curl -sS \ + -H "Authorization: token ${GA_TOKEN}" \ + "${API}/pulls/${PR_NUMBER}") + + MERGEABLE=$(echo "$PR_DATA" | jq -r '.mergeable // false') + + if [ "$MERGEABLE" != "true" ]; then + echo "โš ๏ธ PR #${PR_NUMBER} has conflicts โ€” leaving open for manual resolution" + exit 0 + fi + + # Merge the PR + RESPONSE=$(curl -sS -w "\n%{http_code}" \ + -X POST \ + -H "Authorization: token ${GA_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ + \"Do\": \"merge\", + \"merge_message_field\": \"chore: cascade main โ†’ dev [skip ci]\", + \"delete_branch_after_merge\": false + }" \ + "${API}/pulls/${PR_NUMBER}/merge") + + HTTP_CODE=$(echo "$RESPONSE" | tail -1) + + if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "204" ]; then + echo "โœ… Cascade PR #${PR_NUMBER} merged โ€” dev is now in sync with main" + else + BODY=$(echo "$RESPONSE" | sed '$d') + echo "โš ๏ธ Merge failed (HTTP ${HTTP_CODE}): ${BODY}" + echo "PR #${PR_NUMBER} left open for manual resolution" + fi -- 2.52.0 From b28f3bea96e151a2cae18d418769b49ebbcb046b Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 7 May 2026 14:22:43 -0500 Subject: [PATCH 16/16] =?UTF-8?q?feat(tools):=20expand=20to=2067=20tools?= =?UTF-8?q?=20=E2=80=94=20full=20CRUD=20for=20contacts,=20banners,=20newsf?= =?UTF-8?q?eeds,=20tags,=20fields,=20menu=20items,=20messages,=20media,=20?= =?UTF-8?q?redirects,=20associations,=20checkin,=20and=20content=20history?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- src/index.ts | 541 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 541 insertions(+) diff --git a/src/index.ts b/src/index.ts index 81335b0..0f274cb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -633,6 +633,547 @@ server.tool( }, ); +// โ”€โ”€ Contacts (CRUD) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +server.tool( + 'joomla_contact_get', + 'Get a single contact by ID', + { + id: z.number().describe('Contact ID'), + ...ConnectionParam, + }, + async ({ id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.get(`/contact/${id}`)); + }, +); + +server.tool( + 'joomla_contact_create', + 'Create a new contact', + { + name: z.string().describe('Contact name'), + alias: z.string().optional().describe('URL alias'), + catid: z.number().optional().describe('Category ID'), + email_to: z.string().optional().describe('Email address'), + telephone: z.string().optional().describe('Phone number'), + address: z.string().optional().describe('Street address'), + suburb: z.string().optional().describe('City/suburb'), + state: z.string().optional().describe('State/province'), + postcode: z.string().optional().describe('Postal code'), + country_id: z.number().optional().describe('Country ID'), + published: z.number().optional().describe('1=published, 0=unpublished'), + language: z.string().optional().describe('Language code (default "*")'), + ...ConnectionParam, + }, + async ({ name, alias, catid, email_to, telephone, address, suburb, state, postcode, country_id, published, language, connection }) => { + const client = clientFor(connection); + const body: Record = { name, language: language ?? '*' }; + if (alias) body.alias = alias; + if (catid !== undefined) body.catid = catid; + if (email_to) body.email_to = email_to; + if (telephone) body.telephone = telephone; + if (address) body.address = address; + if (suburb) body.suburb = suburb; + if (state) body.state = state; + if (postcode) body.postcode = postcode; + if (country_id !== undefined) body.country_id = country_id; + if (published !== undefined) body.published = published; + return formatResponse(await client.post('/contact', body)); + }, +); + +server.tool( + 'joomla_contact_update', + 'Update an existing contact', + { + id: z.number().describe('Contact ID'), + name: z.string().optional().describe('Contact name'), + email_to: z.string().optional().describe('Email address'), + telephone: z.string().optional().describe('Phone number'), + address: z.string().optional().describe('Street address'), + published: z.number().optional().describe('1=published, 0=unpublished'), + ...ConnectionParam, + }, + async ({ id, name, email_to, telephone, address, published, connection }) => { + const client = clientFor(connection); + const body: Record = {}; + if (name !== undefined) body.name = name; + if (email_to !== undefined) body.email_to = email_to; + if (telephone !== undefined) body.telephone = telephone; + if (address !== undefined) body.address = address; + if (published !== undefined) body.published = published; + return formatResponse(await client.patch(`/contact/${id}`, body)); + }, +); + +server.tool( + 'joomla_contact_delete', + 'Delete a contact', + { + id: z.number().describe('Contact ID'), + ...ConnectionParam, + }, + async ({ id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.delete(`/contact/${id}`)); + }, +); + +// โ”€โ”€ Banners (CRUD) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +server.tool( + 'joomla_banner_get', + 'Get a single banner by ID', + { + id: z.number().describe('Banner ID'), + ...ConnectionParam, + }, + async ({ id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.get(`/banners/${id}`)); + }, +); + +server.tool( + 'joomla_banner_create', + 'Create a new banner', + { + name: z.string().describe('Banner name'), + catid: z.number().optional().describe('Category ID'), + clickurl: z.string().optional().describe('Click URL'), + custombannercode: z.string().optional().describe('Custom HTML/code for the banner'), + state: z.number().optional().describe('1=published, 0=unpublished'), + ...ConnectionParam, + }, + async ({ name, catid, clickurl, custombannercode, state, connection }) => { + const client = clientFor(connection); + const body: Record = { name }; + if (catid !== undefined) body.catid = catid; + if (clickurl) body.clickurl = clickurl; + if (custombannercode) body.custombannercode = custombannercode; + if (state !== undefined) body.state = state; + return formatResponse(await client.post('/banners', body)); + }, +); + +server.tool( + 'joomla_banner_delete', + 'Delete a banner', + { + id: z.number().describe('Banner ID'), + ...ConnectionParam, + }, + async ({ id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.delete(`/banners/${id}`)); + }, +); + +// โ”€โ”€ Banner Clients โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +server.tool( + 'joomla_banner_clients_list', + 'List banner clients', + { ...ConnectionParam }, + async ({ connection }) => { + const client = clientFor(connection); + return formatResponse(await client.get('/banners/clients')); + }, +); + +// โ”€โ”€ Newsfeeds (CRUD) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +server.tool( + 'joomla_newsfeed_get', + 'Get a single newsfeed by ID', + { + id: z.number().describe('Newsfeed ID'), + ...ConnectionParam, + }, + async ({ id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.get(`/newsfeeds/${id}`)); + }, +); + +server.tool( + 'joomla_newsfeed_create', + 'Create a new newsfeed', + { + name: z.string().describe('Feed name'), + link: z.string().describe('Feed URL'), + catid: z.number().describe('Category ID'), + numarticles: z.number().optional().describe('Number of articles to display'), + published: z.number().optional().describe('1=published, 0=unpublished'), + language: z.string().optional().describe('Language code (default "*")'), + ...ConnectionParam, + }, + async ({ name, link, catid, numarticles, published, language, connection }) => { + const client = clientFor(connection); + const body: Record = { name, link, catid, language: language ?? '*' }; + if (numarticles !== undefined) body.numarticles = numarticles; + if (published !== undefined) body.published = published; + return formatResponse(await client.post('/newsfeeds', body)); + }, +); + +server.tool( + 'joomla_newsfeed_delete', + 'Delete a newsfeed', + { + id: z.number().describe('Newsfeed ID'), + ...ConnectionParam, + }, + async ({ id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.delete(`/newsfeeds/${id}`)); + }, +); + +// โ”€โ”€ Tags (CRUD) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +server.tool( + 'joomla_tag_get', + 'Get a single tag by ID', + { + id: z.number().describe('Tag ID'), + ...ConnectionParam, + }, + async ({ id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.get(`/tags/${id}`)); + }, +); + +server.tool( + 'joomla_tag_update', + 'Update a tag', + { + id: z.number().describe('Tag ID'), + title: z.string().optional().describe('New tag title'), + published: z.number().optional().describe('1=published, 0=unpublished'), + ...ConnectionParam, + }, + async ({ id, title, published, connection }) => { + const client = clientFor(connection); + const body: Record = {}; + if (title !== undefined) body.title = title; + if (published !== undefined) body.published = published; + return formatResponse(await client.patch(`/tags/${id}`, body)); + }, +); + +server.tool( + 'joomla_tag_delete', + 'Delete a tag', + { + id: z.number().describe('Tag ID'), + ...ConnectionParam, + }, + async ({ id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.delete(`/tags/${id}`)); + }, +); + +// โ”€โ”€ Custom Fields (CRUD) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +server.tool( + 'joomla_field_get', + 'Get a single custom field by ID', + { + id: z.number().describe('Field ID'), + ...ConnectionParam, + }, + async ({ id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.get(`/fields/com_content.article/${id}`)); + }, +); + +server.tool( + 'joomla_field_create', + 'Create a custom field', + { + title: z.string().describe('Field title'), + name: z.string().describe('Field name (system identifier)'), + type: z.string().describe('Field type (text, textarea, list, radio, checkboxes, etc.)'), + context: z.string().optional().describe('Context (default "com_content.article")'), + label: z.string().optional().describe('Display label'), + description: z.string().optional().describe('Field description'), + required: z.number().optional().describe('1=required, 0=optional'), + state: z.number().optional().describe('1=published, 0=unpublished'), + ...ConnectionParam, + }, + async ({ title, name, type, context, label, description, required: req, state, connection }) => { + const client = clientFor(connection); + const ctx = context ?? 'com_content.article'; + const body: Record = { title, name, type }; + if (label) body.label = label; + if (description) body.description = description; + if (req !== undefined) body.required = req; + if (state !== undefined) body.state = state; + return formatResponse(await client.post(`/fields/${ctx}`, body)); + }, +); + +server.tool( + 'joomla_field_delete', + 'Delete a custom field', + { + id: z.number().describe('Field ID'), + ...ConnectionParam, + }, + async ({ id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.delete(`/fields/com_content.article/${id}`)); + }, +); + +// โ”€โ”€ Menu Items (CRUD) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +server.tool( + 'joomla_menu_item_create', + 'Create a new menu item', + { + title: z.string().describe('Menu item title'), + menutype: z.string().describe('Menu type alias (e.g. "mainmenu")'), + type: z.string().describe('Menu item type (e.g. "component", "url", "alias", "separator", "heading")'), + link: z.string().optional().describe('URL or component link'), + parent_id: z.number().optional().describe('Parent menu item ID (default 1 = root)'), + published: z.number().optional().describe('1=published, 0=unpublished'), + access: z.number().optional().describe('Access level ID'), + language: z.string().optional().describe('Language code (default "*")'), + ...ConnectionParam, + }, + async ({ title, menutype, type, link, parent_id, published, access, language, connection }) => { + const client = clientFor(connection); + const body: Record = { + title, + menutype, + type, + language: language ?? '*', + }; + if (link) body.link = link; + if (parent_id !== undefined) body.parent_id = parent_id; + if (published !== undefined) body.published = published; + if (access !== undefined) body.access = access; + return formatResponse(await client.post('/menus/items', body)); + }, +); + +server.tool( + 'joomla_menu_item_update', + 'Update a menu item', + { + id: z.number().describe('Menu item ID'), + title: z.string().optional().describe('New title'), + link: z.string().optional().describe('New link URL'), + published: z.number().optional().describe('1=published, 0=unpublished'), + parent_id: z.number().optional().describe('New parent ID'), + ...ConnectionParam, + }, + async ({ id, title, link, published, parent_id, connection }) => { + const client = clientFor(connection); + const body: Record = {}; + if (title !== undefined) body.title = title; + if (link !== undefined) body.link = link; + if (published !== undefined) body.published = published; + if (parent_id !== undefined) body.parent_id = parent_id; + return formatResponse(await client.patch(`/menus/items/${id}`, body)); + }, +); + +server.tool( + 'joomla_menu_item_delete', + 'Delete a menu item', + { + id: z.number().describe('Menu item ID'), + ...ConnectionParam, + }, + async ({ id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.delete(`/menus/items/${id}`)); + }, +); + +// โ”€โ”€ Messages (Send) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +server.tool( + 'joomla_message_send', + 'Send a private message to a Joomla user', + { + user_id_to: z.number().describe('Recipient user ID'), + subject: z.string().describe('Message subject'), + message: z.string().describe('Message body'), + ...ConnectionParam, + }, + async ({ user_id_to, subject, message, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.post('/messages', { user_id_to, subject, message })); + }, +); + +server.tool( + 'joomla_message_delete', + 'Delete a private message', + { + id: z.number().describe('Message ID'), + ...ConnectionParam, + }, + async ({ id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.delete(`/messages/${id}`)); + }, +); + +// โ”€โ”€ Media (Upload/Delete) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +server.tool( + 'joomla_media_file_get', + 'Get metadata for a specific media file', + { + path: z.string().describe('File path relative to media root (e.g. "images/logo.png")'), + ...ConnectionParam, + }, + async ({ path, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.get(`/media/files/${encodeURIComponent(path)}`)); + }, +); + +server.tool( + 'joomla_media_file_delete', + 'Delete a media file', + { + path: z.string().describe('File path relative to media root'), + ...ConnectionParam, + }, + async ({ path, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.delete(`/media/files/${encodeURIComponent(path)}`)); + }, +); + +server.tool( + 'joomla_media_folder_create', + 'Create a new media folder', + { + path: z.string().describe('Full folder path to create (e.g. "images/photos/2026")'), + ...ConnectionParam, + }, + async ({ path, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.post('/media/files', { path })); + }, +); + +// โ”€โ”€ Content History โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +server.tool( + 'joomla_content_history_list', + 'List version history for a content item', + { + type_alias: z.string().describe('Content type alias (e.g. "com_content.article")'), + item_id: z.number().describe('Item ID'), + ...ConnectionParam, + }, + async ({ type_alias, item_id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.get('/content/history', { + 'filter[type_alias]': type_alias, + 'filter[item_id]': String(item_id), + })); + }, +); + +// โ”€โ”€ Checkin โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +server.tool( + 'joomla_checkin', + 'Check in (unlock) a content item that is checked out', + { + context: z.string().describe('Context (e.g. "com_content.article")'), + id: z.number().describe('Item ID to check in'), + ...ConnectionParam, + }, + async ({ context, id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.post(`/checkin/${context}/${id}`, {})); + }, +); + +// โ”€โ”€ Redirects โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +server.tool( + 'joomla_redirects_list', + 'List URL redirects', + { + search: z.string().optional().describe('Search in old URL'), + state: z.enum(['0', '1', '2', '-2']).optional().describe('0=disabled, 1=enabled, 2=archived, -2=trashed'), + ...ConnectionParam, + }, + async ({ search, state, connection }) => { + const client = clientFor(connection); + const params: Record = {}; + if (search) params['filter[search]'] = search; + if (state !== undefined) params['filter[state]'] = state; + return formatResponse(await client.get('/redirects', params)); + }, +); + +server.tool( + 'joomla_redirect_create', + 'Create a URL redirect', + { + old_url: z.string().describe('Source URL to redirect from'), + new_url: z.string().describe('Destination URL to redirect to'), + status_code: z.enum(['301', '302']).optional().describe('301=permanent, 302=temporary (default 301)'), + published: z.number().optional().describe('1=enabled, 0=disabled'), + ...ConnectionParam, + }, + async ({ old_url, new_url, status_code, published, connection }) => { + const client = clientFor(connection); + const body: Record = { + old_url, + new_url, + header: status_code ? Number(status_code) : 301, + published: published ?? 1, + }; + return formatResponse(await client.post('/redirects', body)); + }, +); + +server.tool( + 'joomla_redirect_delete', + 'Delete a URL redirect', + { + id: z.number().describe('Redirect ID'), + ...ConnectionParam, + }, + async ({ id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.delete(`/redirects/${id}`)); + }, +); + +// โ”€โ”€ Associations (Multilingual) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +server.tool( + 'joomla_associations_list', + 'List multilingual associations for a content type', + { + context: z.string().describe('Context (e.g. "com_content.article")'), + id: z.number().describe('Item ID to get associations for'), + ...ConnectionParam, + }, + async ({ context, id, connection }) => { + const client = clientFor(connection); + return formatResponse(await client.get(`/associations/${context}/${id}`)); + }, +); + // โ”€โ”€ Generic API Call โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ server.tool( -- 2.52.0