diff --git a/.github/workflows/auto-assign.yml b/.github/workflows/auto-assign.yml new file mode 100644 index 00000000..3752b662 --- /dev/null +++ b/.github/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.05.11 +# BRIEF: Auto-assign jmiller-moko to unassigned issues and PRs every 15 minutes + +name: Auto-Assign Issues & PRs + +on: + issues: + types: [opened] + pull_request_target: + types: [opened] + schedule: + - cron: '0 */12 * * *' + workflow_dispatch: + +permissions: + issues: write + pull-requests: write + +jobs: + auto-assign: + name: Assign unassigned issues and PRs + runs-on: ubuntu-latest + + steps: + - name: Assign unassigned issues + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + REPO="${{ github.repository }}" + ASSIGNEE="jmiller-moko" + + echo "## ๐Ÿท๏ธ Auto-Assign Report" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + ASSIGNED_ISSUES=0 + ASSIGNED_PRS=0 + + # Assign unassigned open issues + ISSUES=$(gh api "repos/$REPO/issues?state=open&per_page=100&assignee=none" --jq '.[].number' 2>/dev/null || true) + for NUM in $ISSUES; do + # Skip PRs (the issues endpoint returns PRs too) + IS_PR=$(gh api "repos/$REPO/issues/$NUM" --jq '.pull_request // empty' 2>/dev/null || true) + if [ -z "$IS_PR" ]; then + gh api "repos/$REPO/issues/$NUM/assignees" -X POST -f "assignees[]=$ASSIGNEE" --silent 2>/dev/null && { + ASSIGNED_ISSUES=$((ASSIGNED_ISSUES + 1)) + echo " Assigned issue #$NUM" + } || true + fi + done + + # Assign unassigned open PRs + PRS=$(gh api "repos/$REPO/pulls?state=open&per_page=100" --jq '.[] | select(.assignees | length == 0) | .number' 2>/dev/null || true) + for NUM in $PRS; do + gh api "repos/$REPO/issues/$NUM/assignees" -X POST -f "assignees[]=$ASSIGNEE" --silent 2>/dev/null && { + ASSIGNED_PRS=$((ASSIGNED_PRS + 1)) + echo " Assigned PR #$NUM" + } || true + done + + echo "| Type | Assigned |" >> $GITHUB_STEP_SUMMARY + echo "|------|----------|" >> $GITHUB_STEP_SUMMARY + echo "| Issues | $ASSIGNED_ISSUES |" >> $GITHUB_STEP_SUMMARY + echo "| Pull Requests | $ASSIGNED_PRS |" >> $GITHUB_STEP_SUMMARY + + if [ "$ASSIGNED_ISSUES" -eq 0 ] && [ "$ASSIGNED_PRS" -eq 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "โœ… All issues and PRs already have assignees" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.github/workflows/auto-dev-issue.yml b/.github/workflows/auto-dev-issue.yml new file mode 100644 index 00000000..38730aba --- /dev/null +++ b/.github/workflows/auto-dev-issue.yml @@ -0,0 +1,192 @@ +# 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.05.13 +# 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/')) + + 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" + 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 dev 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 to main|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 Main|Create PR from rc branch to main|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 + + # โ”€โ”€ RC: Create or update draft release โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + if [[ "$BRANCH" == rc/* ]]; then + MAJOR=$(echo "$VERSION" | awk -F. '{print $1}') + RELEASE_TAG="v${MAJOR}" + DRAFT_EXISTS=$(gh release view "$RELEASE_TAG" --json isDraft -q .isDraft 2>/dev/null || true) + + if [ -z "$DRAFT_EXISTS" ]; then + # No release exists โ€” create draft + gh release create "$RELEASE_TAG" \ + --title "v${MAJOR} (RC: ${VERSION})" \ + --notes "## Release Candidate ${VERSION}\n\nRC branch: \`${BRANCH}\`\nTracking issue: ${PARENT_URL}" \ + --draft \ + --target main 2>/dev/null || true + echo "Draft release created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY + elif [ "$DRAFT_EXISTS" = "true" ]; then + # Draft exists โ€” update title + gh release edit "$RELEASE_TAG" \ + --title "v${MAJOR} (RC: ${VERSION})" --draft 2>/dev/null || true + echo "Draft release updated: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY + else + # Release exists and is published โ€” set back to draft for RC + gh release edit "$RELEASE_TAG" \ + --title "v${MAJOR} (RC: ${VERSION})" --draft 2>/dev/null || true + echo "Release ${RELEASE_TAG} set to draft for RC" >> $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 new file mode 100644 index 00000000..3e1caf3f --- /dev/null +++ b/.github/workflows/auto-release.yml @@ -0,0 +1,549 @@ +# 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/joomla/auto-release.yml.template +# VERSION: 04.05.13 +# BRIEF: Joomla build & release โ€” ZIP package, updates.xml, SHA-256 checksum +# +# +========================================================================+ +# | BUILD & RELEASE PIPELINE (JOOMLA) | +# +========================================================================+ +# | | +# | Triggers on push to main (skips bot commits + [skip ci]): | +# | | +# | Every push: | +# | 1. Read version from README.md | +# | 3. Set platform version (Joomla ) | +# | 4. Update [VERSION: XX.YY.ZZ] badges in markdown files | +# | 5. Write updates.xml (Joomla update server XML) | +# | 6. Create git tag vXX.YY.ZZ | +# | 7a. Patch: update existing GitHub Release for this minor | +# | 8. Build ZIP, upload asset, write SHA-256 to updates.xml | +# | | +# | 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 (Joomla)" >> $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 + + # -- Joomla: manifest version drift -------- + MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1) + if [ -n "$MANIFEST" ]; then + XML_VER=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1) + if [ -n "$XML_VER" ] && [ "$XML_VER" != "$VERSION" ]; then + echo "- Manifest drift: \`${XML_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + else + echo "- Manifest version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + fi + + # -- Joomla: XML manifest existence -------- + if [ -z "$MANIFEST" ]; then + echo "- No Joomla XML manifest found" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + else + echo "- Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY + + # -- Joomla: extension type check -------- + TYPE=$(grep -oP ']+type="\K[^"]+' "$MANIFEST" 2>/dev/null) + echo "- Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "$ERRORS" -gt 0 ]; then + echo "**${ERRORS} error(s) โ€” release may be incomplete**" >> $GITHUB_STEP_SUMMARY + else + echo "**All sanity checks passed**" >> $GITHUB_STEP_SUMMARY + fi + + # -- STEP 2: Create or update version/XX.YY archive branch --------------- + # Always runs โ€” every version change on main archives to version/XX.YY + - name: "Step 2: Version archive branch" + if: steps.check.outputs.already_released != 'true' + run: | + BRANCH="${{ steps.version.outputs.branch }}" + IS_MINOR="${{ steps.version.outputs.is_minor }}" + PATCH="${{ steps.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 + + # -- STEP 5: Write updates.xml (Joomla update server) --------------------- + - name: "Step 5: Write updates.xml" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.version.outputs.version }}" + REPO="${{ github.repository }}" + + # -- Parse extension metadata from XML manifest ---------------- + MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1) + if [ -z "$MANIFEST" ]; then + echo "Warning: No Joomla XML manifest found โ€” skipping updates.xml" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + 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 || echo "") + EXT_CLIENT=$(grep -oP ']+client="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "") + EXT_FOLDER=$(grep -oP ']+group="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "") + TARGET_PLATFORM=$(grep -oP '' "$MANIFEST" 2>/dev/null | head -1 || echo "") + PHP_MINIMUM=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "") + + # Derive element from manifest filename if not in XML + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(basename "$MANIFEST" .xml) + fi + + # Build client tag: plugins and frontend modules need site + CLIENT_TAG="" + if [ -n "$EXT_CLIENT" ]; then + CLIENT_TAG="${EXT_CLIENT}" + elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then + CLIENT_TAG="site" + fi + + # Build folder tag for plugins (required for Joomla to match the update) + FOLDER_TAG="" + if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then + FOLDER_TAG="${EXT_FOLDER}" + fi + + # Build targetplatform (fallback to Joomla 5 if not in manifest) + if [ -z "$TARGET_PLATFORM" ]; then + TARGET_PLATFORM=$(printf '' "/") + fi + + # Build php_minimum tag + PHP_TAG="" + if [ -n "$PHP_MINIMUM" ]; then + PHP_TAG="${PHP_MINIMUM}" + fi + + DOWNLOAD_URL="https://github.com/${REPO}/releases/download/v${VERSION}/${EXT_ELEMENT}-${VERSION}.zip" + INFO_URL="https://github.com/${REPO}/releases/tag/v${VERSION}" + + # -- Build stable entry โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + STABLE_ENTRY=$(cat < + ${EXT_NAME} + ${EXT_NAME} update + ${EXT_ELEMENT} + ${EXT_TYPE} + ${VERSION} + $([ -n "$CLIENT_TAG" ] && echo " ${CLIENT_TAG}") + $([ -n "$FOLDER_TAG" ] && echo " ${FOLDER_TAG}") + + stable + + ${INFO_URL} + + ${DOWNLOAD_URL} + + ${TARGET_PLATFORM} + $([ -n "$PHP_TAG" ] && echo " ${PHP_TAG}") + Moko Consulting + https://mokoconsulting.tech + +XMLEOF +) + + # -- Write updates.xml preserving dev/rc entries โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + # Extract existing dev and rc entries if present + RC_ENTRY="" + DEV_ENTRY="" + if [ -f "updates.xml" ]; then + RC_ENTRY=$(python3 -c " +import re +with open('updates.xml') as f: c = f.read() +m = re.search(r'( .*?rc.*?)', c, re.DOTALL) +if m: print(m.group(1)) +" 2>/dev/null || true) + DEV_ENTRY=$(python3 -c " +import re +with open('updates.xml') as f: c = f.read() +m = re.search(r'( .*?development.*?)', c, re.DOTALL) +if m: print(m.group(1)) +" 2>/dev/null || true) + fi + + { + printf '%s\n' '' + printf '%s\n' '' + echo "$STABLE_ENTRY" + [ -n "$RC_ENTRY" ] && echo "$RC_ENTRY" + [ -n "$DEV_ENTRY" ] && echo "$DEV_ENTRY" + printf '%s\n' '' + } > updates.xml + + echo "updates.xml: ${VERSION} (stable + rc/dev preserved)" >> $GITHUB_STEP_SUMMARY + + # -- Commit all changes --------------------------------------------------- + - name: Commit release changes + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + if git diff --quiet && git diff --cached --quiet; then + echo "No changes to commit" + exit 0 + fi + VERSION="${{ steps.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 + 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 + # Append version notes to existing major release + 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 + + # -- STEP 8: Build Joomla install ZIP + SHA-256 checksum ------------------ + # Every patch builds an install-ready ZIP and uploads it to the minor release. + # Result: one Release per minor version with a ZIP for each patch. + - name: "Step 8: Build Joomla package and update checksum" + if: >- + steps.version.outputs.skip != 'true' + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + VERSION="${{ steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + REPO="${{ github.repository }}" + + # All ZIPs upload to the major release tag (vXX) + gh release view "$RELEASE_TAG" --json tagName > /dev/null 2>&1 || { + echo "No release ${RELEASE_TAG} found โ€” skipping ZIP upload" + exit 0 + } + + # Find extension element name from manifest + MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1 || true) + [ -z "$MANIFEST" ] && exit 0 + + EXT_ELEMENT=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || basename "$MANIFEST" .xml) + PACKAGE_NAME="${EXT_ELEMENT}-${VERSION}.zip" + + # -- Build install-ready ZIP from src/ ---------------------------- + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + [ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ โ€” skipping package"; exit 0; } + + cd "$SOURCE_DIR" + zip -r "/tmp/${PACKAGE_NAME}" . + cd .. + + FILESIZE=$(stat -c%s "/tmp/${PACKAGE_NAME}" 2>/dev/null || stat -f%z "/tmp/${PACKAGE_NAME}" 2>/dev/null || echo "unknown") + + # -- Calculate SHA-256 ------------------------------------------- + SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1) + + # -- Upload ZIP to the minor release tag ------------------------- + gh release upload "$RELEASE_TAG" "/tmp/${PACKAGE_NAME}" --clobber 2>/dev/null || { + echo "Could not upload with --clobber, retrying..." + gh release upload "$RELEASE_TAG" "/tmp/${PACKAGE_NAME}" 2>/dev/null || true + } + + # -- Update updates.xml with SHA-256 for latest patch ------------- + if [ -f "updates.xml" ]; then + if grep -q '' updates.xml; then + sed -i "s|.*|sha256:${SHA256}|" updates.xml + else + sed -i "s||\n sha256:${SHA256}|" updates.xml + fi + + # Also update the download URL to point to this patch's ZIP + DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}" + sed -i "s|]*>[^<]*|${DOWNLOAD_URL}|" updates.xml + + git add updates.xml + git commit -m "chore(release): SHA-256 + download URL for ${VERSION} [skip ci]" \ + --author="github-actions[bot] " || true + git push || true + fi + + echo "### Joomla Package" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Package | \`${PACKAGE_NAME}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Size | ${FILESIZE} bytes |" >> $GITHUB_STEP_SUMMARY + echo "| SHA-256 | \`${SHA256}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Release | \`${RELEASE_TAG}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Download | [${PACKAGE_NAME}](https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}) |" >> $GITHUB_STEP_SUMMARY + + # -- 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 (Joomla)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY + echo "|------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Release | [View](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 new file mode 100644 index 00000000..9ed880aa --- /dev/null +++ b/.github/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.05.13 +# BRIEF: Validates CHANGELOG.md format and version consistency +# NOTE: Deployed to .github/workflows/changelog-validation.yml in governed repos. + +name: Changelog Validation + +on: + push: + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + validate-changelog: + name: Validate CHANGELOG.md + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check CHANGELOG.md exists + run: | + echo "### Changelog Validation" >> $GITHUB_STEP_SUMMARY + if [ ! -f "CHANGELOG.md" ]; then + echo "CHANGELOG.md not found in repository root." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "CHANGELOG.md exists." >> $GITHUB_STEP_SUMMARY + + - name: Check VERSION header matches README.md + run: | + # Extract version from README.md FILE INFORMATION block + README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md | head -1) + if [ -z "$README_VERSION" ]; then + echo "No VERSION found in README.md FILE INFORMATION block." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + # Check that CHANGELOG.md has a matching version header + CHANGELOG_VERSION=$(grep -oP '^\#\#\s*\[\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' CHANGELOG.md | head -1) + if [ -z "$CHANGELOG_VERSION" ]; then + echo "No version header found in CHANGELOG.md (expected \`## [XX.YY.ZZ] - YYYY-MM-DD\`)." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + if [ "$CHANGELOG_VERSION" != "$README_VERSION" ]; then + echo "CHANGELOG latest version \`${CHANGELOG_VERSION}\` does not match README VERSION \`${README_VERSION}\`." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + echo "CHANGELOG version \`${CHANGELOG_VERSION}\` matches README VERSION." >> $GITHUB_STEP_SUMMARY + + - name: Validate conventional changelog format + run: | + ERRORS=0 + + # Check that version entries follow ## [XX.YY.ZZ] - YYYY-MM-DD format + while IFS= read -r LINE; do + if ! echo "$LINE" | grep -qP '^\#\#\s*\[[0-9]{2}\.[0-9]{2}\.[0-9]{2}\]\s*-\s*[0-9]{4}-[0-9]{2}-[0-9]{2}'; then + echo "Malformed version header: \`${LINE}\`" >> $GITHUB_STEP_SUMMARY + echo " Expected format: \`## [XX.YY.ZZ] - YYYY-MM-DD\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done < <(grep -P '^\#\#\s*\[' CHANGELOG.md) + + ENTRY_COUNT=$(grep -cP '^\#\#\s*\[' CHANGELOG.md || echo "0") + if [ "$ENTRY_COUNT" -eq 0 ]; then + echo "No version entries found in CHANGELOG.md." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Found ${ENTRY_COUNT} version entr(ies) in CHANGELOG.md." >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} format issue(s) found.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Changelog format validation passed.**" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.github/workflows/ci-joomla.yml b/.github/workflows/ci-joomla.yml new file mode 100644 index 00000000..6281236b --- /dev/null +++ b/.github/workflows/ci-joomla.yml @@ -0,0 +1,391 @@ +# Copyright (C) 2026 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: GitHub.Workflow.Template +# INGROUP: MokoStandards.CI +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /templates/workflows/joomla/ci-joomla.yml.template +# VERSION: 04.05.13 +# BRIEF: CI workflow for Joomla extensions โ€” lint, validate, test +# NOTE: Deployed to .github/workflows/ci-joomla.yml in governed Joomla extension repos. + +name: Joomla Extension CI + +on: + push: + branches: + - main + - dev/** + - rc/** + - version/** + pull_request: + branches: + - main + - dev/** + - rc/** + workflow_dispatch: + +permissions: + contents: read + pull-requests: write + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + lint-and-validate: + name: Lint & Validate + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: '8.2' + extensions: mbstring, xml, zip, gd, curl, json, simplexml + tools: composer:v2 + coverage: none + + - name: Clone MokoStandards + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + git clone --depth 1 --branch version/04 --quiet \ + "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ + /tmp/mokostandards + + - name: Install dependencies + env: + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' + run: | + if [ -f "composer.json" ]; then + composer install \ + --no-interaction \ + --prefer-dist \ + --optimize-autoloader + else + echo "No composer.json found โ€” skipping dependency install" + fi + + - name: PHP syntax check + run: | + ERRORS=0 + for DIR in src/ htdocs/; do + if [ -d "$DIR" ]; then + FOUND=1 + while IFS= read -r -d '' FILE; do + OUTPUT=$(php -l "$FILE" 2>&1) + if echo "$OUTPUT" | grep -q "Parse error"; then + echo "::error file=${FILE}::${OUTPUT}" + ERRORS=$((ERRORS + 1)) + fi + done < <(find "$DIR" -name "*.php" -print0) + fi + done + echo "### PHP Syntax Check" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} syntax error(s) found.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY + fi + + - name: XML manifest validation + run: | + echo "### XML Manifest Validation" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + # Find the extension manifest (XML with /dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No Joomla extension manifest found (XML file with \`> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Manifest found: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY + + # Validate well-formed XML + php -r " + \$xml = @simplexml_load_file('$MANIFEST'); + if (\$xml === false) { + echo 'INVALID'; + exit(1); + } + echo 'VALID'; + " > /tmp/xml_result 2>&1 + XML_RESULT=$(cat /tmp/xml_result) + if [ "$XML_RESULT" != "VALID" ]; then + echo "Manifest is not well-formed XML." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Manifest is well-formed XML." >> $GITHUB_STEP_SUMMARY + fi + + # Check required tags: name, version, author, namespace (Joomla 5+) + for TAG in name version author namespace; do + if ! grep -q "<${TAG}>" "$MANIFEST" 2>/dev/null; then + echo "Missing required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Found required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY + fi + done + fi + + if [ "${ERRORS}" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**${ERRORS} manifest issue(s) found.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Manifest validation passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Check language files referenced in manifest + run: | + echo "### Language File Check" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -n "$MANIFEST" ]; then + # Extract language file references from manifest + LANG_FILES=$(grep -oP 'language\s+tag="[^"]*"[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true) + if [ -z "$LANG_FILES" ]; then + echo "No language file references found in manifest โ€” skipping." >> $GITHUB_STEP_SUMMARY + else + while IFS= read -r LANG_FILE; do + LANG_FILE=$(echo "$LANG_FILE" | xargs) + if [ -z "$LANG_FILE" ]; then + continue + fi + # Check in common locations + FOUND=0 + for BASE in "." "src" "htdocs"; do + if [ -f "${BASE}/${LANG_FILE}" ]; then + FOUND=1 + break + fi + done + if [ "$FOUND" -eq 0 ]; then + echo "Missing language file: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Language file present: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY + fi + done <<< "$LANG_FILES" + fi + else + echo "No manifest found โ€” skipping language check." >> $GITHUB_STEP_SUMMARY + fi + + if [ "${ERRORS}" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**${ERRORS} missing language file(s).**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Language file check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Check index.html files in directories + run: | + echo "### Index.html Check" >> $GITHUB_STEP_SUMMARY + MISSING=0 + CHECKED=0 + + for DIR in src/ htdocs/; do + if [ -d "$DIR" ]; then + while IFS= read -r -d '' SUBDIR; do + CHECKED=$((CHECKED + 1)) + if [ ! -f "${SUBDIR}/index.html" ]; then + echo "Missing index.html in: \`${SUBDIR}\`" >> $GITHUB_STEP_SUMMARY + MISSING=$((MISSING + 1)) + fi + done < <(find "$DIR" -type d -print0) + fi + done + + if [ "${CHECKED}" -eq 0 ]; then + echo "No src/ or htdocs/ directories found โ€” skipping." >> $GITHUB_STEP_SUMMARY + elif [ "${MISSING}" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**${MISSING} director(ies) missing index.html out of ${CHECKED} checked.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "All ${CHECKED} directories contain index.html." >> $GITHUB_STEP_SUMMARY + fi + + release-readiness: + name: Release Readiness Check + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' && github.base_ref == 'main' + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Validate release readiness + run: | + echo "## Release Readiness" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + # Extract version from README.md + README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md | head -1) + if [ -z "$README_VERSION" ]; then + echo "No VERSION found in README.md FILE INFORMATION block." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "README version: \`${README_VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + + # Find the extension manifest + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No Joomla extension manifest found." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY + + # Check matches README VERSION + MANIFEST_VERSION=$(grep -oP '\K[^<]+' "$MANIFEST" | head -1) + if [ -z "$MANIFEST_VERSION" ]; then + echo "No \`\` tag in manifest." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + elif [ -n "$README_VERSION" ] && [ "$MANIFEST_VERSION" != "$README_VERSION" ]; then + echo "Manifest version \`${MANIFEST_VERSION}\` does not match README \`${README_VERSION}\`." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Manifest version: \`${MANIFEST_VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + + # Check extension type, element, client attributes + EXT_TYPE=$(grep -oP ']*\btype="\K[^"]+' "$MANIFEST" | head -1) + if [ -z "$EXT_TYPE" ]; then + echo "Missing \`type\` attribute on \`\` tag." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Extension type: \`${EXT_TYPE}\`" >> $GITHUB_STEP_SUMMARY + fi + + # Element check (component/module/plugin name) + HAS_ELEMENT=$(grep -cP '<(element|name)>' "$MANIFEST" 2>/dev/null || echo "0") + if [ "$HAS_ELEMENT" -eq 0 ]; then + echo "Missing \`\` or \`\` in manifest." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + + # Client attribute for site/admin modules and plugins + if echo "$EXT_TYPE" | grep -qP "^(module|plugin)$"; then + HAS_CLIENT=$(grep -cP ']*\bclient=' "$MANIFEST" 2>/dev/null || echo "0") + if [ "$HAS_CLIENT" -eq 0 ]; then + echo "Missing \`client\` attribute for ${EXT_TYPE} extension." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + fi + fi + + # Check updates.xml exists + if [ -f "updates.xml" ] || [ -f "updates.xml" ]; then + echo "Update XML present." >> $GITHUB_STEP_SUMMARY + else + echo "No updates.xml found." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + + # Check CHANGELOG.md exists + if [ -f "CHANGELOG.md" ]; then + echo "CHANGELOG.md present." >> $GITHUB_STEP_SUMMARY + else + echo "No CHANGELOG.md found." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ $ERRORS -gt 0 ]; then + echo "**${ERRORS} issue(s) must be resolved before release.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Extension is ready for release.**" >> $GITHUB_STEP_SUMMARY + fi + + test: + name: Tests (PHP ${{ matrix.php }}) + runs-on: ubuntu-latest + needs: lint-and-validate + + strategy: + fail-fast: false + matrix: + php: ['8.2', '8.3'] + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP ${{ matrix.php }} + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: ${{ matrix.php }} + extensions: mbstring, xml, zip, gd, curl, json, simplexml + tools: composer:v2 + coverage: none + + - name: Install dependencies + env: + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' + run: | + if [ -f "composer.json" ]; then + composer install \ + --no-interaction \ + --prefer-dist \ + --optimize-autoloader + else + echo "No composer.json found โ€” skipping dependency install" + fi + + - name: Run tests + run: | + echo "### Test Results (PHP ${{ matrix.php }})" >> $GITHUB_STEP_SUMMARY + if [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then + vendor/bin/phpunit --testdox 2>&1 | tee /tmp/test-output.log + EXIT=${PIPESTATUS[0]} + if [ $EXIT -eq 0 ]; then + echo "All tests passed." >> $GITHUB_STEP_SUMMARY + else + echo "Test failures detected โ€” see log." >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + cat /tmp/test-output.log >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + fi + exit $EXIT + else + echo "No phpunit.xml found โ€” skipping tests." >> $GITHUB_STEP_SUMMARY + fi diff --git a/.github/workflows/enterprise-firewall-setup.yml b/.github/workflows/enterprise-firewall-setup.yml new file mode 100644 index 00000000..b6cf7abe --- /dev/null +++ b/.github/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.05.13 +# 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@v6 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.11' + + - name: Apply Firewall Rules to Runner (Auto-run only) + if: github.event_name != 'workflow_dispatch' + env: + DEV_FTP_HOST: ${{ vars.DEV_FTP_HOST }} + DEV_FTP_PORT: ${{ vars.DEV_FTP_PORT }} + run: | + echo "๐Ÿ”ฅ Applying firewall rules for coding agent environment..." + echo "" + echo "This step ensures the GitHub Actions runner can access trusted domains" + echo "including license providers, package registries, and documentation sources." + echo "" + + # Note: GitHub Actions runners are ephemeral and run in controlled environments + # This step documents what domains are being accessed during the workflow + # Actual firewall configuration is managed by GitHub + + cat > /tmp/trusted-domains.txt << 'EOF' + # Trusted domains for coding agent environment + # License Providers + www.gnu.org + opensource.org + choosealicense.com + spdx.org + creativecommons.org + apache.org + fsf.org + + # Documentation & Standards + semver.org + keepachangelog.com + conventionalcommits.org + + # GitHub & Related + github.com + api.github.com + docs.github.com + raw.githubusercontent.com + ghcr.io + + # Package Registries + npmjs.com + registry.npmjs.org + pypi.org + files.pythonhosted.org + packagist.org + repo.packagist.org + rubygems.org + + # Platform-Specific + joomla.org + downloads.joomla.org + docs.joomla.org + php.net + getcomposer.org + dolibarr.org + wiki.dolibarr.org + docs.dolibarr.org + + # Moko Consulting + mokoconsulting.tech + + # SFTP Deployment Server (DEV_FTP_HOST) + ${DEV_FTP_HOST:-} + + # Google Services + drive.google.com + docs.google.com + sheets.google.com + accounts.google.com + storage.googleapis.com + fonts.googleapis.com + fonts.gstatic.com + + # GitHub Extended + upload.github.com + objects.githubusercontent.com + user-images.githubusercontent.com + codeload.github.com + pkg.github.com + + # Developer Reference + developer.mozilla.org + stackoverflow.com + git-scm.com + + # CDN & Infrastructure + cdn.jsdelivr.net + unpkg.com + cdnjs.cloudflare.com + img.shields.io + + # Container Registries + hub.docker.com + registry-1.docker.io + + # CI & Code Quality + codecov.io + sonarcloud.io + + # Terraform & Infrastructure + registry.terraform.io + releases.hashicorp.com + checkpoint-api.hashicorp.com + EOF + + echo "โœ“ Trusted domains documented for this runner" + echo "โœ“ GitHub Actions runners have network access to these domains" + echo "" + + # Test connectivity to key domains + echo "Testing connectivity to key domains..." + for domain in "github.com" "www.gnu.org" "npmjs.com" "pypi.org"; do + if curl -s --max-time 3 -o /dev/null -w "%{http_code}" "https://$domain" | grep -q "200\|301\|302"; then + echo " โœ“ $domain is accessible" + else + echo " โš ๏ธ $domain connectivity check failed (may be expected)" + fi + done + + # Test SFTP server connectivity (TCP port check) + SFTP_HOST="${DEV_FTP_HOST:-}" + SFTP_PORT="${DEV_FTP_PORT:-22}" + if [ -n "$SFTP_HOST" ]; then + # Strip any embedded :port suffix + SFTP_HOST="${SFTP_HOST%%:*}" + echo "" + echo "Testing SFTP deployment server connectivity..." + if timeout 5 bash -c "echo >/dev/tcp/${SFTP_HOST}/${SFTP_PORT}" 2>/dev/null; then + echo " โœ“ SFTP server ${SFTP_HOST}:${SFTP_PORT} is reachable" + else + echo " โš ๏ธ SFTP server ${SFTP_HOST}:${SFTP_PORT} is not reachable from runner (firewall rule needed)" + fi + else + echo "" + echo " โ„น๏ธ DEV_FTP_HOST not configured โ€” skipping SFTP connectivity check" + fi + + - name: Generate Firewall Configuration + id: generate + env: + DEV_FTP_HOST: ${{ vars.DEV_FTP_HOST }} + DEV_FTP_PORT: ${{ vars.DEV_FTP_PORT }} + run: | + cat > generate_firewall_config.py << 'PYTHON_EOF' + #!/usr/bin/env python3 + """ + Enterprise Firewall Configuration Generator + + Generates firewall rules for enterprise-ready deployments allowing + access to trusted domains including license providers, documentation + sources, package registries, and platform-specific sites. + """ + + import json + import os + import yaml + import sys + from typing import List, Dict + + # SFTP deployment server from org variables + _sftp_host_raw = os.environ.get("DEV_FTP_HOST", "").strip() + _sftp_port = os.environ.get("DEV_FTP_PORT", "").strip() or "22" + # Strip embedded :port suffix if present + _sftp_host = _sftp_host_raw.split(":")[0] if _sftp_host_raw else "" + if ":" in _sftp_host_raw and not _sftp_port: + _sftp_port = _sftp_host_raw.split(":")[1] + + SFTP_HOST = _sftp_host + SFTP_PORT = int(_sftp_port) if _sftp_port.isdigit() else 22 + + # Trusted domains from .github/copilot.yml + TRUSTED_DOMAINS = { + "license_providers": [ + "www.gnu.org", + "opensource.org", + "choosealicense.com", + "spdx.org", + "creativecommons.org", + "apache.org", + "fsf.org", + ], + "documentation_standards": [ + "semver.org", + "keepachangelog.com", + "conventionalcommits.org", + ], + "github_related": [ + "github.com", + "api.github.com", + "docs.github.com", + "raw.githubusercontent.com", + "ghcr.io", + ], + "package_registries": [ + "npmjs.com", + "registry.npmjs.org", + "pypi.org", + "files.pythonhosted.org", + "packagist.org", + "repo.packagist.org", + "rubygems.org", + ], + "standards_organizations": [ + "json-schema.org", + "w3.org", + "ietf.org", + ], + "platform_specific": [ + "joomla.org", + "downloads.joomla.org", + "docs.joomla.org", + "php.net", + "getcomposer.org", + "dolibarr.org", + "wiki.dolibarr.org", + "docs.dolibarr.org", + ], + "moko_consulting": [ + "mokoconsulting.tech", + ], + "google_services": [ + "drive.google.com", + "docs.google.com", + "sheets.google.com", + "accounts.google.com", + "storage.googleapis.com", + "fonts.googleapis.com", + "fonts.gstatic.com", + ], + "github_extended": [ + "upload.github.com", + "objects.githubusercontent.com", + "user-images.githubusercontent.com", + "codeload.github.com", + "pkg.github.com", + ], + "developer_reference": [ + "developer.mozilla.org", + "stackoverflow.com", + "git-scm.com", + ], + "cdn_and_infrastructure": [ + "cdn.jsdelivr.net", + "unpkg.com", + "cdnjs.cloudflare.com", + "img.shields.io", + ], + "container_registries": [ + "hub.docker.com", + "registry-1.docker.io", + ], + "ci_code_quality": [ + "codecov.io", + "sonarcloud.io", + ], + "terraform_infrastructure": [ + "registry.terraform.io", + "releases.hashicorp.com", + "checkpoint-api.hashicorp.com", + ], + } + + # Inject SFTP deployment server as a separate category (port 22, not 443) + if SFTP_HOST: + TRUSTED_DOMAINS["sftp_deployment_server"] = [SFTP_HOST] + print(f"โ„น๏ธ SFTP deployment server: {SFTP_HOST}:{SFTP_PORT}") + + def generate_sftp_iptables_rules(host: str, port: int) -> str: + """Generate iptables rules specifically for SFTP egress""" + return ( + f"# Allow SFTP to deployment server {host}:{port}\n" + f"iptables -A OUTPUT -p tcp -d $(dig +short {host} | head -1)" + f" --dport {port} -j ACCEPT # SFTP deploy\n" + ) + + def generate_sftp_ufw_rules(host: str, port: int) -> str: + """Generate UFW rules for SFTP egress""" + return ( + f"# Allow SFTP to deployment server\n" + f"ufw allow out to $(dig +short {host} | head -1)" + f" port {port} proto tcp comment 'SFTP deploy to {host}'\n" + ) + + def generate_sftp_firewalld_rules(host: str, port: int) -> str: + """Generate firewalld rules for SFTP egress""" + return ( + f"# Allow SFTP to deployment server\n" + f"firewall-cmd --permanent --add-rich-rule='" + f"rule family=ipv4 destination address=$(dig +short {host} | head -1)" + f" port port={port} protocol=tcp accept' # SFTP deploy\n" + ) + + def generate_iptables_rules(domains: List[str]) -> str: + """Generate iptables firewall rules""" + rules = ["#!/bin/bash", "", "# Enterprise Firewall Rules - iptables", ""] + rules.append("# Allow outbound HTTPS to trusted domains") + rules.append("") + + for domain in domains: + rules.append(f"# Allow {domain}") + rules.append(f"iptables -A OUTPUT -p tcp -d $(dig +short {domain} | head -1) --dport 443 -j ACCEPT") + + rules.append("") + rules.append("# Allow DNS lookups") + rules.append("iptables -A OUTPUT -p udp --dport 53 -j ACCEPT") + rules.append("iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT") + + return "\n".join(rules) + + def generate_ufw_rules(domains: List[str]) -> str: + """Generate UFW firewall rules""" + rules = ["#!/bin/bash", "", "# Enterprise Firewall Rules - UFW", ""] + rules.append("# Allow outbound HTTPS to trusted domains") + rules.append("") + + for domain in domains: + rules.append(f"# Allow {domain}") + rules.append(f"ufw allow out to $(dig +short {domain} | head -1) port 443 proto tcp comment 'Allow {domain}'") + + rules.append("") + rules.append("# Allow DNS") + rules.append("ufw allow out 53/udp comment 'Allow DNS UDP'") + rules.append("ufw allow out 53/tcp comment 'Allow DNS TCP'") + + return "\n".join(rules) + + def generate_firewalld_rules(domains: List[str]) -> str: + """Generate firewalld rules""" + rules = ["#!/bin/bash", "", "# Enterprise Firewall Rules - firewalld", ""] + rules.append("# Add trusted domains to firewall") + rules.append("") + + for domain in domains: + rules.append(f"# Allow {domain}") + rules.append(f"firewall-cmd --permanent --add-rich-rule='rule family=ipv4 destination address=$(dig +short {domain} | head -1) port port=443 protocol=tcp accept'") + + rules.append("") + rules.append("# Reload firewall") + rules.append("firewall-cmd --reload") + + return "\n".join(rules) + + def generate_aws_security_group(domains: List[str]) -> Dict: + """Generate AWS Security Group rules (JSON format)""" + rules = { + "SecurityGroupRules": { + "Egress": [] + } + } + + for domain in domains: + rules["SecurityGroupRules"]["Egress"].append({ + "Description": f"Allow HTTPS to {domain}", + "IpProtocol": "tcp", + "FromPort": 443, + "ToPort": 443, + "CidrIp": "0.0.0.0/0", # In practice, resolve to specific IPs + "Tags": [{ + "Key": "Domain", + "Value": domain + }] + }) + + # Add DNS + rules["SecurityGroupRules"]["Egress"].append({ + "Description": "Allow DNS", + "IpProtocol": "udp", + "FromPort": 53, + "ToPort": 53, + "CidrIp": "0.0.0.0/0" + }) + + return rules + + def generate_markdown_documentation(domains_by_category: Dict[str, List[str]]) -> str: + """Generate markdown documentation""" + md = ["# Enterprise Firewall Configuration Guide", ""] + md.append("## Overview") + md.append("") + md.append("This document provides firewall configuration guidance for enterprise-ready deployments.") + md.append("It lists trusted domains that should be whitelisted for outbound access to ensure") + md.append("proper functionality of license validation, package management, and documentation access.") + md.append("") + + md.append("## Trusted Domains by Category") + md.append("") + + all_domains = [] + for category, domains in domains_by_category.items(): + category_name = category.replace("_", " ").title() + md.append(f"### {category_name}") + md.append("") + md.append("| Domain | Purpose |") + md.append("|--------|---------|") + + for domain in domains: + all_domains.append(domain) + purpose = get_domain_purpose(domain) + md.append(f"| `{domain}` | {purpose} |") + + md.append("") + + md.append("## Implementation Examples") + md.append("") + + md.append("### iptables Example") + md.append("") + md.append("```bash") + md.append("# Allow HTTPS to trusted domain") + md.append(f"iptables -A OUTPUT -p tcp -d $(dig +short {all_domains[0]}) --dport 443 -j ACCEPT") + md.append("```") + md.append("") + + md.append("### UFW Example") + md.append("") + md.append("```bash") + md.append("# Allow HTTPS to trusted domain") + md.append(f"ufw allow out to {all_domains[0]} port 443 proto tcp") + md.append("```") + md.append("") + + md.append("### AWS Security Group Example") + md.append("") + md.append("```json") + md.append("{") + md.append(' "IpPermissions": [{') + md.append(' "IpProtocol": "tcp",') + md.append(' "FromPort": 443,') + md.append(' "ToPort": 443,') + md.append(' "IpRanges": [{"CidrIp": "0.0.0.0/0", "Description": "HTTPS to trusted domains"}]') + md.append(" }]") + md.append("}") + md.append("```") + md.append("") + + md.append("## Ports Required") + md.append("") + md.append("| Port | Protocol | Purpose |") + md.append("|------|----------|---------|") + md.append("| 443 | TCP | HTTPS (secure web access) |") + md.append("| 80 | TCP | HTTP (redirects to HTTPS) |") + md.append("| 53 | UDP/TCP | DNS resolution |") + md.append("") + + md.append("## Security Considerations") + md.append("") + md.append("1. **DNS Resolution**: Ensure DNS queries are allowed (port 53 UDP/TCP)") + md.append("2. **Certificate Validation**: HTTPS requires ability to reach certificate authorities") + md.append("3. **Dynamic IPs**: Some domains use CDNs with dynamic IPs - consider using FQDNs in rules") + md.append("4. **Regular Updates**: Review and update whitelist as services change") + md.append("5. **Logging**: Enable logging for blocked connections to identify missing rules") + md.append("") + + md.append("## Compliance Notes") + md.append("") + md.append("- All listed domains provide read-only access to public information") + md.append("- License providers enable GPL compliance verification") + md.append("- Package registries support dependency security scanning") + md.append("- No authentication credentials are transmitted to these domains") + md.append("") + + return "\n".join(md) + + def get_domain_purpose(domain: str) -> str: + """Get human-readable purpose for a domain""" + purposes = { + "www.gnu.org": "GNU licenses and documentation", + "opensource.org": "Open Source Initiative resources", + "choosealicense.com": "GitHub license selection tool", + "spdx.org": "Software Package Data Exchange identifiers", + "creativecommons.org": "Creative Commons licenses", + "apache.org": "Apache Software Foundation licenses", + "fsf.org": "Free Software Foundation resources", + "semver.org": "Semantic versioning specification", + "keepachangelog.com": "Changelog format standards", + "conventionalcommits.org": "Commit message conventions", + "github.com": "GitHub platform access", + "api.github.com": "GitHub API access", + "docs.github.com": "GitHub documentation", + "raw.githubusercontent.com": "GitHub raw content access", + "npmjs.com": "npm package registry", + "pypi.org": "Python Package Index", + "packagist.org": "PHP Composer package registry", + "rubygems.org": "Ruby gems registry", + "joomla.org": "Joomla CMS platform", + "php.net": "PHP documentation and downloads", + "dolibarr.org": "Dolibarr ERP/CRM platform", + } + return purposes.get(domain, "Trusted resource") + + def main(): + # Use inputs if provided (manual dispatch), otherwise use defaults (auto-run) + firewall_type = "${{ github.event.inputs.firewall_type }}" or "all" + output_format = "${{ github.event.inputs.output_format }}" or "markdown" + + print(f"Running in {'manual' if '${{ github.event.inputs.firewall_type }}' else 'automatic'} mode") + print(f"Firewall type: {firewall_type}") + print(f"Output format: {output_format}") + print("") + + # Collect all domains + all_domains = [] + for domains in TRUSTED_DOMAINS.values(): + all_domains.extend(domains) + + # Remove duplicates and sort + all_domains = sorted(set(all_domains)) + + print(f"Generating firewall rules for {len(all_domains)} trusted domains...") + print("") + + # Exclude SFTP server from HTTPS rule generation (different port) + https_domains = [d for d in all_domains if d != SFTP_HOST] + + # Generate based on firewall type + if firewall_type in ["iptables", "all"]: + rules = generate_iptables_rules(https_domains) + if SFTP_HOST: + rules += "\n# โ”€โ”€ SFTP Deployment Server โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n" + rules += generate_sftp_iptables_rules(SFTP_HOST, SFTP_PORT) + with open("firewall-rules-iptables.sh", "w") as f: + f.write(rules) + print("โœ“ Generated iptables rules: firewall-rules-iptables.sh") + + if firewall_type in ["ufw", "all"]: + rules = generate_ufw_rules(https_domains) + if SFTP_HOST: + rules += "\n# โ”€โ”€ SFTP Deployment Server โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n" + rules += generate_sftp_ufw_rules(SFTP_HOST, SFTP_PORT) + with open("firewall-rules-ufw.sh", "w") as f: + f.write(rules) + print("โœ“ Generated UFW rules: firewall-rules-ufw.sh") + + if firewall_type in ["firewalld", "all"]: + rules = generate_firewalld_rules(https_domains) + if SFTP_HOST: + rules += "\n# โ”€โ”€ SFTP Deployment Server โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n" + rules += generate_sftp_firewalld_rules(SFTP_HOST, SFTP_PORT) + with open("firewall-rules-firewalld.sh", "w") as f: + f.write(rules) + print("โœ“ Generated firewalld rules: firewall-rules-firewalld.sh") + + if firewall_type in ["aws-security-group", "all"]: + rules = generate_aws_security_group(all_domains) + with open("firewall-rules-aws-sg.json", "w") as f: + json.dump(rules, f, indent=2) + print("โœ“ Generated AWS Security Group rules: firewall-rules-aws-sg.json") + + if output_format in ["yaml", "all"]: + with open("trusted-domains.yml", "w") as f: + yaml.dump(TRUSTED_DOMAINS, f, default_flow_style=False) + print("โœ“ Generated YAML domain list: trusted-domains.yml") + + if output_format in ["json", "all"]: + with open("trusted-domains.json", "w") as f: + json.dump(TRUSTED_DOMAINS, f, indent=2) + print("โœ“ Generated JSON domain list: trusted-domains.json") + + if output_format in ["markdown", "all"]: + md = generate_markdown_documentation(TRUSTED_DOMAINS) + with open("FIREWALL_CONFIGURATION.md", "w") as f: + f.write(md) + print("โœ“ Generated documentation: FIREWALL_CONFIGURATION.md") + + print("") + print("Domain Categories:") + for category, domains in TRUSTED_DOMAINS.items(): + print(f" - {category}: {len(domains)} domains") + + print("") + print("Total unique domains: ", len(all_domains)) + + if __name__ == "__main__": + main() + PYTHON_EOF + + chmod +x generate_firewall_config.py + pip install PyYAML + python3 generate_firewall_config.py + + - name: Upload Firewall Configuration Artifacts + uses: actions/upload-artifact@v6 + with: + name: firewall-configurations + path: | + firewall-rules-*.sh + firewall-rules-*.json + trusted-domains.* + FIREWALL_CONFIGURATION.md + retention-days: 90 + + - name: Display Summary + run: | + echo "## Firewall Configuration" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "**Mode**: Manual Execution" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Firewall rules have been generated for enterprise-ready deployments." >> $GITHUB_STEP_SUMMARY + else + echo "**Mode**: Automatic Execution (Coding Agent Active)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "This workflow ran automatically because a coding agent (GitHub Copilot) is active." >> $GITHUB_STEP_SUMMARY + echo "Firewall configuration has been validated for the coding agent environment." >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Files Generated" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if ls firewall-rules-* trusted-domains.* FIREWALL_CONFIGURATION.md 2>/dev/null; then + ls -lh firewall-rules-* trusted-domains.* FIREWALL_CONFIGURATION.md 2>/dev/null | awk '{print "- " $9 " (" $5 ")"}' >> $GITHUB_STEP_SUMMARY + else + echo "- Documentation generated" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "### Download Artifacts" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Download the generated firewall configurations from the workflow artifacts." >> $GITHUB_STEP_SUMMARY + else + echo "### Trusted Domains Active" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "The coding agent has access to:" >> $GITHUB_STEP_SUMMARY + echo "- License providers (GPL, OSI, SPDX, Apache, etc.)" >> $GITHUB_STEP_SUMMARY + echo "- Package registries (npm, PyPI, Packagist, RubyGems)" >> $GITHUB_STEP_SUMMARY + echo "- Documentation sources (GitHub, Joomla, Dolibarr, PHP)" >> $GITHUB_STEP_SUMMARY + echo "- Standards organizations (W3C, IETF, JSON Schema)" >> $GITHUB_STEP_SUMMARY + fi + +# Usage Instructions: +# +# This workflow runs in two modes: +# +# 1. AUTOMATIC MODE (Coding Agent): +# - Triggers when coding agent branches (copilot/**, agent/**) are pushed or PR'd +# - Validates firewall configuration for the coding agent environment +# - Documents accessible domains for compliance +# - Ensures license sources and package registries are available +# +# 2. MANUAL MODE (Enterprise Configuration): +# - Manually trigger from the Actions tab +# - Select desired firewall type and output format +# - Download generated artifacts +# - Apply firewall rules to your enterprise environment +# +# Configuration: +# - Trusted domains are sourced from .github/copilot.yml +# - Modify copilot.yml to add/remove trusted domains +# - Changes automatically propagate to firewall rules +# +# Important Notes: +# - Review generated rules before applying to production +# - Some domains may use CDNs with dynamic IPs +# - Consider using FQDN-based rules where supported +# - Test thoroughly in staging environment first +# - Monitor logs for blocked connections +# - Update rules as domains/services change diff --git a/.github/workflows/repo_health.yml b/.github/workflows/repo_health.yml new file mode 100644 index 00000000..885203aa --- /dev/null +++ b/.github/workflows/repo_health.yml @@ -0,0 +1,795 @@ +# ============================================================================ +# Copyright (C) 2025 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: GitHub.Workflow +# INGROUP: MokoStandards.Validation +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /.github/workflows/repo_health.yml +# VERSION: 04.05.00 +# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts. +# NOTE: Field is user-managed. +# ============================================================================ + +name: Repo Health + +concurrency: + group: repo-health-${{ github.repository }}-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +on: + workflow_dispatch: + inputs: + profile: + description: 'Validation profile: all, release, scripts, or repo' + required: true + default: all + type: choice + options: + - all + - release + - scripts + - repo + pull_request: + push: + +permissions: + contents: read + +env: + # Release policy - Repository Variables Only + RELEASE_REQUIRED_REPO_VARS: RS_FTP_PATH_SUFFIX + RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX + + # Scripts governance policy + # Note: directories listed without a trailing slash. + SCRIPTS_REQUIRED_DIRS: + SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate + + # Repo health policy + # Files are listed as-is; directories must end with a trailing slash. + REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.github/workflows/ + REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/ + REPO_DISALLOWED_DIRS: + REPO_DISALLOWED_FILES: TODO.md,todo.md + + # Extended checks toggles + EXTENDED_CHECKS: "true" + + # File / directory variables (moved to top-level env) + DOCS_INDEX: docs/docs-index.md + SCRIPT_DIR: scripts + WORKFLOWS_DIR: .github/workflows + SHELLCHECK_PATTERN: '*.sh' + SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml' + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + access_check: + name: Access control + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + + outputs: + allowed: ${{ steps.perm.outputs.allowed }} + permission: ${{ steps.perm.outputs.permission }} + + steps: + - name: Check actor permission (admin only) + id: perm + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GH_TOKEN }} + script: | + const actor = context.actor; + let permission = "unknown"; + let allowed = false; + let method = ""; + + // Hardcoded authorized users โ€” always allowed + const authorizedUsers = ["jmiller-moko", "github-actions[bot]"]; + if (authorizedUsers.includes(actor)) { + allowed = true; + permission = "admin"; + method = "hardcoded allowlist"; + } else { + // Check via API for other actors + try { + const res = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: actor, + }); + permission = (res?.data?.permission || "unknown").toLowerCase(); + allowed = permission === "admin" || permission === "maintain"; + method = "repo collaborator API"; + } catch (error) { + core.warning(`Could not fetch permissions for '${actor}': ${error.message}`); + permission = "unknown"; + allowed = false; + method = "API error"; + } + } + + core.setOutput("permission", permission); + core.setOutput("allowed", allowed ? "true" : "false"); + + const lines = [ + "## ๐Ÿ” Access Authorization", + "", + "| Field | Value |", + "|-------|-------|", + `| **Actor** | \`${actor}\` |`, + `| **Repository** | \`${context.repo.owner}/${context.repo.repo}\` |`, + `| **Permission** | \`${permission}\` |`, + `| **Method** | ${method} |`, + `| **Authorized** | ${allowed} |`, + `| **Trigger** | \`${context.eventName}\` |`, + `| **Branch** | \`${context.ref.replace('refs/heads/', '')}\` |`, + "", + allowed + ? `โœ… ${actor} authorized (${method})` + : `โŒ ${actor} is NOT authorized. Requires admin or maintain role, or be in the hardcoded allowlist.`, + ]; + + await core.summary.addRaw(lines.join("\n")).write(); + + - name: Deny execution when not permitted + if: ${{ steps.perm.outputs.allowed != 'true' }} + run: | + set -euo pipefail + printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}" + exit 1 + + release_config: + name: Release configuration + needs: access_check + if: ${{ needs.access_check.outputs.allowed == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + + - name: Guardrails release vars + env: + PROFILE_RAW: ${{ github.event.inputs.profile }} + RS_FTP_PATH_SUFFIX: ${{ vars.RS_FTP_PATH_SUFFIX }} + DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }} + run: | + set -euo pipefail + + profile="${PROFILE_RAW:-all}" + case "${profile}" in + all|release|scripts|repo) ;; + *) + printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" + exit 1 + ;; + esac + + if [ "${profile}" = 'scripts' ] || [ "${profile}" = 'repo' ]; then + { + printf '%s\n' '### Release configuration (Repository Variables)' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' 'Status: SKIPPED' + printf '%s\n' 'Reason: profile excludes release validation' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 0 + fi + + IFS=',' read -r -a required <<< "${RELEASE_REQUIRED_REPO_VARS}" + IFS=',' read -r -a optional <<< "${RELEASE_OPTIONAL_REPO_VARS}" + + missing=() + missing_optional=() + + for k in "${required[@]}"; do + v="${!k:-}" + [ -z "${v}" ] && missing+=("${k}") + done + + for k in "${optional[@]}"; do + v="${!k:-}" + [ -z "${v}" ] && missing_optional+=("${k}") + done + + { + printf '%s\n' '### Release configuration (Repository Variables)' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' '| Variable | Status |' + printf '%s\n' '|---|---|' + printf '%s\n' "| RS_FTP_PATH_SUFFIX | ${RS_FTP_PATH_SUFFIX:-NOT SET} |" + printf '%s\n' "| DEV_FTP_SUFFIX | ${DEV_FTP_SUFFIX:-NOT SET} |" + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + if [ "${#missing_optional[@]}" -gt 0 ]; then + { + printf '%s\n' '### Missing optional repository variables' + for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + if [ "${#missing[@]}" -gt 0 ]; then + { + printf '%s\n' '### Missing required repository variables' + for m in "${missing[@]}"; do printf '%s\n' "- ${m}"; done + printf '%s\n' 'ERROR: Guardrails failed. Missing required repository variables.' + } >> "${GITHUB_STEP_SUMMARY}" + exit 1 + fi + + { + printf '%s\n' '### Repository variables validation result' + printf '%s\n' 'Status: OK' + printf '%s\n' 'All required repository variables present.' + printf '%s\n' '' + printf '%s\n' '**Note**: Organization secrets (RS_FTP_HOST, RS_FTP_USER, etc.) are validated at deployment time, not in repository health checks.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + scripts_governance: + name: Scripts governance + needs: access_check + if: ${{ needs.access_check.outputs.allowed == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + + - name: Scripts folder checks + env: + PROFILE_RAW: ${{ github.event.inputs.profile }} + run: | + set -euo pipefail + + profile="${PROFILE_RAW:-all}" + case "${profile}" in + all|release|scripts|repo) ;; + *) + printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" + exit 1 + ;; + esac + + if [ "${profile}" = 'release' ] || [ "${profile}" = 'repo' ]; then + { + printf '%s\n' '### Scripts governance' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' 'Status: SKIPPED' + printf '%s\n' 'Reason: profile excludes scripts governance' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 0 + fi + + if [ ! -d "${SCRIPT_DIR}" ]; then + { + printf '%s\n' '### Scripts governance' + printf '%s\n' 'Status: OK (advisory)' + printf '%s\n' 'scripts/ directory not present. No scripts governance enforced.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 0 + fi + + IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}" + IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}" + + missing_dirs=() + unapproved_dirs=() + + for d in "${required_dirs[@]}"; do + req="${d%/}" + [ ! -d "${req}" ] && missing_dirs+=("${req}/") + done + + while IFS= read -r d; do + allowed=false + for a in "${allowed_dirs[@]}"; do + a_norm="${a%/}" + [ "${d%/}" = "${a_norm}" ] && allowed=true + done + [ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/") + done < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##') + + { + printf '%s\n' '### Scripts governance' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' '| Area | Status | Notes |' + printf '%s\n' '|---|---|---|' + + if [ "${#missing_dirs[@]}" -gt 0 ]; then + printf '%s\n' '| Required directories | Warning | Missing required subfolders |' + else + printf '%s\n' '| Required directories | OK | All required subfolders present |' + fi + + if [ "${#unapproved_dirs[@]}" -gt 0 ]; then + printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |' + else + printf '%s\n' '| Directory policy | OK | No unapproved directories |' + fi + + printf '%s\n' '| Enforcement mode | Advisory | scripts folder is optional |' + printf '\n' + + if [ "${#missing_dirs[@]}" -gt 0 ]; then + printf '%s\n' 'Missing required script directories:' + for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + else + printf '%s\n' 'Missing required script directories: none.' + printf '\n' + fi + + if [ "${#unapproved_dirs[@]}" -gt 0 ]; then + printf '%s\n' 'Unapproved script directories detected:' + for m in "${unapproved_dirs[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + else + printf '%s\n' 'Unapproved script directories detected: none.' + printf '\n' + fi + + printf '%s\n' 'Scripts governance completed in advisory mode.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + repo_health: + name: Repository health + needs: access_check + if: ${{ needs.access_check.outputs.allowed == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + + - name: Repository health checks + env: + PROFILE_RAW: ${{ github.event.inputs.profile }} + run: | + set -euo pipefail + + profile="${PROFILE_RAW:-all}" + case "${profile}" in + all|release|scripts|repo) ;; + *) + printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" + exit 1 + ;; + esac + + if [ "${profile}" = 'release' ] || [ "${profile}" = 'scripts' ]; then + { + printf '%s\n' '### Repository health' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' 'Status: SKIPPED' + printf '%s\n' 'Reason: profile excludes repository health' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 0 + fi + + # Source directory: src/ or htdocs/ (either is valid) + if [ -d "src" ]; then + SOURCE_DIR="src" + elif [ -d "htdocs" ]; then + SOURCE_DIR="htdocs" + else + missing_required+=("src/ or htdocs/ (source directory required)") + fi + + IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}" + IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}" + IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}" + IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES}" + + missing_required=() + missing_optional=() + + for item in "${required_artifacts[@]}"; do + if printf '%s' "${item}" | grep -q '/$'; then + d="${item%/}" + [ ! -d "${d}" ] && missing_required+=("${item}") + else + [ ! -f "${item}" ] && missing_required+=("${item}") + fi + done + + # Optional entries: handle files and directories (trailing slash indicates dir) + for f in "${optional_files[@]}"; do + if printf '%s' "${f}" | grep -q '/$'; then + d="${f%/}" + [ ! -d "${d}" ] && missing_optional+=("${f}") + else + [ ! -f "${f}" ] && missing_optional+=("${f}") + fi + done + + for d in "${disallowed_dirs[@]}"; do + d_norm="${d%/}" + [ -d "${d_norm}" ] && missing_required+=("${d_norm}/ (disallowed)") + done + + for f in "${disallowed_files[@]}"; do + [ -f "${f}" ] && missing_required+=("${f} (disallowed)") + done + + git fetch origin --prune + + dev_paths=() + dev_branches=() + + # Look for remote branches matching origin/dev*. + # A plain origin/dev is considered invalid; we require dev/ branches. + while IFS= read -r b; do + name="${b#origin/}" + if [ "${name}" = 'dev' ]; then + dev_branches+=("${name}") + else + dev_paths+=("${name}") + fi + done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//') + + # If there are no dev/* branches, fail the guardrail. + if [ "${#dev_paths[@]}" -eq 0 ]; then + missing_required+=("dev/* branch (e.g. dev/01.00.00)") + fi + + # If a plain dev branch exists (origin/dev), flag it as invalid. + if [ "${#dev_branches[@]}" -gt 0 ]; then + missing_required+=("invalid branch dev (must be dev/)") + fi + + content_warnings=() + + if [ -f 'CHANGELOG.md' ] && ! grep -Eq '^# Changelog' CHANGELOG.md; then + content_warnings+=("CHANGELOG.md missing '# Changelog' header") + fi + + if [ -f 'CHANGELOG.md' ] && grep -Eq '^[# ]*Unreleased' CHANGELOG.md; then + content_warnings+=("CHANGELOG.md contains Unreleased section (review release readiness)") + fi + + if [ -f 'LICENSE' ] && ! grep -qiE 'GNU GENERAL PUBLIC LICENSE|GPL' LICENSE; then + content_warnings+=("LICENSE does not look like a GPL text") + fi + + if [ -f 'README.md' ] && ! grep -qiE 'moko|Moko' README.md; then + content_warnings+=("README.md missing expected brand keyword") + fi + + export PROFILE_RAW="${profile}" + export MISSING_REQUIRED="$(printf '%s\n' "${missing_required[@]:-}")" + export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")" + export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")" + + report_json="$(python3 - <<'PY' + import json + import os + + profile = os.environ.get('PROFILE_RAW') or 'all' + + missing_required = os.environ.get('MISSING_REQUIRED', '').splitlines() if os.environ.get('MISSING_REQUIRED') else [] + missing_optional = os.environ.get('MISSING_OPTIONAL', '').splitlines() if os.environ.get('MISSING_OPTIONAL') else [] + content_warnings = os.environ.get('CONTENT_WARNINGS', '').splitlines() if os.environ.get('CONTENT_WARNINGS') else [] + + out = { + 'profile': profile, + 'missing_required': [x for x in missing_required if x], + 'missing_optional': [x for x in missing_optional if x], + 'content_warnings': [x for x in content_warnings if x], + } + + print(json.dumps(out, indent=2)) + PY + )" + + { + printf '%s\n' '### Repository health' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' '| Metric | Value |' + printf '%s\n' '|---|---|' + printf '%s\n' "| Missing required | ${#missing_required[@]} |" + printf '%s\n' "| Missing optional | ${#missing_optional[@]} |" + printf '%s\n' "| Content warnings | ${#content_warnings[@]} |" + printf '\n' + + printf '%s\n' '### Guardrails report (JSON)' + printf '%s\n' '```json' + printf '%s\n' "${report_json}" + printf '%s\n' '```' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + if [ "${#missing_required[@]}" -gt 0 ]; then + { + printf '%s\n' '### Missing required repo artifacts' + for m in "${missing_required[@]}"; do printf '%s\n' "- ${m}"; done + printf '%s\n' 'ERROR: Guardrails failed. Missing required repository artifacts.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 1 + fi + + if [ "${#missing_optional[@]}" -gt 0 ]; then + { + printf '%s\n' '### Missing optional repo artifacts' + for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + if [ "${#content_warnings[@]}" -gt 0 ]; then + { + printf '%s\n' '### Repo content warnings' + for m in "${content_warnings[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + # โ”€โ”€ Joomla-specific checks โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + joomla_findings=() + + # XML manifest: find any XML file containing tag)") + else + # Check tag exists + if ! grep -qP '' "${MANIFEST}"; then + joomla_findings+=("XML manifest: tag missing") + fi + # Check extension type attribute + if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then + joomla_findings+=("XML manifest: type attribute missing or invalid") + fi + # Check tag + if ! grep -qP '' "${MANIFEST}"; then + joomla_findings+=("XML manifest: tag missing") + fi + # Check tag + if ! grep -qP '' "${MANIFEST}"; then + joomla_findings+=("XML manifest: tag missing") + fi + # Check for Joomla 5+ + if ! grep -qP ' missing (required for Joomla 5+)") + fi + fi + + # Language files: check for at least one .ini file + INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)" + if [ "${INI_COUNT}" -eq 0 ]; then + joomla_findings+=("No .ini language files found") + fi + + # updates.xml must exist in root (Joomla update server) + if [ ! -f 'updates.xml' ]; then + joomla_findings+=("updates.xml missing in root (required for Joomla update server)") + fi + + # index.html files for directory listing protection + INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site") + for dir in "${INDEX_DIRS[@]}"; do + if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then + joomla_findings+=("${dir}/index.html missing (directory listing protection)") + fi + done + + if [ "${#joomla_findings[@]}" -gt 0 ]; then + { + printf '%s\n' '### Joomla extension checks' + printf '%s\n' '| Check | Status |' + printf '%s\n' '|---|---|' + for f in "${joomla_findings[@]}"; do + printf '%s\n' "| ${f} | Warning |" + done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + else + { + printf '%s\n' '### Joomla extension checks' + printf '%s\n' 'All Joomla-specific checks passed.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + extended_enabled="${EXTENDED_CHECKS:-true}" + extended_findings=() + + if [ "${extended_enabled}" = 'true' ]; then + # CODEOWNERS presence + if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then + : + else + extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)") + fi + + # Workflow pinning advisory: flag uses @main/@master + if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then + bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)" + if [ -n "${bad_refs}" ]; then + extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt") + { + printf '%s\n' '### Workflow pinning advisory' + printf '%s\n' 'Found uses: entries pinned to main/master:' + printf '%s\n' '```' + printf '%s\n' "${bad_refs}" + printf '%s\n' '```' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + # Docs index link integrity (docs/docs-index.md) + if [ -f "${DOCS_INDEX}" ]; then + missing_links="$(python3 - <<'PY' + import os + import re + + idx = os.environ.get('DOCS_INDEX', 'docs/docs-index.md') + base = os.getcwd() + + bad = [] + pat = re.compile(r'\[[^\]]+\]\(([^)]+)\)') + + with open(idx, 'r', encoding='utf-8') as f: + for line in f: + for m in pat.findall(line): + link = m.strip() + if link.startswith('http://') or link.startswith('https://') or link.startswith('#') or link.startswith('mailto:'): + continue + if link.startswith('/'): + rel = link.lstrip('/') + else: + rel = os.path.normpath(os.path.join(os.path.dirname(idx), link)) + rel = rel.split('#', 1)[0] + rel = rel.split('?', 1)[0] + if not rel: + continue + p = os.path.join(base, rel) + if not os.path.exists(p): + bad.append(rel) + + print('\n'.join(sorted(set(bad)))) + PY + )" + if [ -n "${missing_links}" ]; then + extended_findings+=("docs/docs-index.md contains broken relative links") + { + printf '%s\n' '### Docs index link integrity' + printf '%s\n' 'Broken relative links:' + while IFS= read -r l; do [ -n "${l}" ] && printf '%s\n' "- ${l}"; done <<< "${missing_links}" + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + # ShellCheck advisory + if [ -d "${SCRIPT_DIR}" ]; then + if ! command -v shellcheck >/dev/null 2>&1; then + sudo apt-get update -qq + sudo apt-get install -y shellcheck >/dev/null + fi + + sc_out='' + while IFS= read -r shf; do + [ -z "${shf}" ] && continue + out_one="$(shellcheck -S warning -x "${shf}" 2>/dev/null || true)" + if [ -n "${out_one}" ]; then + sc_out="${sc_out}${out_one}\n" + fi + done < <(find "${SCRIPT_DIR}" -type f -name "${SHELLCHECK_PATTERN}" 2>/dev/null | sort) + + if [ -n "${sc_out}" ]; then + extended_findings+=("ShellCheck warnings detected (advisory)") + sc_head="$(printf '%s' "${sc_out}" | head -n 200)" + { + printf '%s\n' '### ShellCheck (advisory)' + printf '%s\n' '```' + printf '%s\n' "${sc_head}" + printf '%s\n' '```' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + # SPDX header advisory for common source types + spdx_missing=() + IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}" + spdx_args=() + for g in "${spdx_globs[@]}"; do spdx_args+=("${g}"); done + + while IFS= read -r f; do + [ -z "${f}" ] && continue + if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then + spdx_missing+=("${f}") + fi + done < <(git ls-files "${spdx_args[@]}" 2>/dev/null || true) + + if [ "${#spdx_missing[@]}" -gt 0 ]; then + extended_findings+=("SPDX header missing in some tracked files (advisory)") + { + printf '%s\n' '### SPDX header advisory' + printf '%s\n' 'Files missing SPDX-License-Identifier (first 40 lines scan):' + for f in "${spdx_missing[@]}"; do printf '%s\n' "- ${f}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + # Git hygiene advisory: branches older than 180 days (remote) + stale_cutoff_days=180 + stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 [...] + if [ -n "${stale_branches}" ]; then + extended_findings+=("Stale remote branches detected (advisory)") + { + printf '%s\n' '### Git hygiene advisory' + printf '%s\n' "Branches with last commit older than ${stale_cutoff_days} days (sample up to 50):" + while IFS= read -r b; do [ -n "${b}" ] && printf '%s\n' "- ${b}"; done <<< "${stale_branches}" + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + { + printf '%s\n' '### Guardrails coverage matrix' + printf '%s\n' '| Domain | Status | Notes |' + printf '%s\n' '|---|---|---|' + printf '%s\n' '| Access control | OK | Admin-only execution gate |' + printf '%s\n' '| Release variables | OK | Repository variables validation |' + printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |' + printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |' + printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |' + if [ "${extended_enabled}" = 'true' ]; then + if [ "${#extended_findings[@]}" -gt 0 ]; then + printf '%s\n' '| Extended checks | Warning | See extended findings below |' + else + printf '%s\n' '| Extended checks | OK | No findings |' + fi + else + printf '%s\n' '| Extended checks | SKIPPED | EXTENDED_CHECKS disabled |' + fi + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + if [ "${extended_enabled}" = 'true' ] && [ "${#extended_findings[@]}" -gt 0 ]; then + { + printf '%s\n' '### Extended findings (advisory)' + for f in "${extended_findings[@]}"; do printf '%s\n' "- ${f}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}" diff --git a/.github/workflows/repository-cleanup.yml b/.github/workflows/repository-cleanup.yml new file mode 100644 index 00000000..b0780619 --- /dev/null +++ b/.github/workflows/repository-cleanup.yml @@ -0,0 +1,521 @@ +# 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.05.13 +# 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" + ) + + 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 new file mode 100644 index 00000000..773279de --- /dev/null +++ b/.github/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.05.00 +# BRIEF: MokoStandards compliance validation workflow +# NOTE: Validates repository structure, documentation, and coding standards + +name: Standards Compliance + +# โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— +# โ•‘ MOKOSTANDARDS COMPLIANCE WORKFLOW โ•‘ +# โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ +# โ•‘ โ•‘ +# โ•‘ 28 checks across 4 priority tiers: โ•‘ +# โ•‘ โ•‘ +# โ•‘ TIER 1 โ€” CRITICAL (must pass) โ•‘ +# โ•‘ secret-scanning, license-compliance, repository-structure, โ•‘ +# โ•‘ coding-standards, version-consistency โ•‘ +# โ•‘ โ•‘ +# โ•‘ TIER 2 โ€” IMPORTANT (should pass) โ•‘ +# โ•‘ workflow-validation, documentation-quality, readme-completeness, โ•‘ +# โ•‘ git-hygiene, script-integrity โ•‘ +# โ•‘ โ•‘ +# โ•‘ TIER 3 โ€” QUALITY (code metrics) โ•‘ +# โ•‘ line-length, file-naming, insecure-patterns, complexity, โ•‘ +# โ•‘ duplication, dead-code โ•‘ +# โ•‘ โ•‘ +# โ•‘ TIER 4 โ€” SUPPLEMENTARY (informational) โ•‘ +# โ•‘ file-size, binary, todo, deps, links, api-docs, accessibility, โ•‘ +# โ•‘ performance, enterprise, health, terraform โ•‘ +# โ•‘ โ•‘ +# โ•‘ File size: warning >15MB, critical >20MB โ•‘ +# โ•‘ Exempt: .mmdb, .woff2, .woff, .ttf, .otf โ•‘ +# โ•‘ โ•‘ +# โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +env: + WORKFLOW_VERSION: "04.04.01" + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +# MokoStandards Policy Compliance: +# - File formatting: Enforces organizational coding standards +# - Reference: docs/policy/file-formatting.md + +# โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +# โ”‚ WORKFLOW FLOW DIAGRAM โ”‚ +# โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +# +# TRIGGER: Push/PR to main/dev/rc branches +# โ”‚ +# โ–ผ +# โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +# โ”‚ PARALLEL VALIDATION CHECKS โ”‚ +# โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +# โ”‚ +# โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +# โ–ผ โ–ผ โ–ผ โ–ผ โ–ผ +# โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +# โ”‚Repository โ”‚File Header โ”‚Code Styleโ”‚ โ”‚ Docs โ”‚ โ”‚ License โ”‚ +# โ”‚Structureโ”‚ โ”‚ Validationโ”‚ โ”‚ Check โ”‚ โ”‚ Check โ”‚ โ”‚ Check โ”‚ +# โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +# โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +# โ–ผ โ–ผ โ–ผ โ–ผ โ–ผ +# โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +# โ”‚ Check โ”‚ โ”‚ Verify โ”‚ โ”‚ Run โ”‚ โ”‚ Check โ”‚ โ”‚ Verify โ”‚ +# โ”‚Required โ”‚ โ”‚Copyright โ”‚ โ”‚ Linters โ”‚ โ”‚README โ”‚ โ”‚SPDX-ID โ”‚ +# โ”‚ Dirs โ”‚ โ”‚ Header โ”‚ โ”‚(Python, โ”‚ โ”‚ Exists โ”‚ โ”‚ Present โ”‚ +# โ”‚ โ”‚ โ”‚ Format โ”‚ โ”‚PHP,YAML) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +# โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +# โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +# โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +# โ”‚ +# โ–ผ +# โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +# โ”‚ All Checks Pass?โ”‚ +# โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +# โ”‚ โ”‚ +# YES โ”‚ โ”‚ NO +# โ–ผ โ–ผ +# โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +# โ”‚ SUCCESS โ”‚ โ”‚ CREATE ISSUE โ”‚ +# โ”‚ Summary โ”‚ โ”‚ with Failure โ”‚ +# โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ Details โ”‚ +# โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +on: + push: + branches: [main, dev/**, rc/**, version/**] + pull_request: + branches: [main, dev/**, rc/**] + workflow_dispatch: + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + # TIER 1 โ€” CRITICAL (must pass, blocks merge) + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + secret-scanning: + name: Secret Scanning + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Scan for Secrets + run: | + set -x + echo "## ๐Ÿ”’ Secret Scanning" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Scanning for hardcoded secrets and credentials." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Define secret patterns + VIOLATIONS=0 + + # Check for common secret patterns + echo "### Secret Patterns" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Helper: scan with a pattern, show results with file:line, return count + scan_pattern() { + local label="$1" icon="$2" tmpfile="$3" + local count=0 + if [ -f "$tmpfile" ]; then + count=$(wc -l < "$tmpfile") + fi + if [ "$count" -gt 0 ]; then + echo "${icon} **${label}**: ${count} finding(s)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View locations" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| File | Line | Match |" >> $GITHUB_STEP_SUMMARY + echo "|------|------|-------|" >> $GITHUB_STEP_SUMMARY + head -20 "$tmpfile" | while IFS= read -r line; do + FILE=$(echo "$line" | cut -d: -f1 | sed 's|^\./||') + LINENO=$(echo "$line" | cut -d: -f2) + MATCH=$(echo "$line" | cut -d: -f3- | head -c 80 | sed 's/|/\\|/g') + echo "| \`${FILE}\` | ${LINENO} | \`${MATCH}\` |" >> $GITHUB_STEP_SUMMARY + done + if [ "$count" -gt 20 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "*... and $((count - 20)) more*" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + VIOLATIONS=$((VIOLATIONS + count)) + fi + } + + # Pattern 1: password/secret assignments + grep -r -n -E "(password|passwd|pwd|secret|api[_-]?key|token).*=.*['\"]" . \ + --include="*.php" --include="*.py" --include="*.js" --include="*.ts" \ + --exclude-dir=".git" --exclude-dir="vendor" --exclude-dir="node_modules" 2>/dev/null | \ + grep -v -E '(test|example|sample|getenv|getString|getArgument|config\[|/\.\*/|^\s*//|^\s*\*|CREDENTIAL_PATTERNS|SecurityValidator|SECRET_PATTERN|===|!==|ApiClient|str_contains|gen_wrappers)' | \ + grep -v "= ''" | grep -v '= ""' | grep -v '\$this->config' | \ + grep -v 'type="password"' | grep -v 'type="text"' | grep -v 'name="password"' | grep -v 'name="secretkey"' | \ + grep -v '/dev/null > /tmp/secrets2.txt || true + scan_pattern "Private keys" "โŒ" /tmp/secrets2.txt + + # Pattern 3: AWS keys + grep -r -n -E "AKIA[0-9A-Z]{16}" . \ + --include="*.php" --include="*.py" --include="*.js" --include="*.txt" --include="*.env" \ + --exclude-dir=".git" --exclude-dir="vendor" --exclude-dir="node_modules" 2>/dev/null > /tmp/secrets3.txt || true + scan_pattern "AWS access keys" "โŒ" /tmp/secrets3.txt + + # Pattern 4: GitHub tokens + grep -r -n -E "gh[ps]_[a-zA-Z0-9]{36}" . \ + --include="*.php" --include="*.py" --include="*.js" --include="*.txt" --include="*.env" \ + --exclude-dir=".git" --exclude-dir="vendor" --exclude-dir="node_modules" 2>/dev/null > /tmp/secrets4.txt || true + scan_pattern "GitHub tokens" "โŒ" /tmp/secrets4.txt + + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$VIOLATIONS" -gt 0 ]; then + echo "**Total Violations**: $VIOLATIONS" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View detected secrets (file paths only)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + cat /tmp/secrets*.txt 2>/dev/null | cut -d: -f1 | sort -u >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Action Required**: Remove hardcoded secrets immediately!" >> $GITHUB_STEP_SUMMARY + echo "Use environment variables or secrets management instead." >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "โœ… No hardcoded secrets detected" >> $GITHUB_STEP_SUMMARY + fi + + license-compliance: + name: License Header Validation + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check SPDX Headers + run: | + set -x + echo "### SPDX License Header Check" >> $GITHUB_STEP_SUMMARY + + # Count source files with and without SPDX headers + TOTAL_PHP=0 + WITH_SPDX_PHP=0 + + if find . -name "*.php" -type f ! -path "./vendor/*" | head -1 | grep -q .; then + TOTAL_PHP=$(find . -name "*.php" -type f ! -path "./vendor/*" | wc -l) + WITH_SPDX_PHP=$(find . -name "*.php" -type f ! -path "./vendor/*" -exec grep -l "SPDX-License-Identifier" {} \; | wc -l) + fi + + if [ "$TOTAL_PHP" -gt 0 ]; then + PERCENT=$((WITH_SPDX_PHP * 100 / TOTAL_PHP)) + echo "- PHP files: $WITH_SPDX_PHP/$TOTAL_PHP ($PERCENT%) with SPDX headers" >> $GITHUB_STEP_SUMMARY + + if [ "$PERCENT" -lt 80 ]; then + echo "โš ๏ธ Less than 80% of PHP files have SPDX headers" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… Good SPDX header coverage" >> $GITHUB_STEP_SUMMARY + fi + fi + + - name: Validate License File + run: | + set -x + echo "" >> $GITHUB_STEP_SUMMARY + echo "### License File Validation" >> $GITHUB_STEP_SUMMARY + + if [ ! -f "LICENSE" ]; then + echo "โŒ LICENSE file not found" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### โŒ Validation Failed: LICENSE File Missing" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Error:** LICENSE file is required for all MokoStandards-compliant repositories" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Add LICENSE file with appropriate open-source license (GPL-3.0-or-later recommended)" >> $GITHUB_STEP_SUMMARY + echo "" + echo "โŒ ERROR: LICENSE file not found - This is a critical requirement" + exit 1 + fi + + # Check license type + if grep -qi "GNU GENERAL PUBLIC LICENSE" LICENSE; then + VERSION=$(grep -i "Version 3" LICENSE || echo "") + if [ -n "$VERSION" ]; then + echo "โœ… GPL-3.0-or-later license detected" >> $GITHUB_STEP_SUMMARY + else + echo "โš ๏ธ GPL license detected but version unclear" >> $GITHUB_STEP_SUMMARY + fi + elif grep -qi "MIT License" LICENSE; then + echo "โœ… MIT license detected" >> $GITHUB_STEP_SUMMARY + elif grep -qi "Apache License" LICENSE; then + echo "โœ… Apache license detected" >> $GITHUB_STEP_SUMMARY + else + echo "โ„น๏ธ License type could not be automatically detected" >> $GITHUB_STEP_SUMMARY + fi + + repository-structure: + name: Repository Structure Validation + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check Required Directories + run: | + set -x + echo "## ๐Ÿ“ Repository Structure Validation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + MISSING=0 + PRESENT=0 + TOTAL=2 + + echo "### Required Directories" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Directory | Status | Files | Size | Notes |" >> $GITHUB_STEP_SUMMARY + echo "|-----------|--------|-------|------|-------|" >> $GITHUB_STEP_SUMMARY + + # Check required directories + for dir in docs .github; do + if [ -d "$dir" ]; then + FILE_COUNT=$(find "$dir" -type f 2>/dev/null | wc -l) + DIR_SIZE=$(du -sh "$dir" 2>/dev/null | cut -f1) + echo "| $dir/ | โœ… Pass | $FILE_COUNT files | $DIR_SIZE | Complete |" >> $GITHUB_STEP_SUMMARY + PRESENT=$((PRESENT + 1)) + else + echo "| $dir/ | โŒ **Missing** | - | - | **Action Required** |" >> $GITHUB_STEP_SUMMARY + MISSING=$((MISSING + 1)) + fi + done + + echo "" >> $GITHUB_STEP_SUMMARY + PERCENT=$((PRESENT * 100 / TOTAL)) + echo "**Compliance Score:** $PERCENT% ($PRESENT/$TOTAL directories present)" >> $GITHUB_STEP_SUMMARY + + if [ "$MISSING" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ๐Ÿ”ด Critical Issues: $MISSING" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Remediation Steps:**" >> $GITHUB_STEP_SUMMARY + [ ! -d "docs" ] && echo "- Create docs directory: \`mkdir docs && echo '# Documentation' > docs/README.md\`" >> $GITHUB_STEP_SUMMARY + [ ! -d ".github" ] && echo "- Create .github directory: \`mkdir -p .github/workflows\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "๐Ÿ“š Reference: [MokoStandards Repository Structure](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/docs/policy/core-structure.md)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### โŒ Validation Failed: Required Directories Missing" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Status:** Repository structure does not meet MokoStandards requirements" >> $GITHUB_STEP_SUMMARY + echo "**Missing:** $MISSING required director(y|ies)" >> $GITHUB_STEP_SUMMARY + echo "**Compliance:** $PERCENT% ($PRESENT/$TOTAL directories present)" >> $GITHUB_STEP_SUMMARY + echo "" + echo "โŒ ERROR: Required directories missing - See job summary for remediation steps" + exit 1 + fi + + - name: Check Required Files + run: | + set -x + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Required Files" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + MISSING=0 + PRESENT=0 + TOTAL=5 + + echo "| File | Status | Size | Last Modified | Notes |" >> $GITHUB_STEP_SUMMARY + echo "|------|--------|------|---------------|-------|" >> $GITHUB_STEP_SUMMARY + + # Check required files (CHANGELOG handled separately via find -iname to support src/ChangeLog.md) + for file in README.md LICENSE CONTRIBUTING.md SECURITY.md .editorconfig; do + if [ -f "$file" ]; then + FILE_SIZE=$(wc -c < "$file" 2>/dev/null | awk '{printf "%.1f KB", $1/1024}') + LAST_MOD=$(stat -c %y "$file" 2>/dev/null | cut -d' ' -f1 || echo "Unknown") + CONTENT_CHECK="" + + # Basic content validation + case "$file" in + "README.md") + LINES=$(wc -l < "$file") + [ "$LINES" -lt 10 ] && CONTENT_CHECK="โš ๏ธ Too short" + ;; + "LICENSE") + [ $(wc -c < "$file") -lt 100 ] && CONTENT_CHECK="โš ๏ธ Incomplete?" + ;; + esac + + echo "| $file | โœ… Pass | $FILE_SIZE | $LAST_MOD | Complete $CONTENT_CHECK |" >> $GITHUB_STEP_SUMMARY + PRESENT=$((PRESENT + 1)) + else + echo "| $file | โŒ **Missing** | - | - | **Required** |" >> $GITHUB_STEP_SUMMARY + MISSING=$((MISSING + 1)) + fi + done + + echo "" >> $GITHUB_STEP_SUMMARY + PERCENT=$((PRESENT * 100 / TOTAL)) + echo "**Compliance Score:** $PERCENT% ($PRESENT/$TOTAL files present)" >> $GITHUB_STEP_SUMMARY + + if [ "$MISSING" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ๐Ÿ”ด Critical Issues: $MISSING" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Remediation Steps:**" >> $GITHUB_STEP_SUMMARY + [ ! -f "README.md" ] && echo "- Create README.md: Use [template](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/templates/docs/required/README.md)" >> $GITHUB_STEP_SUMMARY + [ ! -f "LICENSE" ] && echo "- Add LICENSE file: Choose from [OSI-approved licenses](https://opensource.org/licenses)" >> $GITHUB_STEP_SUMMARY + [ ! -f "CONTRIBUTING.md" ] && echo "- Create CONTRIBUTING.md: Use [template](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/templates/docs/required/CONTRIBUTING.md)" >> $GITHUB_STEP_SUMMARY + [ ! -f "SECURITY.md" ] && echo "- Create SECURITY.md: Use [template](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/templates/docs/required/SECURITY.md)" >> $GITHUB_STEP_SUMMARY + [ ! -f ".editorconfig" ] && echo "- Add .editorconfig: Use [template](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/templates/.editorconfig)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "๐Ÿ“š Reference: [MokoStandards File Requirements](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/docs/policy/file-header-standards.md)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### โŒ Validation Failed: Required Files Missing" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Status:** Repository files do not meet MokoStandards requirements" >> $GITHUB_STEP_SUMMARY + echo "**Missing:** $MISSING required file(s)" >> $GITHUB_STEP_SUMMARY + echo "**Compliance:** $PERCENT% ($PRESENT/$TOTAL files present)" >> $GITHUB_STEP_SUMMARY + echo "" + echo "โŒ ERROR: Required files missing - See job summary for remediation steps" + exit 1 + fi + + coding-standards: + name: Coding Standards Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check for Tab Characters + run: | + set -x + echo "### Tab Character Detection" >> $GITHUB_STEP_SUMMARY + + # Policy: Tabs are DEFAULT. Only check for tabs in files that REQUIRE spaces. + # Languages requiring spaces: YAML, Python, Haskell, F#, CoffeeScript, Nim, JSON, RST + TABS_IN_SPACES_FILES=$(find . -type f \ + \( -name "*.yml" -o -name "*.yaml" \ + -o -name "*.py" \ + -o -name "*.hs" -o -name "*.lhs" \ + -o -name "*.fs" -o -name "*.fsx" -o -name "*.fsi" \ + -o -name "*.coffee" -o -name "*.litcoffee" \ + -o -name "*.nim" -o -name "*.nims" -o -name "*.nimble" \ + -o -name "*.json" \ + -o -name "*.rst" \) \ + ! -path "./vendor/*" \ + ! -path "./node_modules/*" \ + ! -path "./.git/*" \ + -exec grep -l $'\t' {} \; 2>/dev/null | head -10) + + if [ -n "$TABS_IN_SPACES_FILES" ]; then + echo "โš ๏ธ Tab characters found in files that require spaces:" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "$TABS_IN_SPACES_FILES" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "These languages require spaces (tabs will break): YAML, Python, Haskell, F#, CoffeeScript, Nim, JSON, RST" >> $GITHUB_STEP_SUMMARY + echo "All other files (including .md, .ps1, LICENSE, etc.) may use tabs per MokoStandards policy" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… No tabs found in files requiring spaces" >> $GITHUB_STEP_SUMMARY + echo "Note: Tabs are allowed in most files (policy default). Only checked files requiring spaces." >> $GITHUB_STEP_SUMMARY + fi + + - name: Check File Encoding + run: | + set -x + echo "" >> $GITHUB_STEP_SUMMARY + echo "### File Encoding Check" >> $GITHUB_STEP_SUMMARY + + # Check for UTF-8 encoding (ASCII is a subset of UTF-8 and is acceptable) + NON_UTF8=$(find . -type f \( -name "*.php" -o -name "*.js" -o -name "*.md" \) \ + ! -path "./vendor/*" \ + ! -path "./node_modules/*" \ + ! -path "./.git/*" \ + -exec file {} \; | grep -v "UTF-8" | grep -v "ASCII" | head -5) + + if [ -n "$NON_UTF8" ]; then + echo "โš ๏ธ Non-UTF-8 files detected:" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "$NON_UTF8" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… All source files appear to be UTF-8 encoded" >> $GITHUB_STEP_SUMMARY + fi + + - name: Check Line Endings + run: | + set -x + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Line Ending Check" >> $GITHUB_STEP_SUMMARY + + # Check for CRLF line endings + CRLF_FILES=$(find . -type f \( -name "*.php" -o -name "*.js" -o -name "*.md" \) \ + ! -path "./vendor/*" \ + ! -path "./node_modules/*" \ + ! -path "./.git/*" \ + -exec file {} \; | grep "CRLF" | head -5) + + if [ -n "$CRLF_FILES" ]; then + echo "โš ๏ธ Files with CRLF line endings found:" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "$CRLF_FILES" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "MokoStandards requires LF line endings" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… Line endings are consistent (LF)" >> $GITHUB_STEP_SUMMARY + fi + + version-consistency: + name: Version Consistency Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Set up PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: '8.1' + extensions: json + tools: composer + coverage: none + + - name: Setup MokoStandards tools + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' + run: | + git clone --depth 1 --branch version/04 --quiet \ + "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ + /tmp/mokostandards 2>/dev/null || true + if [ -d "/tmp/mokostandards" ] && [ -f "/tmp/mokostandards/composer.json" ]; then + cd /tmp/mokostandards + composer install --no-dev --no-interaction --quiet 2>/dev/null || true + fi + + - name: Run Version Consistency Check + id: version_check + run: | + set -x + echo "## ๐Ÿ”ข Version Consistency Validation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Use MokoStandards tools (no Composer needed on the governed repo) + if [ -f "/tmp/mokostandards/api/validate/check_version_consistency.php" ]; then + php /tmp/mokostandards/api/validate/check_version_consistency.php --path . --verbose 2>&1 | tee /tmp/version-check.log + EXIT_CODE=${PIPESTATUS[0]} + elif [ -f "api/validate/check_version_consistency.php" ]; then + php api/validate/check_version_consistency.php --path . --verbose 2>&1 | tee /tmp/version-check.log + EXIT_CODE=${PIPESTATUS[0]} + else + echo "โญ๏ธ MokoStandards tools not available โ€” skipping version check" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + echo '```' >> $GITHUB_STEP_SUMMARY + cat /tmp/version-check.log >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + + if [ "$EXIT_CODE" -eq 0 ]; then + echo "โœ… All version numbers are consistent" >> $GITHUB_STEP_SUMMARY + else + echo "โŒ Version drift detected" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + # TIER 2 โ€” IMPORTANT (should pass) + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + workflow-validation: + name: Workflow Configuration Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check Required Workflows + run: | + set -x + echo "### GitHub Actions Workflows" >> $GITHUB_STEP_SUMMARY + + WORKFLOWS_DIR=".github/workflows" + + if [ ! -d "$WORKFLOWS_DIR" ]; then + echo "โŒ No workflows directory found" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### โŒ Validation Failed: Workflows Directory Missing" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Error:** .github/workflows directory is required for CI/CD automation" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Create .github/workflows directory and add GitHub Actions workflows" >> $GITHUB_STEP_SUMMARY + echo "" + echo "โŒ ERROR: .github/workflows directory not found" + exit 1 + fi + + # Check for recommended workflows + CI_FOUND=false + for wf in ci.yml build.yml ci-dolibarr.yml ci-joomla.yml; do + if [ -f "$WORKFLOWS_DIR/$wf" ]; then + echo "โœ… CI workflow present ($wf)" >> $GITHUB_STEP_SUMMARY + CI_FOUND=true + break + fi + done + if [ "$CI_FOUND" = "false" ]; then + echo "โš ๏ธ No CI workflow found (ci.yml, build.yml, ci-dolibarr.yml, or ci-joomla.yml)" >> $GITHUB_STEP_SUMMARY + fi + + if [ -f "$WORKFLOWS_DIR/codeql-analysis.yml" ]; then + echo "โœ… CodeQL security scanning present" >> $GITHUB_STEP_SUMMARY + else + echo "โš ๏ธ CodeQL workflow not found" >> $GITHUB_STEP_SUMMARY + fi + + # Check for MokoStandards-synced workflows + for wf in deploy-dev.yml deploy-demo.yml deploy-rs.yml sync-version-on-merge.yml auto-release.yml standards-compliance.yml enterprise-firewall-setup.yml; do + if [ -f "$WORKFLOWS_DIR/$wf" ]; then + echo "โœ… ${wf}" >> $GITHUB_STEP_SUMMARY + else + echo "โš ๏ธ ${wf} not found (synced from MokoStandards)" >> $GITHUB_STEP_SUMMARY + fi + done + + - name: Validate Workflow Syntax + run: | + set -x + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Workflow YAML Syntax" >> $GITHUB_STEP_SUMMARY + + INVALID=0 + for workflow in $(find .github/workflows -maxdepth 1 -type f \( -name "*.yml" -o -name "*.yaml" \) 2>/dev/null); do + if [ -f "$workflow" ]; then + if python3 -c "import yaml, sys; yaml.safe_load(open(sys.argv[1]))" "$workflow" 2>/dev/null; then + echo "โœ… $(basename $workflow)" >> $GITHUB_STEP_SUMMARY + else + echo "โŒ $(basename $workflow) - invalid YAML" >> $GITHUB_STEP_SUMMARY + INVALID=$((INVALID + 1)) + fi + fi + done + + if [ "$INVALID" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "### โŒ Validation Failed: Invalid Workflow YAML Syntax" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Error:** $INVALID workflow file(s) have invalid YAML syntax" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Fix YAML syntax errors in the marked workflow files" >> $GITHUB_STEP_SUMMARY + echo "**Tool:** Run \`python3 -c \"import yaml; yaml.safe_load(open('.github/workflows/FILE.yml'))\"\` locally" >> $GITHUB_STEP_SUMMARY + echo "" + echo "โŒ ERROR: $INVALID workflow file(s) with invalid YAML syntax" + exit 1 + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "### โœ… All Workflow Files Have Valid YAML Syntax" >> $GITHUB_STEP_SUMMARY + echo "" + echo "โœ… SUCCESS: All workflow files passed YAML validation" + + - name: Validate CodeQL Configuration + if: hashFiles('.github/workflows/codeql-analysis.yml') != '' + run: | + set -e + echo "" >> $GITHUB_STEP_SUMMARY + echo "### CodeQL Language Configuration" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Inline validation (rewritten from Python to bash for PHP-only architecture) + CODEQL_FILE=".github/workflows/codeql-analysis.yml" + + if [ ! -f "$CODEQL_FILE" ]; then + echo "โš ๏ธ CodeQL workflow file not found" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### โš ๏ธ CodeQL Workflow Not Found" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Status:** CodeQL workflow file not present - skipping language validation" >> $GITHUB_STEP_SUMMARY + echo "" + echo "โš ๏ธ INFO: CodeQL workflow not found - Skipping validation" + exit 0 + fi + + echo "**CodeQL Configuration Analysis**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Extract configured languages from workflow + LANGUAGES=$(grep -A5 "language:" "$CODEQL_FILE" | grep -oP "(?<=')[^']+(?=')" | tr '\n' ' ' || echo "") + + # Check if this is a configuration-only scan (no languages specified) + if grep -q "category.*language:config" "$CODEQL_FILE"; then + echo "**Scan Type:** Configuration-only (no language matrix)" >> $GITHUB_STEP_SUMMARY + echo "**Status:** โœ… Valid configuration for PHP-only repository" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "This CodeQL workflow scans YAML, JSON, shell scripts for security issues." >> $GITHUB_STEP_SUMMARY + echo "PHP security is handled by SecurityValidator enterprise library." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "โœ… SUCCESS: CodeQL configuration-only scan properly configured" + exit 0 + fi + + if [ -z "$LANGUAGES" ]; then + echo "โŒ No languages configured in CodeQL workflow" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### โŒ Validation Failed: CodeQL Languages Not Configured" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Error:** CodeQL workflow exists but has no languages configured" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Configure appropriate languages in codeql-analysis.yml" >> $GITHUB_STEP_SUMMARY + echo "" + echo "โŒ ERROR: No languages configured in CodeQL workflow" + exit 1 + fi + + echo "**Configured Languages:** $LANGUAGES" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Validate language presence in repository + INVALID_LANGS="" + VALID_LANGS="" + + for LANG in $LANGUAGES; do + case "$LANG" in + python) + # Check for Python files (should be none in v04.00.04) + if find . -name "*.py" -type f ! -path "./.git/*" | grep -q .; then + VALID_LANGS="$VALID_LANGS python" + echo "โœ… Python: Found Python files" >> $GITHUB_STEP_SUMMARY + else + INVALID_LANGS="$INVALID_LANGS python" + echo "โŒ Python: No Python files found (PHP-only repository)" >> $GITHUB_STEP_SUMMARY + fi + ;; + javascript|typescript) + # Check for JS/TS files + if find . \( -name "*.js" -o -name "*.ts" -o -name "*.json" \) -type f ! -path "./.git/*" ! -path "./node_modules/*" | grep -q .; then + VALID_LANGS="$VALID_LANGS $LANG" + echo "โœ… $LANG: Found JavaScript/TypeScript/JSON files" >> $GITHUB_STEP_SUMMARY + else + INVALID_LANGS="$INVALID_LANGS $LANG" + echo "โš ๏ธ $LANG: No JavaScript/TypeScript files found" >> $GITHUB_STEP_SUMMARY + fi + ;; + java) + if find . -name "*.java" -type f ! -path "./.git/*" | grep -q .; then + VALID_LANGS="$VALID_LANGS java" + echo "โœ… Java: Found Java files" >> $GITHUB_STEP_SUMMARY + else + INVALID_LANGS="$INVALID_LANGS java" + echo "โš ๏ธ Java: No Java files found" >> $GITHUB_STEP_SUMMARY + fi + ;; + go) + if find . -name "*.go" -type f ! -path "./.git/*" | grep -q .; then + VALID_LANGS="$VALID_LANGS go" + echo "โœ… Go: Found Go files" >> $GITHUB_STEP_SUMMARY + else + INVALID_LANGS="$INVALID_LANGS go" + echo "โš ๏ธ Go: No Go files found" >> $GITHUB_STEP_SUMMARY + fi + ;; + cpp|c) + if find . \( -name "*.cpp" -o -name "*.c" -o -name "*.h" \) -type f ! -path "./.git/*" | grep -q .; then + VALID_LANGS="$VALID_LANGS $LANG" + echo "โœ… $LANG: Found C/C++ files" >> $GITHUB_STEP_SUMMARY + else + INVALID_LANGS="$INVALID_LANGS $LANG" + echo "โš ๏ธ $LANG: No C/C++ files found" >> $GITHUB_STEP_SUMMARY + fi + ;; + ruby) + if find . -name "*.rb" -type f ! -path "./.git/*" | grep -q .; then + VALID_LANGS="$VALID_LANGS ruby" + echo "โœ… Ruby: Found Ruby files" >> $GITHUB_STEP_SUMMARY + else + INVALID_LANGS="$INVALID_LANGS ruby" + echo "โš ๏ธ Ruby: No Ruby files found" >> $GITHUB_STEP_SUMMARY + fi + ;; + *) + echo "โš ๏ธ $LANG: Unknown language, skipping validation" >> $GITHUB_STEP_SUMMARY + ;; + esac + done + + echo "" >> $GITHUB_STEP_SUMMARY + + # Report results + if [ -n "$INVALID_LANGS" ]; then + echo "**โš ๏ธ Warning:** Some configured languages may not have corresponding files:" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "Invalid languages: $INVALID_LANGS" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Note:** This is informational. CodeQL will skip languages without source files." >> $GITHUB_STEP_SUMMARY + echo "For PHP repository (v04.00.04), JavaScript language covers JSON/YAML/shell scripts." >> $GITHUB_STEP_SUMMARY + else + echo "โœ… **All configured CodeQL languages have corresponding source files**" >> $GITHUB_STEP_SUMMARY + fi + + # Always succeed - this is informational only + echo "" >> $GITHUB_STEP_SUMMARY + echo "### โœ… CodeQL Configuration Validation Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Status:** CodeQL language configuration reviewed successfully" >> $GITHUB_STEP_SUMMARY + echo "" + echo "โœ… SUCCESS: CodeQL validation complete" + exit 0 + + documentation-quality: + name: Documentation Quality Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Validate README.md + run: | + set -x + echo "## ๐Ÿ“š Documentation Quality Check" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### README.md Analysis" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ ! -f "README.md" ]; then + echo "โŒ **Critical:** README.md not found" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### โŒ Validation Failed: README.md Missing" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Error:** README.md is required for all MokoStandards-compliant repositories" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Create README.md with project description, setup instructions, and usage examples" >> $GITHUB_STEP_SUMMARY + echo "" + echo "โŒ ERROR: README.md not found - This is a critical requirement" + exit 1 + fi + + # Detailed content analysis + SIZE=$(wc -c < README.md) + LINES=$(wc -l < README.md) + WORDS=$(wc -w < README.md) + HEADINGS=$(grep -c "^#" README.md || echo 0) + LINKS=$(grep -c "\[.*\](.*)" README.md || echo 0) + CODE_BLOCKS=$(grep -c '```' README.md || echo 0) + + echo "| Metric | Value | Status | Recommendation |" >> $GITHUB_STEP_SUMMARY + echo "|--------|-------|--------|----------------|" >> $GITHUB_STEP_SUMMARY + + # Size check + SIZE_STATUS="โœ… Good" + SIZE_REC="Adequate length" + if [ "$SIZE" -lt 500 ]; then + SIZE_STATUS="โš ๏ธ Warning" + SIZE_REC="Add more content (min 500 bytes)" + elif [ "$SIZE" -gt 50000 ]; then + SIZE_STATUS="โš ๏ธ Warning" + SIZE_REC="Consider splitting into multiple docs" + fi + echo "| Size | $SIZE bytes | $SIZE_STATUS | $SIZE_REC |" >> $GITHUB_STEP_SUMMARY + + # Line count + LINES_STATUS="โœ… Good" + LINES_REC="Good size" + if [ "$LINES" -lt 20 ]; then + LINES_STATUS="โš ๏ธ Warning" + LINES_REC="Add more sections (min 20 lines)" + fi + echo "| Lines | $LINES | $LINES_STATUS | $LINES_REC |" >> $GITHUB_STEP_SUMMARY + + # Word count + WORDS_STATUS="โœ… Good" + WORDS_REC="Good detail" + if [ "$WORDS" -lt 100 ]; then + WORDS_STATUS="โš ๏ธ Warning" + WORDS_REC="Add more description (min 100 words)" + fi + echo "| Words | $WORDS | $WORDS_STATUS | $WORDS_REC |" >> $GITHUB_STEP_SUMMARY + + # Headings + HEADINGS_STATUS="โœ… Good" + HEADINGS_REC="Well structured" + if [ "$HEADINGS" -lt 3 ]; then + HEADINGS_STATUS="โš ๏ธ Warning" + HEADINGS_REC="Add more sections (min 3 headings)" + fi + echo "| Headings | $HEADINGS | $HEADINGS_STATUS | $HEADINGS_REC |" >> $GITHUB_STEP_SUMMARY + + # Links + LINKS_STATUS="โœ… Good" + LINKS_REC="Includes references" + if [ "$LINKS" -lt 1 ]; then + LINKS_STATUS="โ„น๏ธ Info" + LINKS_REC="Consider adding useful links" + fi + echo "| Links | $LINKS | $LINKS_STATUS | $LINKS_REC |" >> $GITHUB_STEP_SUMMARY + + # Code blocks + CODE_STATUS="โœ… Good" + CODE_REC="Includes examples" + if [ "$CODE_BLOCKS" -eq 0 ]; then + CODE_STATUS="โ„น๏ธ Info" + CODE_REC="Consider adding code examples" + fi + echo "| Code blocks | $CODE_BLOCKS | $CODE_STATUS | $CODE_REC |" >> $GITHUB_STEP_SUMMARY + + echo "" >> $GITHUB_STEP_SUMMARY + + # Check for key sections + echo "**Section Coverage:**" >> $GITHUB_STEP_SUMMARY + MISSING_COUNT=0 + grep -qi "install\|setup\|getting started" README.md && echo "- โœ… Installation/Setup instructions" >> $GITHUB_STEP_SUMMARY || { echo "- โš ๏ธ Missing: Installation/Setup" >> $GITHUB_STEP_SUMMARY; MISSING_COUNT=$((MISSING_COUNT + 1)); } + grep -qi "usage\|example\|how to" README.md && echo "- โœ… Usage examples" >> $GITHUB_STEP_SUMMARY || { echo "- โš ๏ธ Missing: Usage examples" >> $GITHUB_STEP_SUMMARY; MISSING_COUNT=$((MISSING_COUNT + 1)); } + grep -qi "license" README.md && echo "- โœ… License information" >> $GITHUB_STEP_SUMMARY || { echo "- โš ๏ธ Missing: License information" >> $GITHUB_STEP_SUMMARY; MISSING_COUNT=$((MISSING_COUNT + 1)); } + grep -qi "contribut" README.md && echo "- โœ… Contributing guidelines" >> $GITHUB_STEP_SUMMARY || echo "- โ„น๏ธ Optional: Contributing section" >> $GITHUB_STEP_SUMMARY + + if [ "$MISSING_COUNT" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**โš ๏ธ $MISSING_COUNT important sections missing**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Validate CHANGELOG.md + run: | + set -x + echo "" >> $GITHUB_STEP_SUMMARY + echo "### CHANGELOG.md Analysis" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Locate changelog case-insensitively; accepted at root, src/, or docs/ + CHANGELOG_PATH=$(find . -maxdepth 3 \( -path ./.git -o -path ./node_modules \) -prune \ + -o -iname "changelog.md" -print | head -1 | sed 's|^\./||') + + if [ -z "$CHANGELOG_PATH" ]; then + echo "โŒ **Critical:** CHANGELOG.md not found (checked root, src/, docs/)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### โŒ Validation Failed: CHANGELOG.md Missing" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Error:** CHANGELOG.md is required for all MokoStandards-compliant repositories" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Create CHANGELOG.md following [Keep a Changelog](https://keepachangelog.com/) format" >> $GITHUB_STEP_SUMMARY + echo "" + echo "โŒ ERROR: CHANGELOG.md not found - This is a critical requirement" + exit 1 + fi + + echo "๐Ÿ“„ Found: $CHANGELOG_PATH" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Analyze changelog structure + VERSIONS=$(grep -c "## \[" "$CHANGELOG_PATH" || echo 0) + UNRELEASED=$(grep -c "## \[Unreleased\]" "$CHANGELOG_PATH" || echo 0) + DATES=$(grep -c "[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}" "$CHANGELOG_PATH" || echo 0) + SIZE=$(wc -c < "$CHANGELOG_PATH") + + echo "| Metric | Value | Status | Notes |" >> $GITHUB_STEP_SUMMARY + echo "|--------|-------|--------|-------|" >> $GITHUB_STEP_SUMMARY + + # Check format + if grep -qi "## \[.*\]" "$CHANGELOG_PATH"; then + echo "| Format | Keep a Changelog | โœ… Pass | Standard format |" >> $GITHUB_STEP_SUMMARY + else + echo "| Format | Custom | โš ๏ธ Warning | Consider [Keep a Changelog](https://keepachangelog.com/) |" >> $GITHUB_STEP_SUMMARY + fi + + # Version count + VERSIONS_STATUS="โœ… Good" + VERSIONS_NOTE="Well maintained" + if [ "$VERSIONS" -lt 1 ]; then + VERSIONS_STATUS="โš ๏ธ Warning" + VERSIONS_NOTE="Add version entries" + fi + echo "| Versions | $VERSIONS | $VERSIONS_STATUS | $VERSIONS_NOTE |" >> $GITHUB_STEP_SUMMARY + + # Unreleased section + if [ "$UNRELEASED" -gt 0 ]; then + echo "| Unreleased | Yes | โœ… Good | Active development tracked |" >> $GITHUB_STEP_SUMMARY + else + echo "| Unreleased | No | โ„น๏ธ Info | Consider adding [Unreleased] section |" >> $GITHUB_STEP_SUMMARY + fi + + # Dates + DATES_STATUS="โœ… Good" + if [ "$DATES" -lt 1 ]; then + DATES_STATUS="โš ๏ธ Warning" + DATES_NOTE="Add release dates" + else + DATES_NOTE="Dates present" + fi + echo "| Release dates | $DATES | $DATES_STATUS | $DATES_NOTE |" >> $GITHUB_STEP_SUMMARY + + # Check for standard sections + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Changelog Sections:**" >> $GITHUB_STEP_SUMMARY + grep -qi "### Added" "$CHANGELOG_PATH" && echo "- โœ… Added section" >> $GITHUB_STEP_SUMMARY || echo "- โ„น๏ธ Added section (optional)" >> $GITHUB_STEP_SUMMARY + grep -qi "### Changed" "$CHANGELOG_PATH" && echo "- โœ… Changed section" >> $GITHUB_STEP_SUMMARY || echo "- โ„น๏ธ Changed section (optional)" >> $GITHUB_STEP_SUMMARY + grep -qi "### Fixed" "$CHANGELOG_PATH" && echo "- โœ… Fixed section" >> $GITHUB_STEP_SUMMARY || echo "- โ„น๏ธ Fixed section (optional)" >> $GITHUB_STEP_SUMMARY + + echo "" >> $GITHUB_STEP_SUMMARY + echo "๐Ÿ“š Reference: [Keep a Changelog](https://keepachangelog.com/)" >> $GITHUB_STEP_SUMMARY + + - name: Check Documentation Index + run: | + set -x + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Documentation Index" >> $GITHUB_STEP_SUMMARY + + if [ -f "docs/index.md" ] || [ -f "docs/README.md" ]; then + echo "โœ… Documentation index found" >> $GITHUB_STEP_SUMMARY + else + echo "โš ๏ธ No documentation index (docs/index.md or docs/README.md)" >> $GITHUB_STEP_SUMMARY + fi + + readme-completeness: + name: README Completeness Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check README Sections + run: | + set -x + echo "## ๐Ÿ“„ README Completeness Check" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ ! -f "README.md" ]; then + echo "โŒ README.md not found" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + # Required sections + REQUIRED_SECTIONS=("Installation" "Usage" "Contributing" "License") + MISSING=0 + PRESENT=0 + + echo "### Required Sections" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + for section in "${REQUIRED_SECTIONS[@]}"; do + if grep -qi "##.*$section" README.md; then + echo "โœ… $section" >> $GITHUB_STEP_SUMMARY + PRESENT=$((PRESENT + 1)) + else + echo "โŒ $section" >> $GITHUB_STEP_SUMMARY + MISSING=$((MISSING + 1)) + fi + done + + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Completeness**: $PRESENT/${#REQUIRED_SECTIONS[@]} required sections present" >> $GITHUB_STEP_SUMMARY + + if [ "$MISSING" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Action Required**: Add missing sections to README.md" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + # ============================================================================ + # PHASE 3: Future Enhancements + # ============================================================================ + + git-hygiene: + name: Git Repository Hygiene + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + + - name: Check .gitignore + run: | + set -x + echo "### .gitignore Validation" >> $GITHUB_STEP_SUMMARY + + if [ ! -f ".gitignore" ]; then + echo "โš ๏ธ .gitignore file not found" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### โš ๏ธ Warning: .gitignore Not Found" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Status:** .gitignore file is recommended but not required" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation:** Add .gitignore to exclude build artifacts, dependencies, and temporary files" >> $GITHUB_STEP_SUMMARY + echo "" + echo "โš ๏ธ WARNING: .gitignore file not found - Continuing validation" + exit 0 + fi + + # Check for common exclusions + MISSING="" + grep -q "vendor/" .gitignore || MISSING="${MISSING}vendor/ " + grep -q "node_modules/" .gitignore || MISSING="${MISSING}node_modules/ " + + if [ -n "$MISSING" ]; then + echo "โš ๏ธ .gitignore may be missing common exclusions: $MISSING" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… .gitignore appears complete" >> $GITHUB_STEP_SUMMARY + fi + + - name: Check for Large Files + run: | + set -x + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Large File Detection" >> $GITHUB_STEP_SUMMARY + + # Find files larger than 1MB + LARGE_FILES=$(find . -type f -size +1M ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" | head -5) + + if [ -n "$LARGE_FILES" ]; then + echo "โš ๏ธ Large files detected (>1MB):" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "$LARGE_FILES" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "Consider using Git LFS for large binary files" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… No unusually large files detected" >> $GITHUB_STEP_SUMMARY + fi + + script-integrity: + name: Script Integrity Validation + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.x' + + - name: Validate Script Integrity + id: script_check + run: | + set -x + echo "## ๐Ÿ” Script Integrity Validation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ -f "api/.script-registry.json" ]; then + echo "### Critical Scripts" >> $GITHUB_STEP_SUMMARY + php api/maintenance/update_sha_hashes.php \ + --dry-run --verbose | tee /tmp/script-validation.log + + EXIT_CODE=$? + + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + cat /tmp/script-validation.log >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + + if [ "$EXIT_CODE" -eq 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "โœ… All critical scripts validated successfully!" >> $GITHUB_STEP_SUMMARY + exit 0 + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "โŒ Script integrity violations detected" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Review validation report and update registry" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + else + echo "โ„น๏ธ Script registry not found - skipping integrity check" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + # TIER 3 โ€” QUALITY (code quality metrics) + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + line-length-validation: + name: Line Length Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check Line Lengths + run: | + set -x + echo "## ๐Ÿ“ Line Length Validation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Line length standards: + # - General source code: 120 characters (hard limit) + # - YAML workflows: 180 characters (exception for GitHub Actions) + # - Markdown files: No limit (content-focused) + + echo "### Line Length Standards" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| File Type | Soft Limit | Hard Limit |" >> $GITHUB_STEP_SUMMARY + echo "|-----------|------------|------------|" >> $GITHUB_STEP_SUMMARY + echo "| General source code | 80 chars | 120 chars |" >> $GITHUB_STEP_SUMMARY + echo "| YAML workflows | 80 chars | 180 chars |" >> $GITHUB_STEP_SUMMARY + echo "| Markdown files | N/A | No limit |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Check YAML files (using yamllint which is already configured) + echo "### YAML Files (180 char limit)" >> $GITHUB_STEP_SUMMARY + + YAML_VIOLATIONS=0 + if command -v yamllint >/dev/null 2>&1; then + # Install yamllint if not present + : + else + pip install yamllint >/dev/null 2>&1 + fi + + # Run yamllint and count line-length warnings + YAML_OUTPUT=$(yamllint .github/workflows/*.yml 2>&1 | grep "line too long" || true) + if [ -n "$YAML_OUTPUT" ]; then + YAML_VIOLATIONS=$(echo "$YAML_OUTPUT" | wc -l) + echo "โš ๏ธ Found $YAML_VIOLATIONS lines exceeding 180 characters in YAML files" >> $GITHUB_STEP_SUMMARY + echo "
View warnings (informational only)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "$YAML_OUTPUT" | head -20 >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… All YAML files comply with 180 character limit" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # Check source code files (PHP, Python, JavaScript, etc.) for 120 char limit + echo "### Source Code Files (120 char limit)" >> $GITHUB_STEP_SUMMARY + + LONG_LINES=$(find . -type f \ + \( -name "*.php" -o -name "*.py" -o -name "*.js" -o -name "*.ts" \ + -o -name "*.go" -o -name "*.rs" -o -name "*.java" -o -name "*.c" \ + -o -name "*.cpp" -o -name "*.h" -o -name "*.sh" \) \ + ! -path "./vendor/*" \ + ! -path "./node_modules/*" \ + ! -path "./.git/*" \ + ! -path "./build/*" \ + ! -path "./dist/*" \ + -exec awk 'length > 120 { print FILENAME ":" NR ": " length " chars" }' {} \; 2>/dev/null | head -20) + + if [ -n "$LONG_LINES" ]; then + LINE_COUNT=$(echo "$LONG_LINES" | wc -l) + echo "โš ๏ธ Found $LINE_COUNT source code lines exceeding 120 characters" >> $GITHUB_STEP_SUMMARY + echo "
View violations (informational)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "$LONG_LINES" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… All source code files comply with 120 character limit" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # Confirm Markdown files are not checked + echo "### Markdown Files" >> $GITHUB_STEP_SUMMARY + echo "โœ… Markdown files have no line length limit per coding standards" >> $GITHUB_STEP_SUMMARY + echo "Rationale: Content-focused format, URLs, tables, and natural prose flow" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Summary + echo "### Summary" >> $GITHUB_STEP_SUMMARY + echo "This check is **informational only** and does not block merges." >> $GITHUB_STEP_SUMMARY + echo "Line length standards help maintain code readability." >> $GITHUB_STEP_SUMMARY + echo "Exceptions documented in: \`docs/policy/coding-style-guide.md\`" >> $GITHUB_STEP_SUMMARY + + file-naming-standards: + name: File Naming Standards + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check File Naming + run: | + set -x + echo "## ๐Ÿ“ File Naming Standards" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + VIOLATIONS=0 + + # Check PHP files (should be PascalCase for classes) + INVALID_PHP=$(find . -name "*.php" ! -path "./vendor/*" ! -path "./.git/*" ! -regex ".*/[A-Z][a-zA-Z0-9]*\.php" ! -name "index.php" ! -name "functions.php" | wc -l || echo 0) + + # Check config files (should be kebab-case) + INVALID_CONFIG=$(find . -name "*.yml" -o -name "*.yaml" -o -name "*.json" ! -path "./vendor/*" ! -path "./.git/*" ! -path "./node_modules/*" | grep -E "[A-Z_]" | wc -l || echo 0) + + echo "### Naming Violations" >> $GITHUB_STEP_SUMMARY + echo "- **PHP files not PascalCase**: $INVALID_PHP" >> $GITHUB_STEP_SUMMARY + echo "- **Config files not kebab-case**: $INVALID_CONFIG" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + VIOLATIONS=$((INVALID_PHP + INVALID_CONFIG)) + + if [ "$VIOLATIONS" -gt 0 ]; then + echo "โš ๏ธ Found $VIOLATIONS naming convention violation(s)" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Follow naming conventions for consistency" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… File naming conventions followed" >> $GITHUB_STEP_SUMMARY + fi + + insecure-patterns: + name: Insecure Code Pattern Detection + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Scan for Insecure Patterns + run: | + set -x + echo "## ๐Ÿ”’ Insecure Code Pattern Detection" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + VIOLATIONS=0 + + # PHP: SQL injection patterns + if grep -r -n "\\$_\(GET\|POST\|REQUEST\).*mysql_query\|mysqli_query" . --include="*.php" ! -path "./vendor/*" 2>/dev/null > /tmp/sql_inject.txt; then + COUNT=$(wc -l < /tmp/sql_inject.txt) + echo "โš ๏ธ Found $COUNT potential SQL injection pattern(s)" >> $GITHUB_STEP_SUMMARY + VIOLATIONS=$((VIOLATIONS + COUNT)) + fi + + # PHP: eval/exec usage + if grep -r -n "eval\|exec\|system\|passthru\|shell_exec" . --include="*.php" ! -path "./vendor/*" 2>/dev/null > /tmp/exec.txt; then + COUNT=$(wc -l < /tmp/exec.txt) + echo "โš ๏ธ Found $COUNT dangerous function call(s)" >> $GITHUB_STEP_SUMMARY + VIOLATIONS=$((VIOLATIONS + COUNT)) + fi + + # Python: eval usage + if grep -r -n "eval(" . --include="*.py" 2>/dev/null > /tmp/py_eval.txt; then + COUNT=$(wc -l < /tmp/py_eval.txt) + echo "โš ๏ธ Found $COUNT Python eval() usage(s)" >> $GITHUB_STEP_SUMMARY + VIOLATIONS=$((VIOLATIONS + COUNT)) + fi + + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$VIOLATIONS" -gt 0 ]; then + echo "**Total Violations**: $VIOLATIONS" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Review and secure flagged patterns" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… No insecure patterns detected" >> $GITHUB_STEP_SUMMARY + fi + + code-complexity: + name: Code Complexity Analysis + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: '8.1' + + - name: Analyze Complexity + run: | + set -x + echo "## ๐Ÿ“Š Code Complexity Analysis" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + PHP_COUNT=$(find . -name "*.php" ! -path "./vendor/*" ! -path "./.git/*" | wc -l) + + if [ "$PHP_COUNT" -gt 0 ]; then + # Install phploc + wget https://phar.phpunit.de/phploc.phar 2>/dev/null + chmod +x phploc.phar + + echo "### PHP Code Metrics" >> $GITHUB_STEP_SUMMARY + if ./phploc.phar --exclude vendor --exclude .git . 2>&1 | tee /tmp/phploc.txt; then + COMPLEXITY=$(grep "Cyclomatic Complexity" /tmp/phploc.txt | grep "Average" | awk '{print $NF}' || echo "N/A") + echo "**Average Cyclomatic Complexity**: $COMPLEXITY" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$COMPLEXITY" != "N/A" ] && [ $(echo "$COMPLEXITY > 10" | bc -l) -eq 1 ]; then + echo "โš ๏ธ Average complexity exceeds recommended threshold (10)" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Refactor complex functions" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… Code complexity within acceptable limits" >> $GITHUB_STEP_SUMMARY + fi + fi + else + echo "โ„น๏ธ No PHP files found for complexity analysis" >> $GITHUB_STEP_SUMMARY + fi + + code-duplication: + name: Code Duplication Detection + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: '8.1' + + - name: Detect Duplicates + run: | + set -x + echo "## ๐Ÿ” Code Duplication Detection" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Check if PHP files exist + PHP_COUNT=$(find . -name "*.php" ! -path "./vendor/*" ! -path "./.git/*" | wc -l) + + if [ "$PHP_COUNT" -gt 0 ]; then + echo "### PHP Code Duplication" >> $GITHUB_STEP_SUMMARY + + # Install phpcpd + wget https://phar.phpunit.de/phpcpd.phar 2>/dev/null + chmod +x phpcpd.phar + + # Run duplication detection + if ./phpcpd.phar --exclude vendor --exclude .git . 2>&1 | tee /tmp/phpcpd.txt; then + DUPLICATION=$(grep "Found" /tmp/phpcpd.txt | grep -oE "[0-9]+\.[0-9]+%" | head -1 || echo "0.00%") + echo "๐Ÿ“Š **Duplication Rate**: $DUPLICATION" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + DUPLICATION_NUM=$(echo "$DUPLICATION" | sed 's/%//') + if [ $(echo "$DUPLICATION_NUM > 5.0" | bc -l) -eq 1 ]; then + echo "โš ๏ธ Code duplication exceeds 5% threshold" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View duplication details" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + cat /tmp/phpcpd.txt >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… Code duplication within acceptable limits (<5%)" >> $GITHUB_STEP_SUMMARY + fi + else + echo "โœ… No significant code duplication detected" >> $GITHUB_STEP_SUMMARY + fi + else + echo "โ„น๏ธ No PHP files found for duplication analysis" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Note**: This is an informational check to encourage DRY principles." >> $GITHUB_STEP_SUMMARY + + dead-code-detection: + name: Dead Code Detection + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.x' + + - name: Detect Dead Code + run: | + set -x + echo "## ๐Ÿ—‘๏ธ Dead Code Detection" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + PY_COUNT=$(find . -name "*.py" ! -path "./vendor/*" ! -path "./.git/*" ! -path "./venv/*" | wc -l) + + if [ "$PY_COUNT" -gt 0 ]; then + pip install vulture 2>/dev/null + echo "### Python Dead Code" >> $GITHUB_STEP_SUMMARY + + if vulture . --exclude vendor,venv,.git 2>&1 | tee /tmp/vulture.txt; then + DEAD_COUNT=$(wc -l < /tmp/vulture.txt || echo 0) + if [ "$DEAD_COUNT" -gt 0 ]; then + echo "โš ๏ธ Found $DEAD_COUNT potential dead code item(s)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View dead code" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + head -50 /tmp/vulture.txt >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… No dead code detected" >> $GITHUB_STEP_SUMMARY + fi + fi + else + echo "โ„น๏ธ No Python files found for dead code analysis" >> $GITHUB_STEP_SUMMARY + fi + + + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + # TIER 4 โ€” SUPPLEMENTARY (informational) + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + file-size-limits: + name: File Size Limits + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check File Sizes + run: | + set -x + echo "## ๐Ÿ“ฆ File Size Validation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Exempt file types (allowed to be large) + EXEMPT="! -name *.mmdb ! -name *.woff2 ! -name *.woff ! -name *.ttf ! -name *.otf" + + # Find large files (>15MB warning, >20MB critical) + LARGE_FILES=$(find . -type f -size +15M $EXEMPT ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" 2>/dev/null | wc -l) + HUGE_FILES=$(find . -type f -size +20M $EXEMPT ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" 2>/dev/null | wc -l) + + echo "### Size Thresholds" >> $GITHUB_STEP_SUMMARY + echo "- **Warning**: Files >15MB" >> $GITHUB_STEP_SUMMARY + echo "- **Critical**: Files >20MB" >> $GITHUB_STEP_SUMMARY + echo "- **Exempt**: .mmdb, .woff2, .woff, .ttf, .otf" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$HUGE_FILES" -gt 0 ]; then + echo "โŒ **Critical**: Found $HUGE_FILES file(s) exceeding 20MB" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View files >20MB" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + find . -type f -size +20M $EXEMPT ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" -exec ls -lh {} + 2>/dev/null | awk '{print $5, $9}' >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Action Required**: Remove or optimize files >20MB" >> $GITHUB_STEP_SUMMARY + exit 1 + elif [ "$LARGE_FILES" -gt 0 ]; then + echo "โš ๏ธ **Warning**: Found $LARGE_FILES file(s) between 15MB and 20MB" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View files >15MB" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + find . -type f -size +15M $EXEMPT ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" -exec ls -lh {} + 2>/dev/null | awk '{print $5, $9}' >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Consider optimizing large files" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… All files within acceptable size limits" >> $GITHUB_STEP_SUMMARY + fi + + binary-file-detection: + name: Binary File Detection + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Detect Binary Files + run: | + set -x + echo "## ๐Ÿ” Binary File Detection" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Find binary files excluding allowed types + BINARIES=$(find . -type f ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" \ + ! -name "*.png" ! -name "*.jpg" ! -name "*.jpeg" ! -name "*.gif" ! -name "*.svg" ! -name "*.ico" \ + ! -name "*.woff" ! -name "*.woff2" ! -name "*.ttf" ! -name "*.eot" \ + -exec file {} \; | grep -v "text" | grep -v "empty" | wc -l || echo 0) + + if [ "$BINARIES" -gt 0 ]; then + echo "โš ๏ธ Found $BINARIES non-image binary file(s)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View binary files" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + find . -type f ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" \ + ! -name "*.png" ! -name "*.jpg" ! -name "*.jpeg" ! -name "*.gif" ! -name "*.svg" ! -name "*.ico" \ + ! -name "*.woff" ! -name "*.woff2" ! -name "*.ttf" ! -name "*.eot" \ + -exec file {} \; | grep -v "text" | grep -v "empty" | cut -d: -f1 >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Source control should primarily contain text files" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… No unexpected binary files detected" >> $GITHUB_STEP_SUMMARY + fi + + # ============================================================================ + # PHASE 4: Nice to Have Checks + # ============================================================================ + + todo-fixme-tracking: + name: TODO/FIXME Tracking + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Track Technical Debt + run: | + set -x + echo "## ๐Ÿ“ TODO/FIXME Tracking" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Tracking technical debt markers in source code." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Search for technical debt markers + PATTERNS="TODO|FIXME|HACK|XXX" + EXTENSIONS="*.php *.py *.js *.ts *.go *.rs *.java *.c *.cpp *.h *.hpp *.sh" + + echo "### Technical Debt Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + TOTAL_COUNT=0 + for ext in $EXTENSIONS; do + COUNT=$(find . -type f -name "$ext" ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" -exec grep -n -E "($PATTERNS)" {} + 2>/dev/null | wc -l || echo 0) + TOTAL_COUNT=$((TOTAL_COUNT + COUNT)) + done + + if [ "$TOTAL_COUNT" -gt 0 ]; then + echo "โš ๏ธ Found **$TOTAL_COUNT** technical debt item(s)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View technical debt items" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + for ext in $EXTENSIONS; do + find . -type f -name "$ext" ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" -exec grep -n -H -E "($PATTERNS)" {} + 2>/dev/null | head -100 || true + done >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… No technical debt markers found" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Note**: This is an informational check. Technical debt items don't block compliance." >> $GITHUB_STEP_SUMMARY + + dependency-vulnerabilities: + name: Dependency Vulnerability Scanning + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: '8.1' + + - name: Setup Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.x' + + - name: Scan Dependencies + run: | + set -x + echo "## ๐Ÿ›ก๏ธ Dependency Vulnerability Scanning" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + VULNERABILITIES=0 + + # PHP Dependencies + if [ -f "composer.json" ]; then + echo "### PHP Dependencies (composer)" >> $GITHUB_STEP_SUMMARY + if composer audit --no-dev 2>&1 | tee /tmp/php_audit.txt; then + echo "โœ… No PHP vulnerabilities detected" >> $GITHUB_STEP_SUMMARY + else + VULN_COUNT=$(grep -c "vulnerability" /tmp/php_audit.txt || echo 0) + echo "โš ๏ธ Found $VULN_COUNT PHP vulnerability/vulnerabilities" >> $GITHUB_STEP_SUMMARY + VULNERABILITIES=$((VULNERABILITIES + VULN_COUNT)) + fi + echo "" >> $GITHUB_STEP_SUMMARY + fi + + # Python Dependencies + if [ -f "requirements.txt" ]; then + echo "### Python Dependencies" >> $GITHUB_STEP_SUMMARY + pip install pip-audit 2>&1 > /dev/null + if pip-audit -r requirements.txt 2>&1 | tee /tmp/py_audit.txt; then + echo "โœ… No Python vulnerabilities detected" >> $GITHUB_STEP_SUMMARY + else + VULN_COUNT=$(grep -c "vulnerability" /tmp/py_audit.txt || echo 0) + echo "โš ๏ธ Found $VULN_COUNT Python vulnerability/vulnerabilities" >> $GITHUB_STEP_SUMMARY + VULNERABILITIES=$((VULNERABILITIES + VULN_COUNT)) + fi + echo "" >> $GITHUB_STEP_SUMMARY + fi + + # NPM Dependencies + if [ -f "package.json" ]; then + echo "### NPM Dependencies" >> $GITHUB_STEP_SUMMARY + if npm audit --production 2>&1 | tee /tmp/npm_audit.txt; then + echo "โœ… No NPM vulnerabilities detected" >> $GITHUB_STEP_SUMMARY + else + VULN_COUNT=$(grep -c "vulnerability" /tmp/npm_audit.txt || echo 0) + echo "โš ๏ธ Found $VULN_COUNT NPM vulnerability/vulnerabilities" >> $GITHUB_STEP_SUMMARY + VULNERABILITIES=$((VULNERABILITIES + VULN_COUNT)) + fi + echo "" >> $GITHUB_STEP_SUMMARY + fi + + if [ "$VULNERABILITIES" -gt 0 ]; then + echo "**Total Vulnerabilities**: $VULNERABILITIES" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Action Required**: Update vulnerable dependencies" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "โœ… No dependency vulnerabilities detected" >> $GITHUB_STEP_SUMMARY + fi + + unused-dependencies: + name: Unused Dependencies Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: '8.1' + + - name: Check Unused Dependencies + run: | + set -x + echo "## ๐Ÿ“ฆ Unused Dependencies Check" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ -f "composer.json" ]; then + echo "### PHP Dependencies" >> $GITHUB_STEP_SUMMARY + + # Install composer-unused + composer global require icanhazstring/composer-unused 2>/dev/null || true + + if composer global exec composer-unused 2>&1 | tee /tmp/unused.txt; then + UNUSED_COUNT=$(grep "unused" /tmp/unused.txt | wc -l || echo 0) + if [ "$UNUSED_COUNT" -gt 0 ]; then + echo "โš ๏ธ Found $UNUSED_COUNT unused dependency/dependencies" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View unused dependencies" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + cat /tmp/unused.txt >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… No unused dependencies detected" >> $GITHUB_STEP_SUMMARY + fi + else + echo "โœ… All dependencies appear to be in use" >> $GITHUB_STEP_SUMMARY + fi + else + echo "โ„น๏ธ No composer.json found" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Remove unused dependencies to reduce attack surface" >> $GITHUB_STEP_SUMMARY + + broken-link-detection: + name: Broken Link Detection + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check Internal Links + run: | + set -x + echo "## ๐Ÿ”— Broken Link Detection" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Checking internal links in markdown files." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + BROKEN_LINKS=0 + CHECKED_LINKS=0 + + # Find all markdown files + MD_FILES=$(find . -name "*.md" ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*") + + for file in $MD_FILES; do + # Extract markdown links [text](path) + while IFS= read -r line; do + # Extract path from [text](path) + link=$(echo "$line" | sed -n 's/.*\](\([^)]*\)).*/\1/p') + + # Skip external links (http/https) + if echo "$link" | grep -qE "^https?://"; then + continue + fi + + # Skip anchors only + if echo "$link" | grep -qE "^#"; then + continue + fi + + CHECKED_LINKS=$((CHECKED_LINKS + 1)) + + # Get directory of the markdown file + basedir=$(dirname "$file") + + # Resolve relative path + if [ -n "$link" ]; then + # Remove anchor if present + clean_link=$(echo "$link" | sed 's/#.*//') + + # Check if file exists + if [ ! -e "$basedir/$clean_link" ] && [ ! -e "$clean_link" ]; then + echo "Broken link in $file: $link" >> /tmp/broken_links.txt + BROKEN_LINKS=$((BROKEN_LINKS + 1)) + fi + fi + done < <(grep -o '\[.*\](.*)' "$file" 2>/dev/null || true) + done + + echo "### Link Validation Results" >> $GITHUB_STEP_SUMMARY + echo "- **Links Checked**: $CHECKED_LINKS" >> $GITHUB_STEP_SUMMARY + echo "- **Broken Links**: $BROKEN_LINKS" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$BROKEN_LINKS" -gt 0 ]; then + echo "โš ๏ธ Found $BROKEN_LINKS broken internal link(s)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View broken links" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + cat /tmp/broken_links.txt 2>/dev/null >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Fix or remove broken links to maintain documentation quality" >> $GITHUB_STEP_SUMMARY + else + if [ "$CHECKED_LINKS" -gt 0 ]; then + echo "โœ… All internal links are valid" >> $GITHUB_STEP_SUMMARY + else + echo "โ„น๏ธ No internal links found to check" >> $GITHUB_STEP_SUMMARY + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Note**: This check validates internal file references only. External URLs are not validated." >> $GITHUB_STEP_SUMMARY + + # ============================================================================ + # PHASE 2: Medium Priority Checks + # ============================================================================ + + api-documentation: + name: API Documentation Coverage + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check Documentation + run: | + set -x + echo "## ๐Ÿ“š API Documentation Coverage" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Count public functions/classes + PUBLIC_METHODS=$(grep -r "public function" . --include="*.php" ! -path "./vendor/*" | wc -l || echo 0) + DOCUMENTED=$(grep -B5 -r "public function" . --include="*.php" ! -path "./vendor/*" | grep -c "/\*\*" || echo 0) + + if [ "$PUBLIC_METHODS" -gt 0 ]; then + COVERAGE=$((DOCUMENTED * 100 / PUBLIC_METHODS)) + echo "**Documentation Coverage**: $COVERAGE% ($DOCUMENTED/$PUBLIC_METHODS)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$COVERAGE" -lt 80 ]; then + echo "โš ๏ธ Documentation coverage below 80% threshold" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Add PHPDoc blocks to public methods" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… Good documentation coverage" >> $GITHUB_STEP_SUMMARY + fi + else + echo "โ„น๏ธ No public methods found for documentation check" >> $GITHUB_STEP_SUMMARY + fi + + accessibility-check: + name: Accessibility Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check Accessibility + run: | + set -x + echo "## โ™ฟ Accessibility Check" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + HTML_COUNT=$(find . -name "*.html" ! -path "./vendor/*" ! -path "./.git/*" ! -path "./node_modules/*" | wc -l || echo 0) + MD_IMG_COUNT=$(find . -name "*.md" ! -path "./vendor/*" ! -path "./.git/*" -exec grep -l "!\[" {} + 2>/dev/null | wc -l || echo 0) + + if [ "$HTML_COUNT" -gt 0 ] || [ "$MD_IMG_COUNT" -gt 0 ]; then + # Check for images without alt text + MISSING_ALT=0 + + if [ "$HTML_COUNT" -gt 0 ]; then + MISSING_ALT=$(grep -r "> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$MISSING_ALT" -gt 0 ]; then + echo "โš ๏ธ Found images without alt text" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Add descriptive alt text for accessibility" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… All images have alt text" >> $GITHUB_STEP_SUMMARY + fi + else + echo "โ„น๏ธ No HTML files found for accessibility check" >> $GITHUB_STEP_SUMMARY + fi + + performance-metrics: + name: Performance Metrics + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check Performance Metrics + run: | + set -x + echo "## โšก Performance Metrics" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Check if JavaScript bundles exist + if [ -f "package.json" ]; then + echo "### Bundle Analysis" >> $GITHUB_STEP_SUMMARY + + # Check for common bundle files + BUNDLE_SIZE=0 + if [ -d "dist" ]; then + BUNDLE_SIZE=$(du -sb dist/ 2>/dev/null | cut -f1 || echo 0) + elif [ -d "build" ]; then + BUNDLE_SIZE=$(du -sb build/ 2>/dev/null | cut -f1 || echo 0) + fi + + if [ "$BUNDLE_SIZE" -gt 0 ]; then + BUNDLE_MB=$((BUNDLE_SIZE / 1024 / 1024)) + echo "**Bundle Size**: ${BUNDLE_MB}MB" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$BUNDLE_MB" -gt 5 ]; then + echo "โš ๏ธ Bundle size exceeds 5MB threshold" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Optimize bundle size" >> $GITHUB_STEP_SUMMARY + else + echo "โœ… Bundle size within acceptable limits" >> $GITHUB_STEP_SUMMARY + fi + else + echo "โ„น๏ธ No build artifacts found" >> $GITHUB_STEP_SUMMARY + fi + else + echo "โ„น๏ธ Not a JavaScript project" >> $GITHUB_STEP_SUMMARY + fi + + enterprise-readiness: + name: Enterprise Readiness Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Set up PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: '8.1' + extensions: json, mbstring + tools: composer + coverage: none + + - name: Install API Package + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' + run: | + if [ -f "composer.json" ]; then + composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader + else + echo "No composer.json โ€” pulling MokoStandards tools" + if [ ! -d "/tmp/mokostandards" ]; then + git clone --depth 1 --branch version/04 --quiet \ + "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ + /tmp/mokostandards 2>/dev/null || true + if [ -f "/tmp/mokostandards/composer.json" ]; then + cd /tmp/mokostandards && composer install --no-dev --no-interaction --quiet 2>/dev/null || true + cd - + fi + fi + fi + + - name: Check Enterprise Readiness + id: enterprise_check + run: | + echo "" >> $GITHUB_STEP_SUMMARY + + SCRIPT="" + if [ -f "api/validate/check_enterprise_readiness.php" ]; then + SCRIPT="api/validate/check_enterprise_readiness.php" + elif [ -f "/tmp/mokostandards/api/validate/check_enterprise_readiness.php" ]; then + SCRIPT="/tmp/mokostandards/api/validate/check_enterprise_readiness.php" + fi + + if [ -n "$SCRIPT" ]; then + php "$SCRIPT" --verbose | tee /tmp/enterprise-check.log + EXIT_CODE=$? + + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + cat /tmp/enterprise-check.log >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + + if [ "$EXIT_CODE" -eq 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "โœ… Repository meets enterprise readiness criteria!" >> $GITHUB_STEP_SUMMARY + exit 0 + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "โš ๏ธ Enterprise readiness issues detected" >> $GITHUB_STEP_SUMMARY + echo "**Note:** This is informational - review recommendations to improve" >> $GITHUB_STEP_SUMMARY + exit 0 # Non-blocking + fi + else + echo "โ„น๏ธ Enterprise readiness check script not found - skipping" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + repository-health: + name: Repository Health Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Set up PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: '8.1' + extensions: json, mbstring + tools: composer + coverage: none + + - name: Install API Package + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' + run: | + if [ -f "composer.json" ]; then + composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader + else + echo "No composer.json โ€” pulling MokoStandards tools" + if [ ! -d "/tmp/mokostandards" ]; then + git clone --depth 1 --branch version/04 --quiet \ + "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ + /tmp/mokostandards 2>/dev/null || true + if [ -f "/tmp/mokostandards/composer.json" ]; then + cd /tmp/mokostandards && composer install --no-dev --no-interaction --quiet 2>/dev/null || true + cd - + fi + fi + fi + + - name: Check Repository Health + id: health_check + run: | + echo "" >> $GITHUB_STEP_SUMMARY + + SCRIPT="" + if [ -f "api/validate/check_repo_health.php" ]; then + SCRIPT="api/validate/check_repo_health.php" + elif [ -f "/tmp/mokostandards/api/validate/check_repo_health.php" ]; then + SCRIPT="/tmp/mokostandards/api/validate/check_repo_health.php" + fi + + if [ -n "$SCRIPT" ]; then + php "$SCRIPT" --verbose | tee /tmp/health-check.log + EXIT_CODE=$? + + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + cat /tmp/health-check.log >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + + if [ "$EXIT_CODE" -eq 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "โœ… Repository health check passed!" >> $GITHUB_STEP_SUMMARY + exit 0 + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "โš ๏ธ Repository health issues detected" >> $GITHUB_STEP_SUMMARY + echo "**Note:** This is informational - review recommendations to improve" >> $GITHUB_STEP_SUMMARY + exit 0 # Non-blocking + fi + else + echo "โ„น๏ธ Repository health check script not found - skipping" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + terraform-validation: + name: Terraform Configuration Validation + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup Terraform + uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0 + with: + terraform_version: "1.0" + + - name: Validate Terraform Files + run: | + set -x + echo "## ๐Ÿ—๏ธ Terraform Configuration Validation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Check if terraform files exist + TF_COUNT=$(find . -name "*.tf" -type f | wc -l || echo 0) + + if [ "$TF_COUNT" -eq 0 ]; then + echo "โ„น๏ธ No Terraform files found in repository" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + echo "**Terraform Files Found**: $TF_COUNT" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Validation Results + VALIDATION_PASSED=true + WARNINGS=0 + ERRORS=0 + + # 1. Check .github/config.tf location (not root override files) + echo "### Override Configuration Check" >> $GITHUB_STEP_SUMMARY + LEGACY_OVERRIDES=$(find . -maxdepth 1 -name "*override*.tf" -o -name "MokoStandards.override.tf" 2>/dev/null | wc -l || echo 0) + if [ "$LEGACY_OVERRIDES" -gt 0 ]; then + echo "โš ๏ธ Found legacy override files in root directory" >> $GITHUB_STEP_SUMMARY + echo "**Expected Location**: .github/config.tf" >> $GITHUB_STEP_SUMMARY + echo "**Legacy files found**: $LEGACY_OVERRIDES" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + else + if [ -f ".github/config.tf" ]; then + echo "โœ… Override configuration in correct location (.github/config.tf)" >> $GITHUB_STEP_SUMMARY + else + echo "โ„น๏ธ No override configuration found" >> $GITHUB_STEP_SUMMARY + fi + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # 2. Terraform Syntax Validation + echo "### Terraform Syntax Validation" >> $GITHUB_STEP_SUMMARY + SYNTAX_ERRORS=0 + + # Find all directories with terraform files + for dir in $(find . -name "*.tf" -type f -exec dirname {} \; | sort -u); do + cd "$dir" || continue + echo "Validating: $dir" >> $GITHUB_STEP_SUMMARY + + # Initialize without backend + terraform init -backend=false > /dev/null 2>&1 || true + + # Validate + if terraform validate -no-color > /tmp/tf_validate.txt 2>&1; then + echo " โœ… Syntax valid" >> $GITHUB_STEP_SUMMARY + else + echo " โŒ Syntax errors found" >> $GITHUB_STEP_SUMMARY + cat /tmp/tf_validate.txt >> $GITHUB_STEP_SUMMARY + SYNTAX_ERRORS=$((SYNTAX_ERRORS + 1)) + VALIDATION_PASSED=false + fi + cd - > /dev/null + done + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$SYNTAX_ERRORS" -eq 0 ]; then + echo "โœ… All Terraform files have valid syntax" >> $GITHUB_STEP_SUMMARY + else + echo "โŒ Found $SYNTAX_ERRORS directories with syntax errors" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + SYNTAX_ERRORS)) + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # 3. Terraform Formatting Check + echo "### Terraform Formatting Check" >> $GITHUB_STEP_SUMMARY + FORMAT_ISSUES=0 + + for tf_file in $(find . -name "*.tf" -type f); do + if ! terraform fmt -check=true -no-color "$tf_file" > /dev/null 2>&1; then + FORMAT_ISSUES=$((FORMAT_ISSUES + 1)) + fi + done + + if [ "$FORMAT_ISSUES" -eq 0 ]; then + echo "โœ… All Terraform files properly formatted" >> $GITHUB_STEP_SUMMARY + else + echo "โš ๏ธ Found $FORMAT_ISSUES files with formatting issues" >> $GITHUB_STEP_SUMMARY + echo "**Fix**: Run \`terraform fmt -recursive\`" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # 4. Check for file_metadata blocks + echo "### File Metadata Validation" >> $GITHUB_STEP_SUMMARY + MISSING_METADATA=0 + + for tf_file in $(find . -name "*.tf" -type f); do + if ! grep -q "file_metadata" "$tf_file"; then + MISSING_METADATA=$((MISSING_METADATA + 1)) + fi + done + + if [ "$MISSING_METADATA" -eq 0 ]; then + echo "โœ… All Terraform files contain file_metadata block" >> $GITHUB_STEP_SUMMARY + else + echo "โš ๏ธ Found $MISSING_METADATA files missing file_metadata block" >> $GITHUB_STEP_SUMMARY + echo "**Reference**: docs/policy/terraform-file-standards.md" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # 5. Version Consistency Check + echo "### Version Consistency Check" >> $GITHUB_STEP_SUMMARY + VERSION_MISMATCHES=0 + EXPECTED_VERSION="04.00.04" + + for tf_file in $(find . -name "*.tf" -type f); do + if grep -q "version.*=" "$tf_file"; then + if ! grep -q "version.*=.*\"$EXPECTED_VERSION\"" "$tf_file"; then + VERSION_MISMATCHES=$((VERSION_MISMATCHES + 1)) + fi + fi + done + + if [ "$VERSION_MISMATCHES" -eq 0 ]; then + echo "โœ… All Terraform file versions match $EXPECTED_VERSION" >> $GITHUB_STEP_SUMMARY + else + echo "โš ๏ธ Found $VERSION_MISMATCHES files with version mismatches" >> $GITHUB_STEP_SUMMARY + echo "**Expected Version**: $EXPECTED_VERSION" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # 6. Copyright Header Check + echo "### Copyright Header Check" >> $GITHUB_STEP_SUMMARY + MISSING_COPYRIGHT=0 + + for tf_file in $(find . -name "*.tf" -type f); do + if ! grep -q "Copyright (C)" "$tf_file"; then + MISSING_COPYRIGHT=$((MISSING_COPYRIGHT + 1)) + fi + done + + if [ "$MISSING_COPYRIGHT" -eq 0 ]; then + echo "โœ… All Terraform files have copyright headers" >> $GITHUB_STEP_SUMMARY + else + echo "โš ๏ธ Found $MISSING_COPYRIGHT files missing copyright headers" >> $GITHUB_STEP_SUMMARY + echo "**Reference**: docs/policy/terraform-file-standards.md" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # Summary + echo "---" >> $GITHUB_STEP_SUMMARY + echo "### Validation Summary" >> $GITHUB_STEP_SUMMARY + echo "**Total Files**: $TF_COUNT" >> $GITHUB_STEP_SUMMARY + echo "**Errors**: $ERRORS" >> $GITHUB_STEP_SUMMARY + echo "**Warnings**: $WARNINGS" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$VALIDATION_PASSED" = true ] && [ "$ERRORS" -eq 0 ]; then + echo "โœ… **Terraform Validation: PASSED**" >> $GITHUB_STEP_SUMMARY + exit 0 + elif [ "$ERRORS" -gt 0 ]; then + echo "โŒ **Terraform Validation: FAILED**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Note**: This is an informational check and does not block merges" >> $GITHUB_STEP_SUMMARY + exit 0 # Informational only + else + echo "โš ๏ธ **Terraform Validation: PASSED WITH WARNINGS**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Note**: This is an informational check and does not block merges" >> $GITHUB_STEP_SUMMARY + exit 0 # Informational only + fi + + summary: + name: Compliance Summary + runs-on: ubuntu-latest + needs: [ + repository-structure, documentation-quality, coding-standards, line-length-validation, license-compliance, git-hygiene, workflow-validation, version-consistency, script-integrity, enterprise-readiness, repository-health, + todo-fixme-tracking, file-size-limits, secret-scanning, broken-link-detection, + dependency-vulnerabilities, code-duplication, unused-dependencies, readme-completeness, + code-complexity, api-documentation, insecure-patterns, binary-file-detection, + dead-code-detection, file-naming-standards, accessibility-check, performance-metrics, terraform-validation + ] + if: always() + + steps: + - name: Generate Compliance Report + run: | + set -x + echo "# ๐Ÿ“Š MokoStandards Compliance Report" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Calculate overall status + REPO_STATUS="${{ needs.repository-structure.result }}" + DOCS_STATUS="${{ needs.documentation-quality.result }}" + CODE_STATUS="${{ needs.coding-standards.result }}" + LINE_LENGTH_STATUS="${{ needs.line-length-validation.result }}" + LICENSE_STATUS="${{ needs.license-compliance.result }}" + GIT_STATUS="${{ needs.git-hygiene.result }}" + WORKFLOW_STATUS="${{ needs.workflow-validation.result }}" + VERSION_STATUS="${{ needs.version-consistency.result }}" + SCRIPT_STATUS="${{ needs.script-integrity.result }}" + ENTERPRISE_STATUS="${{ needs.enterprise-readiness.result }}" + HEALTH_STATUS="${{ needs.repository-health.result }}" + TERRAFORM_STATUS="${{ needs.terraform-validation.result }}" + + PASSED=0 + FAILED=0 + WARNINGS=0 + TOTAL=28 + + # Critical checks (must pass) + [ "$REPO_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) + [ "$DOCS_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) + [ "$CODE_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) + [ "$LICENSE_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) + [ "$GIT_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) + [ "$WORKFLOW_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) + [ "$VERSION_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) + [ "$SCRIPT_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) + + # Informational checks (don't fail build) + if [ "$ENTERPRISE_STATUS" = "success" ]; then + PASSED=$((PASSED + 1)) + else + WARNINGS=$((WARNINGS + 1)) + fi + + if [ "$HEALTH_STATUS" = "success" ]; then + PASSED=$((PASSED + 1)) + else + WARNINGS=$((WARNINGS + 1)) + fi + + if [ "$TERRAFORM_STATUS" = "success" ]; then + PASSED=$((PASSED + 1)) + else + WARNINGS=$((WARNINGS + 1)) + fi + + # Adjust total to only count critical checks for compliance percentage + CRITICAL_TOTAL=8 + CRITICAL_PASSED=$((PASSED - WARNINGS)) + COMPLIANCE_PERCENT=$((CRITICAL_PASSED * 100 / CRITICAL_TOTAL)) + + # Overall status badge + if [ "$COMPLIANCE_PERCENT" -eq 100 ]; then + echo "## โœ… Overall Status: **COMPLIANT** ($COMPLIANCE_PERCENT%)" >> $GITHUB_STEP_SUMMARY + elif [ "$COMPLIANCE_PERCENT" -ge 80 ]; then + echo "## โš ๏ธ Overall Status: **MOSTLY COMPLIANT** ($COMPLIANCE_PERCENT%)" >> $GITHUB_STEP_SUMMARY + elif [ "$COMPLIANCE_PERCENT" -ge 50 ]; then + echo "## โš ๏ธ Overall Status: **PARTIALLY COMPLIANT** ($COMPLIANCE_PERCENT%)" >> $GITHUB_STEP_SUMMARY + else + echo "## โŒ Overall Status: **NON-COMPLIANT** ($COMPLIANCE_PERCENT%)" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Critical Checks:** $CRITICAL_PASSED/$CRITICAL_TOTAL passed" >> $GITHUB_STEP_SUMMARY + echo "**Total Checks:** $PASSED/$TOTAL passed" >> $GITHUB_STEP_SUMMARY + if [ "$WARNINGS" -gt 0 ]; then + echo "**Informational:** $WARNINGS warning(s)" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # Progress bar + FILLED=$((COMPLIANCE_PERCENT / 5)) + EMPTY=$((20 - FILLED)) + BAR="" + for i in $(seq 1 $FILLED); do BAR="${BAR}โ–ˆ"; done + for i in $(seq 1 $EMPTY); do BAR="${BAR}โ–‘"; done + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "$BAR $COMPLIANCE_PERCENT%" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Detailed breakdown + echo "## Validation Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Area | Status | Result | Priority |" >> $GITHUB_STEP_SUMMARY + echo "|------|--------|--------|----------|" >> $GITHUB_STEP_SUMMARY + + # Repository Structure + if [ "$REPO_STATUS" = "success" ]; then + echo "| ๐Ÿ“ Repository Structure | โœ… Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY + else + echo "| ๐Ÿ“ Repository Structure | โŒ Fail | **Action Required** | ๐Ÿ”ด Critical |" >> $GITHUB_STEP_SUMMARY + fi + + # Documentation Quality + if [ "$DOCS_STATUS" = "success" ]; then + echo "| ๐Ÿ“š Documentation Quality | โœ… Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY + else + echo "| ๐Ÿ“š Documentation Quality | โŒ Fail | **Action Required** | ๐Ÿ”ด Critical |" >> $GITHUB_STEP_SUMMARY + fi + + # Coding Standards + if [ "$CODE_STATUS" = "success" ]; then + echo "| ๐Ÿ’ป Coding Standards | โœ… Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY + else + echo "| ๐Ÿ’ป Coding Standards | โš ๏ธ Warning | Review Recommended | ๐ŸŸก Medium |" >> $GITHUB_STEP_SUMMARY + fi + + # License Compliance + if [ "$LICENSE_STATUS" = "success" ]; then + echo "| โš–๏ธ License Compliance | โœ… Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY + else + echo "| โš–๏ธ License Compliance | โŒ Fail | **Action Required** | ๐Ÿ”ด Critical |" >> $GITHUB_STEP_SUMMARY + fi + + # Git Hygiene + if [ "$GIT_STATUS" = "success" ]; then + echo "| ๐Ÿงน Git Repository Hygiene | โœ… Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY + else + echo "| ๐Ÿงน Git Repository Hygiene | โš ๏ธ Warning | Review Recommended | ๐ŸŸก Medium |" >> $GITHUB_STEP_SUMMARY + fi + + # Workflow Configuration + if [ "$WORKFLOW_STATUS" = "success" ]; then + echo "| โš™๏ธ Workflow Configuration | โœ… Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY + else + echo "| โš™๏ธ Workflow Configuration | โš ๏ธ Warning | Review Recommended | ๐ŸŸก Medium |" >> $GITHUB_STEP_SUMMARY + fi + + # Version Consistency + if [ "$VERSION_STATUS" = "success" ]; then + echo "| ๐Ÿ”ข Version Consistency | โœ… Pass | All versions match | - |" >> $GITHUB_STEP_SUMMARY + else + echo "| ๐Ÿ”ข Version Consistency | โŒ Fail | **Action Required** | ๐Ÿ”ด Critical |" >> $GITHUB_STEP_SUMMARY + fi + + # Script Integrity + if [ "$SCRIPT_STATUS" = "success" ]; then + echo "| ๐Ÿ” Script Integrity | โœ… Pass | SHA hashes validated | - |" >> $GITHUB_STEP_SUMMARY + else + echo "| ๐Ÿ” Script Integrity | โŒ Fail | **Action Required** | ๐Ÿ”ด Critical |" >> $GITHUB_STEP_SUMMARY + fi + + # Enterprise Readiness (Informational) + if [ "$ENTERPRISE_STATUS" = "success" ]; then + echo "| ๐Ÿข Enterprise Readiness | โœ… Pass | Ready for enterprise | โ„น๏ธ Info |" >> $GITHUB_STEP_SUMMARY + else + echo "| ๐Ÿข Enterprise Readiness | โ„น๏ธ Info | Review suggestions | โ„น๏ธ Info |" >> $GITHUB_STEP_SUMMARY + fi + + # Repository Health (Informational) + if [ "$HEALTH_STATUS" = "success" ]; then + echo "| ๐Ÿฅ Repository Health | โœ… Pass | Health check passed | โ„น๏ธ Info |" >> $GITHUB_STEP_SUMMARY + else + echo "| ๐Ÿฅ Repository Health | โ„น๏ธ Info | Review recommendations | โ„น๏ธ Info |" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + + # Action items summary + if [ "$FAILED" -gt 0 ]; then + echo "## โšก Action Items" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**$FAILED validation area(s) require attention:**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + [ "$REPO_STATUS" != "success" ] && echo "- ๐Ÿ”ด **Critical:** Fix repository structure issues" >> $GITHUB_STEP_SUMMARY + [ "$DOCS_STATUS" != "success" ] && echo "- ๐Ÿ”ด **Critical:** Improve documentation quality" >> $GITHUB_STEP_SUMMARY + [ "$LICENSE_STATUS" != "success" ] && echo "- ๐Ÿ”ด **Critical:** Resolve license compliance issues" >> $GITHUB_STEP_SUMMARY + [ "$CODE_STATUS" != "success" ] && echo "- ๐ŸŸก **Medium:** Review coding standards violations" >> $GITHUB_STEP_SUMMARY + [ "$GIT_STATUS" != "success" ] && echo "- ๐ŸŸก **Medium:** Address git repository hygiene items" >> $GITHUB_STEP_SUMMARY + [ "$WORKFLOW_STATUS" != "success" ] && echo "- ๐ŸŸก **Medium:** Review workflow configuration" >> $GITHUB_STEP_SUMMARY + + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Next Steps:**" >> $GITHUB_STEP_SUMMARY + echo "1. Review detailed results in individual job outputs above" >> $GITHUB_STEP_SUMMARY + echo "2. Follow remediation steps provided for each failure" >> $GITHUB_STEP_SUMMARY + echo "3. Re-run this workflow after making corrections" >> $GITHUB_STEP_SUMMARY + echo "4. Reach 100% compliance before merging" >> $GITHUB_STEP_SUMMARY + else + echo "## ๐ŸŽ‰ Excellent!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Your repository is **fully compliant** with MokoStandards!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Achievements:**" >> $GITHUB_STEP_SUMMARY + echo "- โœ… All required directories and files present" >> $GITHUB_STEP_SUMMARY + echo "- โœ… Documentation meets quality standards" >> $GITHUB_STEP_SUMMARY + echo "- โœ… Coding standards followed" >> $GITHUB_STEP_SUMMARY + echo "- โœ… License compliance verified" >> $GITHUB_STEP_SUMMARY + echo "- โœ… Git repository well-maintained" >> $GITHUB_STEP_SUMMARY + echo "- โœ… Workflows properly configured" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "---" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "๐Ÿ“š **Resources:**" >> $GITHUB_STEP_SUMMARY + echo "- [MokoStandards Documentation](https://github.com/mokoconsulting-tech/MokoStandards)" >> $GITHUB_STEP_SUMMARY + echo "- [Repository Structure Guide](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/docs/policy/core-structure.md)" >> $GITHUB_STEP_SUMMARY + echo "- [Documentation Standards](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/docs/policy/document-formatting.md)" >> $GITHUB_STEP_SUMMARY + echo "- [Coding Standards](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/docs/policy/coding-style-guide.md)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "_Generated by MokoStandards Compliance Workflow v${WORKFLOW_VERSION}_" >> $GITHUB_STEP_SUMMARY + + # Create tracking issue for non-compliance if on push + if [ "$COMPLIANCE_PERCENT" -lt 100 ] && [ "${{ github.event_name }}" = "push" ]; then + echo "Creating tracking issue for standards violations..." + fi + + # Exit with error if not fully compliant + if [ "$COMPLIANCE_PERCENT" -lt 100 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "### โŒ Standards Compliance Failed" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Overall Compliance:** $COMPLIANCE_PERCENT%" >> $GITHUB_STEP_SUMMARY + echo "**Status:** Repository does not meet 100% compliance requirement" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Review and fix all validation failures above" >> $GITHUB_STEP_SUMMARY + echo "" + echo "โŒ ERROR: Standards compliance at $COMPLIANCE_PERCENT% - 100% required" + exit 1 + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "### โœ… Full Standards Compliance Achieved" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Overall Compliance:** 100%" >> $GITHUB_STEP_SUMMARY + echo "**Status:** Repository meets all MokoStandards requirements" >> $GITHUB_STEP_SUMMARY + echo "" + echo "โœ… SUCCESS: Repository is fully MokoStandards compliant" + + - name: Create or reopen tracking issue for standards violations + if: failure() + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + REPO="${{ github.repository }}" + RUN_URL="${{ github.server_url }}/${REPO}/actions/runs/${{ github.run_id }}" + DATE=$(date -u '+%Y-%m-%d') + SHA="${{ github.sha }}" + ACTOR="${{ github.actor }}" + BRANCH="${{ github.ref_name }}" + + # Collect failed checks + FAILED="" + [ "${{ needs.repository-structure.result }}" != "success" ] && FAILED="${FAILED}\n- Repository Structure" + [ "${{ needs.documentation-quality.result }}" != "success" ] && FAILED="${FAILED}\n- Documentation Quality" + [ "${{ needs.coding-standards.result }}" != "success" ] && FAILED="${FAILED}\n- Coding Standards" + [ "${{ needs.license-compliance.result }}" != "success" ] && FAILED="${FAILED}\n- License Compliance" + [ "${{ needs.git-hygiene.result }}" != "success" ] && FAILED="${FAILED}\n- Git Hygiene" + [ "${{ needs.workflow-validation.result }}" != "success" ] && FAILED="${FAILED}\n- Workflow Validation" + [ "${{ needs.version-consistency.result }}" != "success" ] && FAILED="${FAILED}\n- Version Consistency" + [ "${{ needs.script-integrity.result }}" != "success" ] && FAILED="${FAILED}\n- Script Integrity" + [ "${{ needs.secret-scanning.result }}" != "success" ] && FAILED="${FAILED}\n- Secret Scanning" + [ "${{ needs.line-length-validation.result }}" != "success" ] && FAILED="${FAILED}\n- Line Length" + [ "${{ needs.file-size-limits.result }}" != "success" ] && FAILED="${FAILED}\n- File Size Limits" + [ "${{ needs.readme-completeness.result }}" != "success" ] && FAILED="${FAILED}\n- README Completeness" + + if [ -z "$FAILED" ]; then + echo "No failed checks to report" + exit 0 + fi + + TITLE="[Standards] Compliance violations โ€” ${DATE}" + BODY="## Standards Compliance Violations + + | Field | Value | + |-------|-------| + | **Branch** | \`${BRANCH}\` | + | **Commit** | \`${SHA:0:7}\` | + | **Actor** | @${ACTOR} | + | **Run** | [View workflow](${RUN_URL}) | + + ### Failed Checks + $(printf '%b' "$FAILED") + + ### Required Actions + 1. Review the [workflow run](${RUN_URL}) for details + 2. Fix each failed check + 3. Push to trigger a new scan + + --- + *Auto-created by standards-compliance workflow*" + + BODY=$(echo "$BODY" | sed 's/^ //') + LABEL="standards-violation" + + gh label create "$LABEL" --repo "$REPO" --color "D73A4A" --description "Standards compliance failure" --force 2>/dev/null || true + + EXISTING=$(gh api "repos/${REPO}/issues?labels=${LABEL}&state=all&per_page=1&sort=created&direction=desc" \ + --jq '.[0].number' 2>/dev/null) + + if [ -n "$EXISTING" ] && [ "$EXISTING" != "null" ]; then + gh api "repos/${REPO}/issues/${EXISTING}" -X PATCH \ + -f title="$TITLE" -f body="$BODY" -f state="open" --silent + echo "Updated issue #${EXISTING}" + else + gh issue create --repo "$REPO" --title "$TITLE" --body "$BODY" \ + --label "$LABEL" --assignee "jmiller-moko" + fi + +# CUSTOMIZATION: +# +# 1. Adjust severity of checks (convert warnings to errors or vice versa) +# 2. Add project-specific validation rules +# 3. Integrate with custom linting tools +# 4. Add notification steps for compliance failures +# 5. Customize required files/directories for your project type + diff --git a/.github/workflows/sync-version-on-merge.yml b/.github/workflows/sync-version-on-merge.yml new file mode 100644 index 00000000..052a7ca7 --- /dev/null +++ b/.github/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.05.13 +# BRIEF: Auto-bump patch version on every push to main and propagate to all file headers +# NOTE: Synced via bulk-repo-sync to .github/workflows/sync-version-on-merge.yml in all governed repos. +# README.md is the single source of truth for the repository version. + +name: Sync Version from README + +on: + push: + branches: + - main + - master + workflow_dispatch: + inputs: + dry_run: + description: 'Dry run (preview only, no commit)' + type: boolean + default: false + +permissions: + contents: write + issues: write + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + sync-version: + name: Propagate README version + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.GH_TOKEN || github.token }} + fetch-depth: 0 + + - name: Set up PHP + uses: shivammathur/setup-php@fcafdd6392932010c2bd5094439b8e33be2a8a09 # v2.37.0 + with: + php-version: '8.1' + tools: composer + + - name: Setup MokoStandards tools + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' + run: | + git clone --depth 1 --branch version/04 --quiet \ + "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ + /tmp/mokostandards + cd /tmp/mokostandards + composer install --no-dev --no-interaction --quiet + + - name: Auto-bump patch version + if: ${{ github.event_name == 'push' && github.actor != 'github-actions[bot]' }} + run: | + if git diff --name-only HEAD~1 HEAD 2>/dev/null | grep -q '^README\.md$'; then + echo "README.md changed in this push โ€” skipping auto-bump" + exit 0 + fi + + RESULT=$(php /tmp/mokostandards/api/cli/version_bump.php --path .) || { + echo "โš ๏ธ Could not bump version โ€” skipping" + exit 0 + } + echo "Auto-bumping patch: $RESULT" + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add README.md + git commit -m "chore(version): auto-bump patch ${RESULT} [skip ci]" \ + --author="github-actions[bot] " + git push + + - name: Extract version from README.md + id: readme_version + run: | + git pull --ff-only 2>/dev/null || true + VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null) + if [ -z "$VERSION" ]; then + echo "โš ๏ธ No VERSION in README.md โ€” skipping propagation" + echo "skip=true" >> $GITHUB_OUTPUT + exit 0 + fi + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "skip=false" >> $GITHUB_OUTPUT + echo "โœ… README.md version: $VERSION" + + - name: Run version sync + if: ${{ steps.readme_version.outputs.skip != 'true' && inputs.dry_run != true }} + run: | + php /tmp/mokostandards/api/maintenance/update_version_from_readme.php \ + --path . \ + --create-issue \ + --repo "${{ github.repository }}" + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + + - name: Commit updated files + if: ${{ steps.readme_version.outputs.skip != 'true' && inputs.dry_run != true }} + run: | + git pull --ff-only 2>/dev/null || true + if git diff --quiet; then + echo "โ„น๏ธ No version changes needed โ€” already up to date" + exit 0 + fi + VERSION="${{ steps.readme_version.outputs.version }}" + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add -A + git commit -m "chore(version): sync badges and headers to ${VERSION} [skip ci]" \ + --author="github-actions[bot] " + git push + + - name: Summary + run: | + VERSION="${{ steps.readme_version.outputs.version }}" + echo "## ๐Ÿ“ฆ Version Sync โ€” ${VERSION}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Source:** \`README.md\` FILE INFORMATION block" >> $GITHUB_STEP_SUMMARY + echo "**Version:** \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/update-server.yml b/.github/workflows/update-server.yml new file mode 100644 index 00000000..fad30ff3 --- /dev/null +++ b/.github/workflows/update-server.yml @@ -0,0 +1,260 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: GitHub.Workflow +# INGROUP: MokoStandards.Joomla +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /templates/workflows/joomla/update-server.yml.template +# VERSION: 04.05.13 +# BRIEF: Update Joomla update server XML feed with stable/rc/dev entries +# +# Writes updates.xml with multiple entries: +# - stable on push to main (from auto-release) +# - rc on push to rc/** +# - development on push to dev/** +# +# Joomla filters by user's "Minimum Stability" setting. + +name: Update Joomla Update Server XML Feed + +on: + push: + branches: + - 'dev/**' + - 'rc/**' + paths: + - 'src/**' + - 'htdocs/**' + workflow_dispatch: + inputs: + stability: + description: 'Stability tag (development, rc, stable)' + required: true + default: 'development' + type: choice + options: + - development + - rc + - stable + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +permissions: + contents: write + +jobs: + update-xml: + name: Update updates.xml + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.GH_TOKEN || github.token }} + fetch-depth: 0 + + - name: Setup MokoStandards tools + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' + run: | + git clone --depth 1 --branch version/04 --quiet \ + "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ + /tmp/mokostandards 2>/dev/null || true + if [ -d "/tmp/mokostandards" ] && [ -f "/tmp/mokostandards/composer.json" ]; then + cd /tmp/mokostandards && composer install --no-dev --no-interaction --quiet 2>/dev/null || true + fi + + - name: Generate updates.xml entry + run: | + BRANCH="${{ github.ref_name }}" + REPO="${{ github.repository }}" + VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null || echo "0.0.0") + + # Determine stability from branch or input + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + STABILITY="${{ inputs.stability }}" + elif [[ "$BRANCH" == rc/* ]]; then + STABILITY="rc" + elif [[ "$BRANCH" == dev/* ]]; then + STABILITY="development" + else + STABILITY="stable" + fi + + # Parse manifest + MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1) + if [ -z "$MANIFEST" ]; then + echo "No Joomla manifest found โ€” skipping" + exit 0 + fi + + EXT_NAME=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "${{ github.event.repository.name }}") + EXT_TYPE=$(grep -oP ']+type="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "component") + EXT_ELEMENT=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || basename "$MANIFEST" .xml) + EXT_CLIENT=$(grep -oP ']+client="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "") + EXT_FOLDER=$(grep -oP ']+group="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "") + TARGET_PLATFORM=$(grep -oP '' "$MANIFEST" 2>/dev/null | head -1 || echo "") + PHP_MINIMUM=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "") + + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(basename "$MANIFEST" .xml) + [ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '' "/") + + CLIENT_TAG="" + [ -n "$EXT_CLIENT" ] && CLIENT_TAG="${EXT_CLIENT}" + [ -z "$CLIENT_TAG" ] && ([ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]) && CLIENT_TAG="site" + + FOLDER_TAG="" + [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ] && FOLDER_TAG="${EXT_FOLDER}" + + PHP_TAG="" + [ -n "$PHP_MINIMUM" ] && PHP_TAG="${PHP_MINIMUM}" + + # Version suffix for non-stable + DISPLAY_VERSION="$VERSION" + [ "$STABILITY" = "rc" ] && DISPLAY_VERSION="${VERSION}-rc" + [ "$STABILITY" = "development" ] && DISPLAY_VERSION="${VERSION}-dev" + + MAJOR=$(echo "$VERSION" | awk -F. '{print $1}') + RELEASE_TAG="v${MAJOR}" + PACKAGE_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.zip" + DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}" + INFO_URL="https://github.com/${REPO}" + + # โ”€โ”€ Build install-ready ZIP โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + if [ -d "$SOURCE_DIR" ]; then + cd "$SOURCE_DIR" + zip -r "/tmp/${PACKAGE_NAME}" . + cd .. + + SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1) + + # Ensure draft release exists for this major + gh release view "$RELEASE_TAG" --json tagName > /dev/null 2>&1 || \ + gh release create "$RELEASE_TAG" --title "v${MAJOR}" --notes "Development release" --draft --target main 2>/dev/null || true + + # Upload ZIP to the major release + gh release upload "$RELEASE_TAG" "/tmp/${PACKAGE_NAME}" --clobber 2>/dev/null || true + + echo "Package: ${PACKAGE_NAME} (SHA: ${SHA256})" >> $GITHUB_STEP_SUMMARY + else + SHA256="" + fi + + # โ”€โ”€ Build the new entry โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + NEW_ENTRY=$(cat < + ${EXT_NAME} + ${EXT_NAME} (${STABILITY}) + ${EXT_ELEMENT} + ${EXT_TYPE} + ${DISPLAY_VERSION} + $([ -n "$CLIENT_TAG" ] && echo " ${CLIENT_TAG}") + $([ -n "$FOLDER_TAG" ] && echo " ${FOLDER_TAG}") + + ${STABILITY} + + ${INFO_URL} + + ${DOWNLOAD_URL} + + $([ -n "$SHA256" ] && echo " sha256:${SHA256}") + ${TARGET_PLATFORM} + $([ -n "$PHP_TAG" ] && echo " ${PHP_TAG}") + Moko Consulting + https://mokoconsulting.tech + +XMLEOF +) + + # โ”€โ”€ Merge into updates.xml โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + if [ ! -f "updates.xml" ]; then + # Create fresh + printf '%s\n' '' > updates.xml + printf '%s\n' '' >> updates.xml + echo "$NEW_ENTRY" >> updates.xml + printf '%s\n' '' >> updates.xml + else + # Remove existing entry for this stability, add new one + # Use python for reliable XML manipulation + python3 -c " +import re, sys + +with open('updates.xml', 'r') as f: + content = f.read() + +# Remove existing entry with this stability tag +pattern = r' .*?${STABILITY}.*?\n?' +content = re.sub(pattern, '', content, flags=re.DOTALL) + +# Insert new entry before +new_entry = '''${NEW_ENTRY}''' +content = content.replace('', new_entry + '\n') + +# Clean up empty lines +content = re.sub(r'\n{3,}', '\n\n', content) + +with open('updates.xml', 'w') as f: + f.write(content) +" 2>/dev/null || { + # Fallback: just rewrite the whole file if python fails + # Keep existing stable entry if present + STABLE_ENTRY="" + if [ "$STABILITY" != "stable" ] && grep -q 'stable' updates.xml; then + STABLE_ENTRY=$(sed -n '//,/<\/update>/{ /stable<\/tag>/,/<\/update>/p; //,/stable<\/tag>/p }' updates.xml | sort -u) + fi + RC_ENTRY="" + if [ "$STABILITY" != "rc" ] && grep -q 'rc' updates.xml; then + RC_ENTRY=$(python3 -c " +import re +with open('updates.xml') as f: c = f.read() +m = re.search(r'(.*?rc.*?)', c, re.DOTALL) +if m: print(m.group(1)) +" 2>/dev/null || true) + fi + DEV_ENTRY="" + if [ "$STABILITY" != "development" ] && grep -q 'development' updates.xml; then + DEV_ENTRY=$(python3 -c " +import re +with open('updates.xml') as f: c = f.read() +m = re.search(r'(.*?development.*?)', c, re.DOTALL) +if m: print(m.group(1)) +" 2>/dev/null || true) + fi + + { + printf '%s\n' '' + printf '%s\n' '' + [ -n "$STABLE_ENTRY" ] && echo "$STABLE_ENTRY" + [ -n "$RC_ENTRY" ] && echo "$RC_ENTRY" + [ -n "$DEV_ENTRY" ] && echo "$DEV_ENTRY" + echo "$NEW_ENTRY" + printf '%s\n' '' + } > updates.xml + } + fi + + # Commit + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add updates.xml + git diff --cached --quiet || { + git commit -m "chore: update updates.xml (${STABILITY}: ${DISPLAY_VERSION}) [skip ci]" \ + --author="github-actions[bot] " + git push + } + + echo "## Joomla Update Server" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${DISPLAY_VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Element | \`${EXT_ELEMENT}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Download | [ZIP](${DOWNLOAD_URL}) |" >> $GITHUB_STEP_SUMMARY