{{template "org/header" .}} - {{template "org/menu" .}}
{{if .WikiEmpty}}
@@ -11,8 +10,7 @@ This organization doesn't have a wiki yet.

- Enable the wiki on the .mokogitea (public) or .mokogitea-private (members-only) - repository to get started. + Enable a public or members-only wiki for this organization to get started.

{{else}} -- 2.52.0 From 31c58d34f19bc8a8d095c1ddec77dd75db62f7b9 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 14 Jul 2026 21:06:52 -0500 Subject: [PATCH 25/31] fix(org-wiki): also rewrite absolute-form intra-wiki links Match an optional https://host prefix so wiki links rendered as absolute URLs (not just root-relative) are rewritten to the /-/wiki/ route too. Covers nested pages like standards/Wiki-Structure. Claude-Session: https://claude.ai/code/session_01Bqe7fAuHQeiLueYfeHFrHw --- routers/web/org/wiki.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routers/web/org/wiki.go b/routers/web/org/wiki.go index 53f5d41188..510257d871 100644 --- a/routers/web/org/wiki.go +++ b/routers/web/org/wiki.go @@ -156,7 +156,7 @@ func Wiki(ctx *context.Context) { // wiki viewer instead of the repo file browser. func rewriteOrgWikiLinks(html htmltemplate.HTML, wikiRepo *repo_model.Repository, wikiBase string) htmltemplate.HTML { prefix := wikiRepo.Link() + "/src/branch/" + util.PathEscapeSegments(wikiRepo.DefaultBranch) + "/" - re := regexp.MustCompile(`href="` + regexp.QuoteMeta(prefix) + `([^"#?]*)([^"]*)"`) + re := regexp.MustCompile(`href="(?:https?://[^/"]+)?` + regexp.QuoteMeta(prefix) + `([^"#?]*)([^"]*)"`) out := re.ReplaceAllStringFunc(string(html), func(m string) string { sm := re.FindStringSubmatch(m) return `href="` + wikiBase + strings.TrimSuffix(sm[1], ".md") + sm[2] + `"` -- 2.52.0 From 3377e6895f1fbf54c71846108f90ba17308d2ce8 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 14 Jul 2026 20:43:52 -0500 Subject: [PATCH 26/31] fix(metadata): remove redundant Organization field from repo metadata page The Organization value is derived from the org profile (repo owner) and was shown as a read-only field; it added no value, so remove it entirely from the settings page. The DerivedOrgName model method stays (API still derives org on read). See #771. Claude-Session: https://claude.ai/code/session_01Bqe7fAuHQeiLueYfeHFrHw --- routers/web/repo/setting/metadata.go | 5 ++--- templates/repo/settings/metadata.tmpl | 9 +-------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/routers/web/repo/setting/metadata.go b/routers/web/repo/setting/metadata.go index 58abe8c548..c18838f5c4 100644 --- a/routers/web/repo/setting/metadata.go +++ b/routers/web/repo/setting/metadata.go @@ -40,9 +40,8 @@ func Metadata(ctx *context.Context) { } } ctx.Data["Manifest"] = manifest - // Organization is derived from the org profile (repo owner), not stored. - // It is displayed read-only. See issue #771. - ctx.Data["DerivedOrg"] = ctx.Repo.Repository.DerivedOrgName(ctx) + // Organization is derived from the org profile (repo owner), not stored, + // and is no longer surfaced on the metadata page (redundant). See #771. // Platform dropdown options come from the admin-configurable setting // (Admin -> Metadata). Keep the repo's current value selectable even if it diff --git a/templates/repo/settings/metadata.tmpl b/templates/repo/settings/metadata.tmpl index 8914ac430b..ee398f36a3 100644 --- a/templates/repo/settings/metadata.tmpl +++ b/templates/repo/settings/metadata.tmpl @@ -6,8 +6,7 @@
{{.CsrfTokenHtml}} -
-
+
{{if eq .Manifest.Platform "joomla"}} @@ -16,12 +15,6 @@ {{end}}
-
- - -

Derived from the organization profile (repository owner).

-
-
-- 2.52.0 From 8a4b479129d769ace6ac596fcb4028812d4cf041 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Wed, 15 Jul 2026 02:51:01 +0000 Subject: [PATCH 27/31] fix(org): point profile/wiki repo lookup at renamed .mokogit repos The org .mokogitea/.mokogitea-private repos were renamed to .mokogit/ .mokogit-private, but the constants still pointed at the old names, so the org profile + wiki lookup returned nil (empty wiki). Update the 4 RepoName* constants to match. Interim rebrand step. --- routers/web/shared/user/header.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go index 9b75cbc724..1e9efb187b 100644 --- a/routers/web/shared/user/header.go +++ b/routers/web/shared/user/header.go @@ -145,14 +145,14 @@ type PrepareOwnerHeaderResult struct { } const ( - RepoNameProfile = ".mokogitea" - RepoNameProfilePrivate = ".mokogitea-private" - RepoNameWikiPublic = ".mokogitea" - RepoNameWikiPrivate = ".mokogitea-private" + RepoNameProfile = ".mokogit" + RepoNameProfilePrivate = ".mokogit-private" + RepoNameWikiPublic = ".mokogit" + RepoNameWikiPrivate = ".mokogit-private" ) // profileFallbacks maps primary repo names to their fallback alternatives. -// Priority: .mokogitea > .profile > .github +// Priority: .mokogit > .profile > .github var profileFallbacks = map[string][]string{ RepoNameProfile: {".profile", ".github"}, RepoNameProfilePrivate: {".profile-private", ".github-private"}, @@ -237,7 +237,7 @@ func loadHeaderCount(ctx *context.Context) error { } // OrgWikiRepoExists checks whether a profile repo's wiki exists and has content. -// Tries fallback repo names (.mokogitea, .github) if the primary doesn't exist. +// Tries fallback repo names (.mokogit, .github) if the primary doesn't exist. func OrgWikiRepoExists(ctx *context.Context, ownerID int64, repoName string) bool { namesToTry := ProfileRepoFallbacks(repoName) -- 2.52.0 From b2396c636c3f65a37fe0d5c46c1d99be1739dab4 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Wed, 15 Jul 2026 03:24:56 +0000 Subject: [PATCH 28/31] fix(ci): harden issue-branch.yml against Actions injection (sync from Template-Generic) --- .mokogit/workflows/issue-branch.yml | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/.mokogit/workflows/issue-branch.yml b/.mokogit/workflows/issue-branch.yml index 0df045ccd0..e736ca81cb 100644 --- a/.mokogit/workflows/issue-branch.yml +++ b/.mokogit/workflows/issue-branch.yml @@ -3,8 +3,8 @@ # SPDX-License-Identifier: GPL-3.0-or-later # # FILE INFORMATION -# DEFGROUP: MokoGit.Workflow -# INGROUP: mokocli.Automation +# DEFGROUP: MokoGIT.Workflow +# INGROUP: MokoCLI.Automation # VERSION: 01.00.00 # BRIEF: Auto-create feature branch when an issue is opened @@ -27,14 +27,22 @@ jobs: runs-on: ubuntu-latest steps: - name: Create branch and comment + # SECURITY: never interpolate github.event.* into a run: script — the + # Actions engine splices the raw value into the shell source (command + # injection via a crafted issue title). Pass everything through env so + # the values arrive as ordinary shell variables that are not re-parsed. + env: + MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} + REPO: ${{ github.repository }} + ISSUE_NUM: ${{ github.event.issue.number }} + ISSUE_TITLE: ${{ github.event.issue.title }} run: | - TOKEN="${{ secrets.MOKOGIT_TOKEN }}" - API="${MOKOGIT_URL}/api/v1/repos/${{ github.repository }}" - ISSUE_NUM="${{ github.event.issue.number }}" - ISSUE_TITLE="${{ github.event.issue.title }}" + TOKEN="${MOKOGIT_TOKEN}" + API="${MOKOGIT_URL}/api/v1/repos/${REPO}" - # Build slug from title: lowercase, replace non-alnum with dash, trim - SLUG=$(echo "${ISSUE_TITLE}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//' | cut -c1-40) + # Build slug from title: lowercase, replace non-alnum with dash, trim. + # printf (not echo) so a title beginning with "-" is not read as flags. + SLUG=$(printf '%s' "${ISSUE_TITLE}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//' | cut -c1-40) BRANCH="feature/${ISSUE_NUM}-${SLUG}" # Check dev branch exists @@ -58,7 +66,7 @@ jobs: echo "Created branch: ${BRANCH}" # Comment on issue with branch link - REPO_URL="${MOKOGIT_URL}/${{ github.repository }}" + REPO_URL="${MOKOGIT_URL}/${REPO}" BODY="Branch created: [\`${BRANCH}\`](${REPO_URL}/src/branch/${BRANCH})\n\n\`\`\`bash\ngit fetch origin\ngit checkout ${BRANCH}\n\`\`\`" curl -sf -X POST \ -- 2.52.0 From 2d28553ed6a70426099f86bfce76c5f42e734503 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Wed, 15 Jul 2026 03:25:02 +0000 Subject: [PATCH 29/31] fix(ci): harden branch-cleanup.yml against Actions injection (sync from Template-Generic) --- .mokogit/workflows/branch-cleanup.yml | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/.mokogit/workflows/branch-cleanup.yml b/.mokogit/workflows/branch-cleanup.yml index 05fb53cad8..183b1df945 100644 --- a/.mokogit/workflows/branch-cleanup.yml +++ b/.mokogit/workflows/branch-cleanup.yml @@ -3,9 +3,9 @@ # SPDX-License-Identifier: GPL-3.0-or-later # # FILE INFORMATION -# DEFGROUP: MokoGit.Workflow -# INGROUP: MokoStandards.Universal -# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli +# DEFGROUP: MokoGIT.Workflow +# INGROUP: MokoCLI.Universal +# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic # PATH: /.mokogit/workflows/branch-cleanup.yml # VERSION: 01.00.00 # BRIEF: Delete feature branches after PR merge @@ -30,13 +30,25 @@ jobs: steps: - name: Delete source branch + # SECURITY: the PR head ref is attacker-controllable. Pass it (and the + # repo/token) through env so the Actions engine cannot splice it into the + # shell source, and validate it to a safe charset before use. + env: + MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} + REPO: ${{ github.repository }} + API_BASE: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + BRANCH: ${{ github.event.pull_request.head.ref }} run: | - BRANCH="${{ github.event.pull_request.head.ref }}" - API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches" - ENCODED=$(php -r "echo rawurlencode('${BRANCH}');") + set -euo pipefail + if ! printf '%s' "${BRANCH}" | grep -Eq '^[A-Za-z0-9._/-]+$'; then + echo "::error::unsafe branch name; refusing to proceed"; exit 1 + fi + API="${API_BASE}/api/v1/repos/${REPO}/branches" + # URL-encode the branch name's slashes (no PHP dependency on the runner) + ENCODED=$(printf '%s' "${BRANCH}" | sed 's|/|%2F|g') STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \ - -H "Authorization: token ${{ secrets.MOKOGIT_TOKEN }}" \ + -H "Authorization: token ${MOKOGIT_TOKEN}" \ "${API}/${ENCODED}" 2>/dev/null || true) if [ "$STATUS" = "204" ]; then -- 2.52.0 From 402f5d94120d90edf25eb29b4260fe50f605bcbd Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 14 Jul 2026 22:55:05 -0500 Subject: [PATCH 30/31] chore: re-trigger dev deploy for MokoGIT rebrand Claude-Session: https://claude.ai/code/session_01Bqe7fAuHQeiLueYfeHFrHw -- 2.52.0 From 4885f3c123efa1cb70cd89e324a6983c7c8aa090 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Wed, 15 Jul 2026 00:37:26 -0500 Subject: [PATCH 31/31] chore: drop orphaned .mokogit/workflows dupes from main merge The main->rebrand merge resurrected .mokogit/workflows/ (workflows live at .mokogitea/workflows during the cutover, per WorkflowDirs). Remove the dupes. Claude-Session: https://claude.ai/code/session_01Bqe7fAuHQeiLueYfeHFrHw --- .mokogit/workflows/auto-bump.yml | 66 - .mokogit/workflows/auto-release.yml | 505 ---- .mokogit/workflows/branch-cleanup.yml | 60 - .mokogit/workflows/cascade-dev.yml | 10 - .mokogit/workflows/ci-generic.yml | 197 -- .mokogit/workflows/ci-issue-reporter.yml | 68 - .mokogit/workflows/cleanup.yml | 87 - .mokogit/workflows/deploy-dev.yml | 136 - .mokogit/workflows/deploy-prod.yml | 136 - .mokogit/workflows/deploy-rc.yml | 138 - .mokogit/workflows/gitleaks.yml | 92 - .mokogit/workflows/issue-branch.yml | 81 - .mokogit/workflows/notify.yml | 70 - .mokogit/workflows/pr-check.yml | 522 ---- .mokogit/workflows/pre-release.yml | 273 -- .mokogit/workflows/rc-revert.yml | 71 - .mokogit/workflows/repo-health.yml | 700 ----- .mokogit/workflows/standards-compliance.yml | 2613 ------------------- .mokogit/workflows/version-set.yml | 130 - 19 files changed, 5955 deletions(-) delete mode 100644 .mokogit/workflows/auto-bump.yml delete mode 100644 .mokogit/workflows/auto-release.yml delete mode 100644 .mokogit/workflows/branch-cleanup.yml delete mode 100644 .mokogit/workflows/cascade-dev.yml delete mode 100644 .mokogit/workflows/ci-generic.yml delete mode 100644 .mokogit/workflows/ci-issue-reporter.yml delete mode 100644 .mokogit/workflows/cleanup.yml delete mode 100644 .mokogit/workflows/deploy-dev.yml delete mode 100644 .mokogit/workflows/deploy-prod.yml delete mode 100644 .mokogit/workflows/deploy-rc.yml delete mode 100644 .mokogit/workflows/gitleaks.yml delete mode 100644 .mokogit/workflows/issue-branch.yml delete mode 100644 .mokogit/workflows/notify.yml delete mode 100644 .mokogit/workflows/pr-check.yml delete mode 100644 .mokogit/workflows/pre-release.yml delete mode 100644 .mokogit/workflows/rc-revert.yml delete mode 100644 .mokogit/workflows/repo-health.yml delete mode 100644 .mokogit/workflows/standards-compliance.yml delete mode 100644 .mokogit/workflows/version-set.yml diff --git a/.mokogit/workflows/auto-bump.yml b/.mokogit/workflows/auto-bump.yml deleted file mode 100644 index 826a41ca40..0000000000 --- a/.mokogit/workflows/auto-bump.yml +++ /dev/null @@ -1,66 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: MokoGit.Workflow -# INGROUP: mokocli.Release -# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli -# PATH: /.mokogit/workflows/auto-bump.yml -# VERSION: 09.02.00 -# BRIEF: Auto patch-bump version on every push to dev (skips merge commits) - -name: "Universal: Auto Version Bump" - -on: - push: - branches: - - dev - - rc - - 'feature/**' - - 'patch/**' - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - -permissions: - contents: write - -jobs: - bump: - name: Version Bump - runs-on: release - if: >- - !contains(github.event.head_commit.message, '[skip ci]') && - !contains(github.event.head_commit.message, '[skip bump]') && - !startsWith(github.event.head_commit.message, 'Merge pull request') - - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.MOKOGIT_TOKEN }} - fetch-depth: 1 - - - name: Setup mokocli tools - run: | - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - if [ -d "/opt/mokocli/cli" ]; then - echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV" - else - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \ - /tmp/mokocli - cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet - echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV" - fi - - - name: Bump version - run: | - php ${MOKO_CLI}/version_auto_bump.php \ - --path . --branch "${GITHUB_REF_NAME}" \ - --token "${{ secrets.MOKOGIT_TOKEN }}" \ - --repo-url "https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" diff --git a/.mokogit/workflows/auto-release.yml b/.mokogit/workflows/auto-release.yml deleted file mode 100644 index aa8f4388ef..0000000000 --- a/.mokogit/workflows/auto-release.yml +++ /dev/null @@ -1,505 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: MokoGit.Workflow -# INGROUP: mokocli.Release -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli -# PATH: /templates/workflows/universal/auto-release.yml.template -# VERSION: 05.00.00 -# BRIEF: Universal build & release � detects platform from manifest.xml -# -# +=======================================================================+ -# | UNIVERSAL BUILD & RELEASE PIPELINE | -# +=======================================================================+ -# | | -# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. | -# | | -# | Platform-specific: | -# | joomla: XML manifest, type-prefixed packages | -# | dolibarr: mod*.class.php, update.txt, dev version reset | -# | generic: README-only, no update stream | -# | | -# +=======================================================================+ - -name: "Universal: Build & Release" - -on: - pull_request: - types: [opened, synchronize, closed] - branches: - - main - paths-ignore: - - '.mokogit/workflows/**' - - '*.md' - - 'wiki/**' - - '.editorconfig' - - '.gitignore' - - '.gitattributes' - - '.gitmessage' - - 'LICENSE' - workflow_dispatch: - inputs: - action: - description: 'Action to perform' - required: false - type: choice - default: release - options: - - release - - promote-rc - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GIT_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GIT_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} - -permissions: - contents: write - -jobs: - # ── PR Opened → Rename branch to RC and build RC release ───────────────────────── - promote-rc: - name: Promote to RC - runs-on: release - if: >- - (github.event.action == 'opened' && github.event.pull_request.merged != true) || - (github.event.action == 'synchronize' && github.event.pull_request.merged != true) || - (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.MOKOGIT_TOKEN }} - fetch-depth: 1 - - - name: Setup mokocli tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - run: | - if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then - echo Using pre-installed /opt/mokocli - echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV - else - echo Falling back to fresh clone - if ! command -v composer > /dev/null 2>&1; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1 - fi - rm -rf /tmp/mokocli - CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git - git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli - cd /tmp/mokocli - composer install --no-dev --no-interaction --quiet - echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV - fi - - - name: Rename branch to rc - run: | - API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" - AUTH="Authorization: token ${{ secrets.MOKOGIT_TOKEN }}" - FROM="${{ github.event.pull_request.head.ref || 'dev' }}" - PR="${{ github.event.pull_request.number }}" - - # Resolve the source branch HEAD commit. - SRC_JSON=$(curl -sf -H "$AUTH" "${API_BASE}/branches/${FROM}") \ - || { echo "::error::Source branch ${FROM} not found"; exit 1; } - SRC_SHA=$(printf '%s' "$SRC_JSON" | python3 -c "import sys, json; print(json.load(sys.stdin)['commit']['id'])" 2>/dev/null || true) - [ -n "$SRC_SHA" ] || { echo "::error::Could not resolve HEAD of ${FROM}"; exit 1; } - - # Point rc at the source commit via git push. Git's git/refs PATCH API - # returns HTTP 405 on ANY protected branch (force or not, even for a user in - # the force-push allowlist), so it cannot move a protected rc. git push honors - # the push + force-push allowlists and creates rc if it is absent. - PUSH_URL="https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@${MOKOGIT_URL#https://}/${GIT_ORG}/${GIT_REPO}.git" - git config --global user.name "mokogit-actions[bot]" - git config --global user.email "actions@mokoconsulting.tech" - git fetch --no-tags "$PUSH_URL" "${FROM}" - git push --force "$PUSH_URL" "FETCH_HEAD:refs/heads/rc" \ - || { echo "::error::Failed to point rc at ${FROM} (${SRC_SHA}) via git push"; exit 1; } - echo "rc set to ${FROM} (${SRC_SHA})" - - # Repoint the PR at rc, then delete the old source branch (non-fatal). - if [ -n "$PR" ]; then - curl -s -X PATCH -H "$AUTH" -H "Content-Type: application/json" \ - "${API_BASE}/pulls/${PR}" -d '{"head":"rc"}' >/dev/null || true - fi - # Never delete permanent branches (dev/main/rc/...); only ephemeral feature branches. - case "$FROM" in - dev|main|master|rc|stable|production|release|develop|staging|beta|alpha) - echo "Keeping permanent branch ${FROM} (not deleting)" ;; - *) - curl -s -X DELETE -H "$AUTH" "${API_BASE}/branches/${FROM}" >/dev/null || true ;; - esac - echo "Renamed ${FROM} -> rc" - - - name: Trigger RC deploy - run: | - # Workflow-token pushes do NOT wake downstream workflows; dispatch deploy-rc explicitly. - curl -sf -X POST -H "Authorization: token ${{ secrets.MOKOGIT_TOKEN }}" \n -H "Content-Type: application/json" \n "${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}/actions/workflows/deploy-rc.yml/dispatches" \n -d '{"ref":"rc"}' \n && echo "Dispatched deploy-rc on rc" \n || echo "::warning::deploy-rc dispatch failed (no deploy-rc.yml on rc?)" - - - name: Checkout rc and configure git - run: | - git fetch origin rc - git checkout rc - git config --local user.email "mokogit-actions[bot]@mokoconsulting.tech" - git config --local user.name "mokogit-actions[bot]" - git remote set-url origin "https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - - - name: Publish RC release - continue-on-error: true - run: | - php ${MOKO_CLI}/release_publish.php \ - --path . --stability rc --bump minor --branch rc \ - --token "${{ secrets.MOKOGIT_TOKEN }}" - - - name: Update RC release notes from CHANGELOG.md - continue-on-error: true - run: | - API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" - TOKEN="${{ secrets.MOKOGIT_TOKEN }}" - - # Extract [Unreleased] section from changelog - NOTES="" - if [ -f "CHANGELOG.md" ]; then - NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md) - fi - [ -z "$NOTES" ] && NOTES="Release candidate" - - # Find the RC release and update its body - RELEASE_ID=$(curl -sf -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/releases/tags/release-candidate" \ - | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) - - if [ -n "$RELEASE_ID" ]; then - python3 -c " - import json, urllib.request - body = open('/dev/stdin').read() - payload = json.dumps({'body': body}).encode() - req = urllib.request.Request( - '${API_BASE}/releases/${RELEASE_ID}', - data=payload, method='PATCH', - headers={ - 'Authorization': 'token ${TOKEN}', - 'Content-Type': 'application/json' - }) - urllib.request.urlopen(req) - " <<< "$NOTES" - echo "RC release notes updated from CHANGELOG.md" - fi - - - name: Summary - if: always() - run: | - echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY - echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY - - # ── Merged PR → Build & Release (or promote RC to stable) ───────────────────────── - release: - name: Build & Release Pipeline - runs-on: release - if: >- - github.event.pull_request.merged == true || - (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc') - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.MOKOGIT_TOKEN }} - fetch-depth: 0 - - - name: Configure git for bot pushes - run: | - git config --local user.email "mokogit-actions[bot]@mokoconsulting.tech" - git config --local user.name "mokogit-actions[bot]" - git remote set-url origin "https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - - - name: Check for merge conflict markers - run: | - CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true) - if [ -n "$CONFLICTS" ]; then - echo "::error::Merge conflict markers found — aborting release" - echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - exit 1 - fi - echo "No conflict markers found" - - - name: Setup mokocli tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}' - run: | - if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then - echo Using pre-installed /opt/mokocli - echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV - else - echo Falling back to fresh clone - if ! command -v composer > /dev/null 2>&1; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1 - fi - rm -rf /tmp/mokocli - CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git - git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli - cd /tmp/mokocli - composer install --no-dev --no-interaction --quiet - echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV - fi - - - name: "Detect platform" - id: platform - run: | - php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true - php ${MOKO_CLI}/manifest_read.php --path . --github-output 2>/dev/null || true - - - name: "Determine version bump level" - id: bump - run: | - # Fix/patch branches: version was already bumped by pre-release, just strip suffix - # Feature/dev branches: bump minor for the new stable release - HEAD_REF="${{ github.event.pull_request.head.ref || 'dev' }}" - case "$HEAD_REF" in - fix/*|patch/*|hotfix/*|bugfix/*) BUMP="none" ;; - *) BUMP="minor" ;; - esac - echo "level=${BUMP}" >> "$GITHUB_OUTPUT" - echo "Bump level: ${BUMP} (from branch: ${HEAD_REF})" - - - name: "Publish stable release" - run: | - BUMP_FLAG="" - if [ "${{ steps.bump.outputs.level }}" != "none" ]; then - BUMP_FLAG="--bump ${{ steps.bump.outputs.level }}" - fi - php ${MOKO_CLI}/release_publish.php \ - --path . --stability stable ${BUMP_FLAG} --branch main \ - --token "${{ secrets.MOKOGIT_TOKEN }}" - - - name: "Read published version" - id: version - run: | - VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "") - VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') - [ -z "$VERSION" ] && VERSION="00.00.00" && echo "skip=true" >> "$GITHUB_OUTPUT" - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - PLATFORM="${{ steps.platform.outputs.platform }}" - if [[ "$PLATFORM" == joomla* ]]; then - echo "tag=stable" >> "$GITHUB_OUTPUT" - echo "release_tag=stable" >> "$GITHUB_OUTPUT" - else - echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT" - echo "release_tag=v${VERSION}" >> "$GITHUB_OUTPUT" - fi - echo "branch=main" >> "$GITHUB_OUTPUT" - echo "Published version: ${VERSION}" - - - name: "Create semver tag for non-Joomla repos" - id: semver - if: | - steps.version.outputs.skip != 'true' && - !startsWith(steps.platform.outputs.platform, 'joomla') - run: | - VERSION="${{ steps.version.outputs.version }}" - API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" - TOKEN="${{ secrets.MOKOGIT_TOKEN }}" - SEMVER_TAG="v${VERSION}" - - echo "Creating semver tag: ${SEMVER_TAG}" - - # Create the git tag via API - HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" \ - -X POST -H "Authorization: token ${TOKEN}" \ - -H "Content-Type: application/json" \ - "${API_BASE}/tags" \ - -d "{\"tag_name\":\"${SEMVER_TAG}\",\"target\":\"main\",\"message\":\"Release ${VERSION}\"}" 2>/dev/null || echo "000") - - if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "200" ]; then - echo "Created semver tag: ${SEMVER_TAG}" - elif [ "$HTTP_CODE" = "409" ]; then - echo "Semver tag ${SEMVER_TAG} already exists (skipped)" - else - echo "::warning::Failed to create semver tag ${SEMVER_TAG} (HTTP ${HTTP_CODE})" - fi - - echo "semver_tag=${SEMVER_TAG}" >> "$GITHUB_OUTPUT" - - - name: Update release notes and promote changelog - run: | - API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" - TOKEN="${{ secrets.MOKOGIT_TOKEN }}" - - # Get the stable release info (version and ID) - RELEASE_JSON=$(curl -sf -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/releases/tags/stable" 2>/dev/null || echo '{}') - RELEASE_ID=$(python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" <<< "$RELEASE_JSON" 2>/dev/null || true) - # Extract version from release name (e.g. "06.17.00" or "v06.17.00") - VERSION=$(python3 -c " - import json, sys, re - r = json.load(sys.stdin) - name = r.get('name', '') - m = re.search(r'(\d+\.\d+\.\d+)', name) - print(m.group(1) if m else '') - " <<< "$RELEASE_JSON" 2>/dev/null || true) - - # Extract [Unreleased] section from changelog - NOTES="" - if [ -f "CHANGELOG.md" ]; then - NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md) - fi - [ -z "$NOTES" ] && NOTES="Stable release" - - # Update release body via API - if [ -n "$RELEASE_ID" ]; then - python3 -c " - import json, urllib.request - body = open('/dev/stdin').read() - payload = json.dumps({'body': body}).encode() - req = urllib.request.Request( - '${API_BASE}/releases/${RELEASE_ID}', - data=payload, method='PATCH', - headers={ - 'Authorization': 'token ${TOKEN}', - 'Content-Type': 'application/json' - }) - urllib.request.urlopen(req) - " <<< "$NOTES" - echo "Release notes updated from CHANGELOG.md" - fi - - # Promote [Unreleased] → [version] in CHANGELOG.md and reset - if [ -n "$VERSION" ] && [ -f "CHANGELOG.md" ]; then - DATE=$(date +%Y-%m-%d) - python3 -c " - import sys - version, date = sys.argv[1], sys.argv[2] - content = open('CHANGELOG.md').read() - old = '## [Unreleased]' - new = f'## [Unreleased]\n\n## [{version}] --- {date}' - content = content.replace(old, new, 1) - open('CHANGELOG.md', 'w').write(content) - " "$VERSION" "$DATE" - git add CHANGELOG.md - git commit -m "chore: promote changelog [Unreleased] → [${VERSION}]" || true - git push origin main || true - echo "Changelog promoted: [Unreleased] → [${VERSION}]" - fi - - # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- - - name: "Step 9: Mirror release to GitHub" - if: >- - steps.version.outputs.skip != 'true' && - secrets.GH_MIRROR_TOKEN != '' - continue-on-error: true - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" - API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" - php ${MOKO_CLI}/release_mirror.php \ - --version "$VERSION" --tag "$RELEASE_TAG" \ - --token "${{ secrets.MOKOGIT_TOKEN }}" --api-base "$API_BASE" \ - --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \ - --branch main 2>&1 || true - echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY - - # -- STEP 10: Sync main branch to GitHub mirror ---------------------------- - - name: "Step 10: Push main to GitHub mirror" - if: >- - steps.version.outputs.skip != 'true' && - secrets.GH_MIRROR_TOKEN != '' - continue-on-error: true - run: | - GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" - GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1) - GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2) - git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ - git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" - git fetch origin main --depth=1 - git push github origin/main:refs/heads/main --force 2>/dev/null \ - && echo "main branch pushed to GitHub mirror" \ - || echo "WARNING: GitHub mirror push failed" - - - name: "Step 11: Delete rc branch and recreate dev from main" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" - TOKEN="${{ secrets.MOKOGIT_TOKEN }}" - - # Delete rc branch (ephemeral — created by promote-rc) - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/branches/rc" 2>/dev/null \ - && echo "Deleted rc branch" || echo "rc branch not found" - - # Delete dev branch - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch" - - # Recreate dev from main (now includes version bump + changelog promotion) - curl -sf -X POST -H "Authorization: token ${TOKEN}" \ - -H "Content-Type: application/json" \ - "${API_BASE}/branches" \ - -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main" - - echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY - - - name: "Step 12: Create version branch from main" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" - TOKEN="${{ secrets.MOKOGIT_TOKEN }}" - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - BRANCH_NAME="version/${VERSION}" - MAIN_SHA=$(git rev-parse HEAD) - - # Delete old version branch if it exists (same version re-release) - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}" - - # Create version/XX.YY.ZZ from main - curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed" - - echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY - - - - # -- Dolibarr post-release: Reset dev version ----------------------------- - - name: "Post-release: Reset dev version" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" - php ${MOKO_CLI}/version_reset_dev.php \ - --token "${{ secrets.MOKOGIT_TOKEN }}" --api-base "${API_BASE}" \ - --branch dev --path . 2>&1 || true - - # -- Summary -------------------------------------------------------------- - - name: Pipeline Summary - if: always() - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - PLATFORM="${{ steps.platform.outputs.platform }}" - if [ "${{ steps.version.outputs.skip }}" = "true" ]; then - echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY - echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY - elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then - echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY - else - echo "" >> $GITHUB_STEP_SUMMARY - echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY - echo "|------|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Release | [View](${MOKOGIT_URL}/${GIT_ORG}/${GIT_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY - fi diff --git a/.mokogit/workflows/branch-cleanup.yml b/.mokogit/workflows/branch-cleanup.yml deleted file mode 100644 index 183b1df945..0000000000 --- a/.mokogit/workflows/branch-cleanup.yml +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: MokoGIT.Workflow -# INGROUP: MokoCLI.Universal -# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic -# PATH: /.mokogit/workflows/branch-cleanup.yml -# VERSION: 01.00.00 -# BRIEF: Delete feature branches after PR merge - -name: "Branch Cleanup" - -on: - pull_request: - types: [closed] - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - cleanup: - name: Delete merged branch - runs-on: ubuntu-latest - if: >- - github.event.pull_request.merged == true && - github.event.pull_request.head.ref != 'dev' && - github.event.pull_request.head.ref != 'main' - - steps: - - name: Delete source branch - # SECURITY: the PR head ref is attacker-controllable. Pass it (and the - # repo/token) through env so the Actions engine cannot splice it into the - # shell source, and validate it to a safe charset before use. - env: - MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} - REPO: ${{ github.repository }} - API_BASE: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - BRANCH: ${{ github.event.pull_request.head.ref }} - run: | - set -euo pipefail - if ! printf '%s' "${BRANCH}" | grep -Eq '^[A-Za-z0-9._/-]+$'; then - echo "::error::unsafe branch name; refusing to proceed"; exit 1 - fi - API="${API_BASE}/api/v1/repos/${REPO}/branches" - # URL-encode the branch name's slashes (no PHP dependency on the runner) - ENCODED=$(printf '%s' "${BRANCH}" | sed 's|/|%2F|g') - - STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \ - -H "Authorization: token ${MOKOGIT_TOKEN}" \ - "${API}/${ENCODED}" 2>/dev/null || true) - - if [ "$STATUS" = "204" ]; then - echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY - elif [ "$STATUS" = "404" ]; then - echo "Branch already deleted: ${BRANCH}" >> $GITHUB_STEP_SUMMARY - else - echo "::warning::Failed to delete branch ${BRANCH} (HTTP ${STATUS})" - fi diff --git a/.mokogit/workflows/cascade-dev.yml b/.mokogit/workflows/cascade-dev.yml deleted file mode 100644 index 5f7c1d7273..0000000000 --- a/.mokogit/workflows/cascade-dev.yml +++ /dev/null @@ -1,10 +0,0 @@ -# DISABLED — auto-release Step 11 recreates dev from main after every release. -# Cascade-dev is redundant and causes version conflicts when both main and dev -# have different version numbers in templateDetails.xml / manifest.xml. -name: "Cascade Main → Dev (DISABLED)" -on: workflow_dispatch -jobs: - noop: - runs-on: ubuntu-latest - steps: - - run: echo "Cascade disabled — auto-release handles dev recreation" diff --git a/.mokogit/workflows/ci-generic.yml b/.mokogit/workflows/ci-generic.yml deleted file mode 100644 index a9e83d398a..0000000000 --- a/.mokogit/workflows/ci-generic.yml +++ /dev/null @@ -1,197 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: MokoGit.Workflow -# INGROUP: MokoStandards.CI -# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic -# PATH: /.gitea/workflows/ci-generic.yml -# VERSION: 01.00.00 -# BRIEF: CI pipeline — lint, validate, and test for generic projects (PHP + Node.js) - -name: "Generic: Project CI" - -on: - pull_request: - branches: - - main - - dev - - dev/** - - rc/** - workflow_dispatch: - -permissions: - contents: read - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - # ── Lint & Validate ─────────────────────────────────────────────────── - lint: - name: Lint & Validate - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Detect toolchain - id: detect - run: | - HAS_PHP=false - HAS_NODE=false - [ -f "composer.json" ] && HAS_PHP=true - [ -f "package.json" ] && HAS_NODE=true - echo "has_php=$HAS_PHP" >> "$GITHUB_OUTPUT" - echo "has_node=$HAS_NODE" >> "$GITHUB_OUTPUT" - echo "Toolchain: PHP=$HAS_PHP Node=$HAS_NODE" - - - name: Setup PHP - if: steps.detect.outputs.has_php == 'true' - run: | - if ! command -v php &> /dev/null; then - sudo apt-get update -qq - sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1 - fi - php -v - - - name: Setup Node.js - if: steps.detect.outputs.has_node == 'true' - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Install PHP dependencies - if: steps.detect.outputs.has_php == 'true' - run: | - if [ -f "composer.json" ]; then - composer install --no-interaction --prefer-dist --quiet 2>/dev/null || true - fi - - - name: Install Node.js dependencies - if: steps.detect.outputs.has_node == 'true' - run: | - if [ -f "package.json" ]; then - npm ci --quiet 2>/dev/null || npm install --quiet 2>/dev/null || true - fi - - - name: PHP syntax check - if: steps.detect.outputs.has_php == 'true' - run: | - ERRORS=0 - while IFS= read -r -d '' file; do - if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then - echo "::error file=${file}::PHP syntax error" - ERRORS=$((ERRORS + 1)) - fi - done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./node_modules/*" -print0) - - echo "## PHP Lint" >> $GITHUB_STEP_SUMMARY - if [ "$ERRORS" -eq 0 ]; then - echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY - else - echo "${ERRORS} file(s) with syntax errors." >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - - name: TypeScript/JavaScript lint - if: steps.detect.outputs.has_node == 'true' - run: | - if [ -f "node_modules/.bin/eslint" ]; then - npx eslint src/ --quiet 2>&1 || { echo "::error::ESLint errors found"; exit 1; } - echo "## ESLint" >> $GITHUB_STEP_SUMMARY - echo "All files passed ESLint." >> $GITHUB_STEP_SUMMARY - elif [ -f ".eslintrc.json" ] || [ -f ".eslintrc.js" ] || [ -f "eslint.config.js" ]; then - echo "::warning::ESLint config found but eslint not installed" - else - echo "No ESLint configured — skipping" - fi - - - name: TypeScript compile check - if: steps.detect.outputs.has_node == 'true' - run: | - if [ -f "tsconfig.json" ] && [ -f "node_modules/.bin/tsc" ]; then - npx tsc --noEmit 2>&1 || { echo "::error::TypeScript compilation errors"; exit 1; } - echo "## TypeScript" >> $GITHUB_STEP_SUMMARY - echo "TypeScript compilation passed." >> $GITHUB_STEP_SUMMARY - fi - - - name: PHPStan static analysis - if: steps.detect.outputs.has_php == 'true' - run: | - if [ -f "phpstan.neon" ] && [ -f "vendor/bin/phpstan" ]; then - vendor/bin/phpstan analyse --no-progress 2>&1 || { echo "::warning::PHPStan found issues"; } - fi - - # ── Tests ───────────────────────────────────────────────────────────── - test: - name: Tests - runs-on: ubuntu-latest - needs: lint - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Detect toolchain - id: detect - run: | - HAS_PHP=false - HAS_NODE=false - [ -f "composer.json" ] && HAS_PHP=true - [ -f "package.json" ] && HAS_NODE=true - echo "has_php=$HAS_PHP" >> "$GITHUB_OUTPUT" - echo "has_node=$HAS_NODE" >> "$GITHUB_OUTPUT" - - - name: Setup PHP - if: steps.detect.outputs.has_php == 'true' - run: | - if ! command -v php &> /dev/null; then - sudo apt-get update -qq - sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1 - fi - - - name: Setup Node.js - if: steps.detect.outputs.has_node == 'true' - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Install dependencies - run: | - [ -f "composer.json" ] && composer install --no-interaction --prefer-dist --quiet 2>/dev/null || true - [ -f "package.json" ] && { npm ci --quiet 2>/dev/null || npm install --quiet 2>/dev/null || true; } - - - name: Run PHP tests - if: steps.detect.outputs.has_php == 'true' - run: | - if [ -f "vendor/bin/phpunit" ]; then - vendor/bin/phpunit --testdox 2>&1 - echo "## PHPUnit" >> $GITHUB_STEP_SUMMARY - echo "Tests passed." >> $GITHUB_STEP_SUMMARY - elif [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then - echo "::warning::PHPUnit config found but phpunit not installed" - else - echo "No PHPUnit configured — skipping" - fi - - - name: Run Node.js tests - if: steps.detect.outputs.has_node == 'true' - run: | - if jq -e '.scripts.test' package.json > /dev/null 2>&1; then - npm test 2>&1 - echo "## Node.js Tests" >> $GITHUB_STEP_SUMMARY - echo "Tests passed." >> $GITHUB_STEP_SUMMARY - else - echo "No test script in package.json — skipping" - fi - - - name: Build check - run: | - if [ -f "Makefile" ]; then - make build 2>&1 || echo "::warning::Build failed or not configured" - elif [ -f "package.json" ] && jq -e '.scripts.build' package.json > /dev/null 2>&1; then - npm run build 2>&1 || echo "::warning::Build failed" - fi diff --git a/.mokogit/workflows/ci-issue-reporter.yml b/.mokogit/workflows/ci-issue-reporter.yml deleted file mode 100644 index b6339da136..0000000000 --- a/.mokogit/workflows/ci-issue-reporter.yml +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: MokoGit.Workflow -# INGROUP: mokocli.Universal -# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli -# PATH: /.mokogit/workflows/ci-issue-reporter.yml -# VERSION: 01.00.00 -# BRIEF: Reusable workflow — creates/updates a MokoGit issue when a CI gate fails. -# Clones MokoCLI and runs cli/ci_issue_reporter.sh. - -name: "Universal: CI Issue Reporter" - -on: - workflow_call: - inputs: - gate: - description: "CI gate name (e.g. PR Validation, Repository Health)" - required: true - type: string - details: - description: "Human-readable failure description" - required: true - type: string - severity: - description: "error or warning" - required: false - type: string - default: "error" - workflow: - description: "Workflow name for the issue title" - required: false - type: string - default: "" - secrets: - MOKOGIT_TOKEN: - required: true - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - report: - name: "Report: ${{ inputs.gate }}" - runs-on: ubuntu-latest - - steps: - - name: Clone MokoCLI - env: - MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} - run: | - MOKOGIT_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}" - git clone --depth 1 --filter=blob:none --sparse "${MOKOGIT_URL}/MokoConsulting/MokoCLI.git" /tmp/mokocli - cd /tmp/mokocli && git sparse-checkout set cli/ci_issue_reporter.sh - - - name: Report CI failure - env: - MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} - MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - run: | - chmod +x /tmp/mokocli/cli/ci_issue_reporter.sh - /tmp/mokocli/cli/ci_issue_reporter.sh \ - --gate "${{ inputs.gate }}" \ - --details "${{ inputs.details }}" \ - --severity "${{ inputs.severity }}" \ - --workflow "${{ inputs.workflow }}" diff --git a/.mokogit/workflows/cleanup.yml b/.mokogit/workflows/cleanup.yml deleted file mode 100644 index f1b76c4ab9..0000000000 --- a/.mokogit/workflows/cleanup.yml +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: MokoGit.Workflow -# INGROUP: MokoStandards.Maintenance -# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards -# PATH: /.gitea/workflows/cleanup.yml -# VERSION: 01.00.00 -# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs - -name: "Universal: Repository Cleanup" - -on: - schedule: - - cron: '0 3 * * 0' # Weekly on Sunday at 03:00 UTC - workflow_dispatch: - -permissions: - contents: write - -env: - MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - -jobs: - cleanup: - name: Clean Merged Branches - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.MOKOGIT_TOKEN }} - - - name: Delete merged branches - env: - MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} - run: | - echo "=== Merged Branch Cleanup ===" - API="${MOKOGIT_URL}/api/v1/repos/${{ github.repository }}" - - # List branches via API - BRANCHES=$(curl -sS -H "Authorization: token ${MOKOGIT_TOKEN}" \ - "${API}/branches?limit=50" | jq -r '.[].name') - - DELETED=0 - for BRANCH in $BRANCHES; do - # Skip protected branches - case "$BRANCH" in - main|master|develop|release/*|hotfix/*) continue ;; - esac - - # Check if branch is merged into main - if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then - echo " Deleting merged branch: ${BRANCH}" - curl -sS -X DELETE -H "Authorization: token ${MOKOGIT_TOKEN}" \ - "${API}/branches/${BRANCH}" 2>/dev/null || true - DELETED=$((DELETED + 1)) - fi - done - - echo "Deleted ${DELETED} merged branch(es)" - - - name: Clean old workflow runs - env: - MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} - run: | - echo "=== Workflow Run Cleanup ===" - API="${MOKOGIT_URL}/api/v1/repos/${{ github.repository }}" - CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ) - - # Get old completed runs - RUNS=$(curl -sS -H "Authorization: token ${MOKOGIT_TOKEN}" \ - "${API}/actions/runs?status=completed&limit=50" | \ - jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null) - - DELETED=0 - for RUN_ID in $RUNS; do - curl -sS -X DELETE -H "Authorization: token ${MOKOGIT_TOKEN}" \ - "${API}/actions/runs/${RUN_ID}" 2>/dev/null || true - DELETED=$((DELETED + 1)) - done - - echo "Deleted ${DELETED} old workflow run(s)" diff --git a/.mokogit/workflows/deploy-dev.yml b/.mokogit/workflows/deploy-dev.yml deleted file mode 100644 index 1d983a73ef..0000000000 --- a/.mokogit/workflows/deploy-dev.yml +++ /dev/null @@ -1,136 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# SPDX-License-Identifier: GPL-3.0-or-later -# BRIEF: Build and deploy to the Dev environment on push to the dev branch. -# Portable across Go server repos: ALL deployment config comes from repo -# Actions variables (vars.*) and secrets (secrets.*), nothing hardcoded. -# The dev branch is the ongoing integration branch. -# OWNER: Template-Go (canonical source; syncs to the root workflows dir). See Template-Go#3. -# -# Required repo VARIABLES (all tier-scoped so each environment is independent — -# a repo may host its rc/dev/prod tiers on different machines): -# DEV_SSH_HOST, DEV_SSH_PORT, DEV_SSH_USERNAME - SSH deploy target for the dev tier -# DEV_REGISTRY, DEV_REGISTRY_USER, DEV_IMAGE - container registry + login user + image -# DEV_CONTAINER - compose service/container to recreate -# DEV_COMPOSE_PROJECT - docker compose -p project name -# DEV_COMPOSE_DIR - dir containing docker-compose.yml on host -# DEV_SOURCE_DIR - build source checkout on host -# DEV_TAG_ENV - compose env-var name that pins the image tag -# DEV_HEALTH_URL - external URL to verify after deploy -# Required SECRETS (already configured org-wide; reused, not re-set): -# DEPLOY_SSH_KEY - deploy private key (repo/org secret) -# MOKOGIT_TOKEN - registry/API token (org secret) - -name: Deploy (Dev) - -on: - push: - branches: - - dev - # Manual trigger for isolated end-to-end tests. - # Runs on the ref it is dispatched from. - workflow_dispatch: - -concurrency: - group: deploy-dev - cancel-in-progress: true - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - deploy-dev: - name: "Build & Deploy to Dev" - runs-on: ubuntu-latest - steps: - - name: Checkout source - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Determine version - id: config - run: | - VERSION=$(git describe --tags --always 2>/dev/null || echo "dev-$(git rev-parse --short HEAD)") - echo "tag=${VERSION}-dev" >> $GITHUB_OUTPUT - echo "Version: ${VERSION}-dev" - - - name: Write deploy key - env: - DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }} - run: | - mkdir -p ~/.ssh - echo "$DEPLOY_KEY" > ~/.ssh/deploy_key - chmod 600 ~/.ssh/deploy_key - - - name: Build and deploy to RC via SSH - env: - REGISTRY_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} - TAG: ${{ steps.config.outputs.tag }} - run: | - # Runner-side values (TAG, REGISTRY_TOKEN) are injected into the remote shell - # via an env prefix; a *quoted* heredoc keeps every $var expanding once, on the - # remote. Repo variables (vars.*) are substituted inline by Actions before ssh. - ssh -i ~/.ssh/deploy_key -p ${{ vars.DEV_SSH_PORT }} \ - -o ConnectTimeout=30 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ - -o ServerAliveInterval=30 -o ServerAliveCountMax=10 \ - ${{ vars.DEV_SSH_USERNAME }}@${{ vars.DEV_SSH_HOST }} \ - "TAG='$TAG' REGISTRY_TOKEN='$REGISTRY_TOKEN' bash -s" <<'DEPLOY_EOF' - set -e - echo 'SSH connected to Dev environment' - - if [ -z "$TAG" ]; then - echo 'ERROR: TAG is empty; refusing to build an untagged image' >&2 - exit 1 - fi - - HEALTH_FMT='{{.State.Health.Status}}' - - echo 'Cleaning Docker build cache...' - docker builder prune -af 2>/dev/null || true - docker image prune -af 2>/dev/null || true - - echo 'Pulling source...' - SOURCE_DIR='${{ vars.DEV_SOURCE_DIR }}' - if [ ! -d "$SOURCE_DIR/.git" ]; then - git clone -b dev https://x-access-token:${REGISTRY_TOKEN}@git.mokoconsulting.tech/${{ github.repository }}.git "$SOURCE_DIR" - fi - cd "$SOURCE_DIR" - git remote set-url origin https://x-access-token:${REGISTRY_TOKEN}@git.mokoconsulting.tech/${{ github.repository }}.git 2>/dev/null || true - git fetch origin dev - git reset --hard origin/dev - - echo "Building image: ${{ vars.DEV_REGISTRY }}/${{ vars.DEV_IMAGE }}:$TAG" - docker build --no-cache --build-arg GOFLAGS='-p 1' \ - --tag "${{ vars.DEV_REGISTRY }}/${{ vars.DEV_IMAGE }}:$TAG" \ - -f Dockerfile . - - echo 'Pushing to registry...' - echo "$REGISTRY_TOKEN" | docker login ${{ vars.DEV_REGISTRY }} -u ${{ vars.DEV_REGISTRY_USER }} --password-stdin - docker push "${{ vars.DEV_REGISTRY }}/${{ vars.DEV_IMAGE }}:$TAG" - - echo 'Restarting Dev container...' - cd '${{ vars.DEV_COMPOSE_DIR }}' - # Drive the rc service image tag via its compose env-var (no sed on the shared - # file); remove any lingering fixed-name container first, then force-recreate. - docker rm -f '${{ vars.DEV_CONTAINER }}' 2>/dev/null || true - ${{ vars.DEV_TAG_ENV }}="$TAG" docker compose -p '${{ vars.DEV_COMPOSE_PROJECT }}' up -d --force-recreate '${{ vars.DEV_CONTAINER }}' - - echo 'Health check...' - for i in 1 2 3 4 5 6 7 8; do - sleep 15 - if docker inspect --format="$HEALTH_FMT" '${{ vars.DEV_CONTAINER }}' 2>/dev/null | grep -q healthy; then - echo 'Dev container healthy!' - exit 0 - fi - echo "Waiting... (attempt $i/8)" - done - echo 'Health check failed' - docker logs '${{ vars.DEV_CONTAINER }}' --tail 20 - exit 1 - DEPLOY_EOF - - - name: Verify Dev instance - continue-on-error: true - run: | - sleep 5 - ssh -i ~/.ssh/deploy_key -p ${{ vars.DEV_SSH_PORT }} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${{ vars.DEV_SSH_USERNAME }}@${{ vars.DEV_SSH_HOST }} "curl -sf '${{ vars.DEV_HEALTH_URL }}'" && echo " Dev API healthy" diff --git a/.mokogit/workflows/deploy-prod.yml b/.mokogit/workflows/deploy-prod.yml deleted file mode 100644 index 4299b40004..0000000000 --- a/.mokogit/workflows/deploy-prod.yml +++ /dev/null @@ -1,136 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# SPDX-License-Identifier: GPL-3.0-or-later -# BRIEF: Build and deploy to the Prod environment on push to the main branch. -# Portable across Go server repos: ALL deployment config comes from repo -# Actions variables (vars.*) and secrets (secrets.*), nothing hardcoded. -# Prod deploys on merge to main (dev -> rc -> main pipeline). -# OWNER: Template-Go (canonical source; syncs to the root workflows dir). See Template-Go#3. -# -# Required repo VARIABLES (all tier-scoped so each environment is independent — -# a repo may host its rc/dev/prod tiers on different machines): -# PROD_SSH_HOST, PROD_SSH_PORT, PROD_SSH_USERNAME - SSH deploy target for the prod tier -# PROD_REGISTRY, PROD_REGISTRY_USER, PROD_IMAGE - container registry + login user + image -# PROD_CONTAINER - compose service/container to recreate -# PROD_COMPOSE_PROJECT - docker compose -p project name -# PROD_COMPOSE_DIR - dir containing docker-compose.yml on host -# PROD_SOURCE_DIR - build source checkout on host -# PROD_TAG_ENV - compose env-var name that pins the image tag -# PROD_HEALTH_URL - external URL to verify after deploy -# Required SECRETS (already configured org-wide; reused, not re-set): -# DEPLOY_SSH_KEY - deploy private key (repo/org secret) -# MOKOGIT_TOKEN - registry/API token (org secret) - -name: Deploy (Prod) - -on: - push: - branches: - - main - # Manual trigger for a prod re-deploy. - # Runs on the ref it is dispatched from (use main). - workflow_dispatch: - -concurrency: - group: deploy-prod - cancel-in-progress: false - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - deploy-prod: - name: "Build & Deploy to Prod" - runs-on: ubuntu-latest - steps: - - name: Checkout source - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Determine version - id: config - run: | - VERSION=$(git describe --tags --always 2>/dev/null || echo "$(git rev-parse --short HEAD)") - echo "tag=${VERSION}" >> $GITHUB_OUTPUT - echo "Version: ${VERSION}" - - - name: Write deploy key - env: - DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }} - run: | - mkdir -p ~/.ssh - echo "$DEPLOY_KEY" > ~/.ssh/deploy_key - chmod 600 ~/.ssh/deploy_key - - - name: Build and deploy to RC via SSH - env: - REGISTRY_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} - TAG: ${{ steps.config.outputs.tag }} - run: | - # Runner-side values (TAG, REGISTRY_TOKEN) are injected into the remote shell - # via an env prefix; a *quoted* heredoc keeps every $var expanding once, on the - # remote. Repo variables (vars.*) are substituted inline by Actions before ssh. - ssh -i ~/.ssh/deploy_key -p ${{ vars.PROD_SSH_PORT }} \ - -o ConnectTimeout=30 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ - -o ServerAliveInterval=30 -o ServerAliveCountMax=10 \ - ${{ vars.PROD_SSH_USERNAME }}@${{ vars.PROD_SSH_HOST }} \ - "TAG='$TAG' REGISTRY_TOKEN='$REGISTRY_TOKEN' bash -s" <<'DEPLOY_EOF' - set -e - echo 'SSH connected to Prod environment' - - if [ -z "$TAG" ]; then - echo 'ERROR: TAG is empty; refusing to build an untagged image' >&2 - exit 1 - fi - - HEALTH_FMT='{{.State.Health.Status}}' - - echo 'Cleaning Docker build cache...' - docker builder prune -af 2>/dev/null || true - docker image prune -af 2>/dev/null || true - - echo 'Pulling source...' - SOURCE_DIR='${{ vars.PROD_SOURCE_DIR }}' - if [ ! -d "$SOURCE_DIR/.git" ]; then - git clone -b main https://x-access-token:${REGISTRY_TOKEN}@git.mokoconsulting.tech/${{ github.repository }}.git "$SOURCE_DIR" - fi - cd "$SOURCE_DIR" - git remote set-url origin https://x-access-token:${REGISTRY_TOKEN}@git.mokoconsulting.tech/${{ github.repository }}.git 2>/dev/null || true - git fetch origin main - git reset --hard origin/main - - echo "Building image: ${{ vars.PROD_REGISTRY }}/${{ vars.PROD_IMAGE }}:$TAG" - docker build --no-cache --build-arg GOFLAGS='-p 1' \ - --tag "${{ vars.PROD_REGISTRY }}/${{ vars.PROD_IMAGE }}:$TAG" \ - -f Dockerfile . - - echo 'Pushing to registry...' - echo "$REGISTRY_TOKEN" | docker login ${{ vars.PROD_REGISTRY }} -u ${{ vars.PROD_REGISTRY_USER }} --password-stdin - docker push "${{ vars.PROD_REGISTRY }}/${{ vars.PROD_IMAGE }}:$TAG" - - echo 'Restarting Prod container...' - cd '${{ vars.PROD_COMPOSE_DIR }}' - # Drive the rc service image tag via its compose env-var (no sed on the shared - # file); remove any lingering fixed-name container first, then force-recreate. - docker rm -f '${{ vars.PROD_CONTAINER }}' 2>/dev/null || true - ${{ vars.PROD_TAG_ENV }}="$TAG" docker compose -p '${{ vars.PROD_COMPOSE_PROJECT }}' up -d --force-recreate '${{ vars.PROD_CONTAINER }}' - - echo 'Health check...' - for i in 1 2 3 4 5 6 7 8; do - sleep 15 - if docker inspect --format="$HEALTH_FMT" '${{ vars.PROD_CONTAINER }}' 2>/dev/null | grep -q healthy; then - echo 'Prod container healthy!' - exit 0 - fi - echo "Waiting... (attempt $i/8)" - done - echo 'Health check failed' - docker logs '${{ vars.PROD_CONTAINER }}' --tail 20 - exit 1 - DEPLOY_EOF - - - name: Verify Prod instance - continue-on-error: true - run: | - sleep 5 - ssh -i ~/.ssh/deploy_key -p ${{ vars.PROD_SSH_PORT }} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${{ vars.PROD_SSH_USERNAME }}@${{ vars.PROD_SSH_HOST }} "curl -sf '${{ vars.PROD_HEALTH_URL }}'" && echo " Prod API healthy" diff --git a/.mokogit/workflows/deploy-rc.yml b/.mokogit/workflows/deploy-rc.yml deleted file mode 100644 index 26bda399ee..0000000000 --- a/.mokogit/workflows/deploy-rc.yml +++ /dev/null @@ -1,138 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# SPDX-License-Identifier: GPL-3.0-or-later -# BRIEF: Build and deploy to the RC environment on push to the rc branch. -# Portable across Go server repos: ALL deployment config comes from repo -# Actions variables (vars.*) and secrets (secrets.*), nothing hardcoded. -# The rc branch is created by promote-rc when a PR to main opens. -# OWNER: Template-Go (canonical source; syncs to the root workflows dir). See Template-Go#3. -# -# Required repo VARIABLES (all tier-scoped so each environment is independent — -# a repo may host its rc/dev/prod tiers on different machines): -# RC_SSH_HOST, RC_SSH_PORT, RC_SSH_USERNAME - SSH deploy target for the rc tier -# RC_REGISTRY, RC_REGISTRY_USER, RC_IMAGE - container registry + login user + image -# RC_CONTAINER - compose service/container to recreate -# RC_COMPOSE_PROJECT - docker compose -p project name -# RC_COMPOSE_DIR - dir containing docker-compose.yml on host -# RC_SOURCE_DIR - build source checkout on host -# RC_TAG_ENV - compose env-var name that pins the image tag -# RC_HEALTH_URL - external URL to verify after deploy -# Required SECRETS (already configured org-wide; reused, not re-set): -# DEPLOY_SSH_KEY - deploy private key (repo/org secret) -# MOKOGIT_TOKEN - registry/API token (org secret) - -name: Deploy (RC) - -on: - push: - branches: - - rc - # Manual trigger for isolated end-to-end tests without a full RC promotion. - # Runs on the ref it is dispatched from; that ref must carry current source - # (>= the RC database migration version) or the rebuilt image will refuse the - # newer DB. Dispatch from `rc` once `rc` is current. - workflow_dispatch: - -concurrency: - group: deploy-rc - cancel-in-progress: true - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - deploy-rc: - name: "Build & Deploy to RC" - runs-on: ubuntu-latest - steps: - - name: Checkout source - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Determine version - id: config - run: | - VERSION=$(git describe --tags --always 2>/dev/null || echo "rc-$(git rev-parse --short HEAD)") - echo "tag=${VERSION}-rc" >> $GITHUB_OUTPUT - echo "Version: ${VERSION}-rc" - - - name: Write deploy key - env: - DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }} - run: | - mkdir -p ~/.ssh - echo "$DEPLOY_KEY" > ~/.ssh/deploy_key - chmod 600 ~/.ssh/deploy_key - - - name: Build and deploy to RC via SSH - env: - REGISTRY_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} - TAG: ${{ steps.config.outputs.tag }} - run: | - # Runner-side values (TAG, REGISTRY_TOKEN) are injected into the remote shell - # via an env prefix; a *quoted* heredoc keeps every $var expanding once, on the - # remote. Repo variables (vars.*) are substituted inline by Actions before ssh. - ssh -i ~/.ssh/deploy_key -p ${{ vars.RC_SSH_PORT }} \ - -o ConnectTimeout=30 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ - -o ServerAliveInterval=30 -o ServerAliveCountMax=10 \ - ${{ vars.RC_SSH_USERNAME }}@${{ vars.RC_SSH_HOST }} \ - "TAG='$TAG' REGISTRY_TOKEN='$REGISTRY_TOKEN' bash -s" <<'DEPLOY_EOF' - set -e - echo 'SSH connected to RC environment' - - if [ -z "$TAG" ]; then - echo 'ERROR: TAG is empty; refusing to build an untagged image' >&2 - exit 1 - fi - - HEALTH_FMT='{{.State.Health.Status}}' - - echo 'Cleaning Docker build cache...' - docker builder prune -af 2>/dev/null || true - docker image prune -af 2>/dev/null || true - - echo 'Pulling source...' - SOURCE_DIR='${{ vars.RC_SOURCE_DIR }}' - if [ ! -d "$SOURCE_DIR/.git" ]; then - git clone -b rc https://x-access-token:${REGISTRY_TOKEN}@git.mokoconsulting.tech/${{ github.repository }}.git "$SOURCE_DIR" - fi - cd "$SOURCE_DIR" - git remote set-url origin https://x-access-token:${REGISTRY_TOKEN}@git.mokoconsulting.tech/${{ github.repository }}.git 2>/dev/null || true - git fetch origin rc - git reset --hard origin/rc - - echo "Building image: ${{ vars.RC_REGISTRY }}/${{ vars.RC_IMAGE }}:$TAG" - docker build --no-cache --build-arg GOFLAGS='-p 1' \ - --tag "${{ vars.RC_REGISTRY }}/${{ vars.RC_IMAGE }}:$TAG" \ - -f Dockerfile . - - echo 'Pushing to registry...' - echo "$REGISTRY_TOKEN" | docker login ${{ vars.RC_REGISTRY }} -u ${{ vars.RC_REGISTRY_USER }} --password-stdin - docker push "${{ vars.RC_REGISTRY }}/${{ vars.RC_IMAGE }}:$TAG" - - echo 'Restarting RC container...' - cd '${{ vars.RC_COMPOSE_DIR }}' - # Drive the rc service image tag via its compose env-var (no sed on the shared - # file); remove any lingering fixed-name container first, then force-recreate. - docker rm -f '${{ vars.RC_CONTAINER }}' 2>/dev/null || true - ${{ vars.RC_TAG_ENV }}="$TAG" docker compose -p '${{ vars.RC_COMPOSE_PROJECT }}' up -d --force-recreate '${{ vars.RC_CONTAINER }}' - - echo 'Health check...' - for i in 1 2 3 4 5 6 7 8; do - sleep 15 - if docker inspect --format="$HEALTH_FMT" '${{ vars.RC_CONTAINER }}' 2>/dev/null | grep -q healthy; then - echo 'RC container healthy!' - exit 0 - fi - echo "Waiting... (attempt $i/8)" - done - echo 'Health check failed' - docker logs '${{ vars.RC_CONTAINER }}' --tail 20 - exit 1 - DEPLOY_EOF - - - name: Verify RC instance - continue-on-error: true - run: | - sleep 5 - ssh -i ~/.ssh/deploy_key -p ${{ vars.RC_SSH_PORT }} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${{ vars.RC_SSH_USERNAME }}@${{ vars.RC_SSH_HOST }} "curl -sf '${{ vars.RC_HEALTH_URL }}'" && echo " RC API healthy" diff --git a/.mokogit/workflows/gitleaks.yml b/.mokogit/workflows/gitleaks.yml deleted file mode 100644 index 685580fb01..0000000000 --- a/.mokogit/workflows/gitleaks.yml +++ /dev/null @@ -1,92 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: MokoGit.Workflow -# INGROUP: MokoStandards.Security -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API -# PATH: /templates/workflows/gitleaks.yml.template -# VERSION: 01.00.00 -# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens -# -# +========================================================================+ -# | SECRET SCANNING | -# +========================================================================+ -# | | -# | Scans commits for leaked secrets using Gitleaks. | -# | | -# | - PR scan: only new commits in the PR | -# | - Scheduled: full repo scan weekly | -# | - Alerts via ntfy on findings | -# | | -# +========================================================================+ - -name: "Universal: Secret Scanning" - -on: - schedule: - - cron: '0 5 * * 1' # Weekly Monday 05:00 UTC - workflow_dispatch: - -permissions: - contents: read - -env: - NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }} - NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'git-security' }} - -jobs: - gitleaks: - name: Gitleaks Secret Scan - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Install Gitleaks - run: | - GITLEAKS_VERSION="8.21.2" - curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \ - | tar -xz -C /usr/local/bin gitleaks - gitleaks version - - - name: Scan for secrets - id: scan - run: | - echo "### Secret Scanning" >> $GITHUB_STEP_SUMMARY - ARGS="--source . --verbose --report-format json --report-path /tmp/gitleaks-report.json" - - if [ "${{ github.event_name }}" = "pull_request" ]; then - # Scan only PR commits - ARGS="$ARGS --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}" - echo "Scanning PR commits only" >> $GITHUB_STEP_SUMMARY - else - echo "Full repository scan" >> $GITHUB_STEP_SUMMARY - fi - - if gitleaks detect $ARGS 2>&1; then - echo "result=clean" >> "$GITHUB_OUTPUT" - echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY - else - echo "result=found" >> "$GITHUB_OUTPUT" - FINDINGS=$(jq length /tmp/gitleaks-report.json 2>/dev/null || echo "unknown") - echo "**${FINDINGS} potential secret(s) detected.**" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Review the findings and rotate any exposed credentials immediately." >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - - name: Notify on findings - if: failure() && steps.scan.outputs.result == 'found' - run: | - REPO="${{ github.event.repository.name }}" - curl -sS \ - -H "Title: ${REPO} — secrets detected in code" \ - -H "Tags: rotating_light,key" \ - -H "Priority: urgent" \ - -d "Gitleaks found potential secrets. Review and rotate credentials immediately." \ - "${NTFY_URL}/${NTFY_TOPIC}" || true diff --git a/.mokogit/workflows/issue-branch.yml b/.mokogit/workflows/issue-branch.yml deleted file mode 100644 index e736ca81cb..0000000000 --- a/.mokogit/workflows/issue-branch.yml +++ /dev/null @@ -1,81 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: MokoGIT.Workflow -# INGROUP: MokoCLI.Automation -# VERSION: 01.00.00 -# BRIEF: Auto-create feature branch when an issue is opened - -name: "Universal: Issue Branch" - -on: - issues: - types: [opened] - -permissions: - contents: write - issues: write - -env: - MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - -jobs: - create-branch: - name: Create feature branch - runs-on: ubuntu-latest - steps: - - name: Create branch and comment - # SECURITY: never interpolate github.event.* into a run: script — the - # Actions engine splices the raw value into the shell source (command - # injection via a crafted issue title). Pass everything through env so - # the values arrive as ordinary shell variables that are not re-parsed. - env: - MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} - REPO: ${{ github.repository }} - ISSUE_NUM: ${{ github.event.issue.number }} - ISSUE_TITLE: ${{ github.event.issue.title }} - run: | - TOKEN="${MOKOGIT_TOKEN}" - API="${MOKOGIT_URL}/api/v1/repos/${REPO}" - - # Build slug from title: lowercase, replace non-alnum with dash, trim. - # printf (not echo) so a title beginning with "-" is not read as flags. - SLUG=$(printf '%s' "${ISSUE_TITLE}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//' | cut -c1-40) - BRANCH="feature/${ISSUE_NUM}-${SLUG}" - - # Check dev branch exists - DEV_EXISTS=$(curl -sf -o /dev/null -w '%{http_code}' \ - -H "Authorization: token ${TOKEN}" \ - "${API}/branches/dev" 2>/dev/null || echo "000") - - if [ "${DEV_EXISTS}" != "200" ]; then - echo "No dev branch -- skipping" - exit 0 - fi - - # Create branch from dev - HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \ - -H "Authorization: token ${TOKEN}" \ - -H "Content-Type: application/json" \ - "${API}/branches" \ - -d "{\"new_branch_name\":\"${BRANCH}\",\"old_branch_name\":\"dev\"}" 2>/dev/null || echo "000") - - if [ "${HTTP}" = "201" ]; then - echo "Created branch: ${BRANCH}" - - # Comment on issue with branch link - REPO_URL="${MOKOGIT_URL}/${REPO}" - BODY="Branch created: [\`${BRANCH}\`](${REPO_URL}/src/branch/${BRANCH})\n\n\`\`\`bash\ngit fetch origin\ngit checkout ${BRANCH}\n\`\`\`" - - curl -sf -X POST \ - -H "Authorization: token ${TOKEN}" \ - -H "Content-Type: application/json" \ - "${API}/issues/${ISSUE_NUM}/comments" \ - -d "{\"body\":\"${BODY}\"}" > /dev/null 2>&1 - - echo "Commented on issue #${ISSUE_NUM}" - else - echo "Failed to create branch (HTTP ${HTTP}) -- may already exist" - fi diff --git a/.mokogit/workflows/notify.yml b/.mokogit/workflows/notify.yml deleted file mode 100644 index 4b9ee68ae6..0000000000 --- a/.mokogit/workflows/notify.yml +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: MokoGit.Workflow -# INGROUP: MokoStandards.Notifications -# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards -# PATH: /.gitea/workflows/notify.yml -# VERSION: 01.00.00 -# BRIEF: Push notifications via ntfy on release success or workflow failure - -name: "Universal: Notifications" - -on: - workflow_run: - workflows: - - "Joomla Build & Release" - - "Joomla Extension CI" - - "Deploy" - types: - - completed - -permissions: - contents: read - -env: - NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }} - NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'git-releases' }} - -jobs: - notify: - name: Send Notification - runs-on: ubuntu-latest - if: >- - github.event.workflow_run.conclusion == 'success' || - github.event.workflow_run.conclusion == 'failure' - - steps: - - name: Notify on success (releases only) - if: >- - github.event.workflow_run.conclusion == 'success' && - contains(github.event.workflow_run.name, 'Release') - run: | - REPO="${{ github.event.repository.name }}" - WORKFLOW="${{ github.event.workflow_run.name }}" - URL="${{ github.event.workflow_run.html_url }}" - - curl -sS \ - -H "Title: ${REPO} released" \ - -H "Tags: white_check_mark,package" \ - -H "Priority: default" \ - -H "Click: ${URL}" \ - -d "${WORKFLOW} completed successfully." \ - "${NTFY_URL}/${NTFY_TOPIC}" - - - name: Notify on failure - if: github.event.workflow_run.conclusion == 'failure' - run: | - REPO="${{ github.event.repository.name }}" - WORKFLOW="${{ github.event.workflow_run.name }}" - URL="${{ github.event.workflow_run.html_url }}" - - curl -sS \ - -H "Title: ${REPO} workflow failed" \ - -H "Tags: x,warning" \ - -H "Priority: high" \ - -H "Click: ${URL}" \ - -d "${WORKFLOW} failed. Check the run for details." \ - "${NTFY_URL}/${NTFY_TOPIC}" diff --git a/.mokogit/workflows/pr-check.yml b/.mokogit/workflows/pr-check.yml deleted file mode 100644 index 8554b09eb8..0000000000 --- a/.mokogit/workflows/pr-check.yml +++ /dev/null @@ -1,522 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: MokoGit.Workflow -# INGROUP: mokocli.CI -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli -# PATH: /templates/workflows/universal/pr-check.yml.template -# VERSION: 09.23.00 -# BRIEF: PR gate — branch policy + code validation before merge - -name: "Universal: PR Check" - -on: - pull_request: - types: [opened, synchronize, reopened, edited] - -permissions: - contents: read - pull-requests: write - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - # ── Branch Policy ────────────────────────────────────────────────────── - branch-policy: - name: Branch Policy - runs-on: ubuntu-latest - steps: - - name: Check branch merge target - run: | - HEAD="${{ github.head_ref }}" - BASE="${{ github.base_ref }}" - - echo "PR: ${HEAD} → ${BASE}" - - ALLOWED=true - REASON="" - - case "$HEAD" in - feature/*|feat/*) - if [ "$BASE" != "dev" ]; then - ALLOWED=false - REASON="Feature branches must target 'dev', not '${BASE}'" - fi - ;; - fix/*|bugfix/*) - if [ "$BASE" != "dev" ]; then - ALLOWED=false - REASON="Fix branches must target 'dev', not '${BASE}'" - fi - ;; - patch/*) - if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then - ALLOWED=false - REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'" - fi - ;; - hotfix/*) - if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then - ALLOWED=false - REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'" - fi - ;; - rc) - if [ "$BASE" != "main" ]; then - ALLOWED=false - REASON="RC branch can only merge into 'main', not '${BASE}'" - fi - ;; - dev) - if [ "$BASE" != "main" ]; then - ALLOWED=false - REASON="Dev branch can only merge into 'main', not '${BASE}'" - fi - ;; - esac - - if [ "$ALLOWED" = false ]; then - echo "::error::${REASON}" - echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "${REASON}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY - echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY - echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY - echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY - echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY - echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - echo "Branch policy: OK (${HEAD} → ${BASE})" - echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY - - # ── Secret Scanning ────────────────────────────────────────────────── - gitleaks: - name: Secret Scan - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Install Gitleaks - run: | - GITLEAKS_VERSION="8.21.2" - curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \ - | tar -xz -C /usr/local/bin gitleaks - - - name: Scan PR commits for secrets - run: | - if gitleaks detect --source . --verbose \ - --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} 2>&1; then - echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY - else - echo "::error::Potential secrets detected in PR commits" - exit 1 - fi - - # ── Code Validation ──────────────────────────────────────────────────── - validate: - name: Validate PR - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Check for merge conflict markers - run: | - CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true) - if [ -n "$CONFLICTS" ]; then - echo "::error::Merge conflict markers found in source files" - echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - exit 1 - fi - echo "No conflict markers found" - - - name: Detect platform - id: platform - run: | - # Platform comes from the MokoGit metadata API (public GET). - API="${GITHUB_SERVER_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${GITHUB_REPOSITORY}/metadata" - PLATFORM="$(curl -sf "$API" 2>/dev/null | python3 -c "import sys, json; print(json.load(sys.stdin).get('platform') or '')" 2>/dev/null || true)" - [ -z "$PLATFORM" ] && PLATFORM="generic" - echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" - echo "Detected platform: $PLATFORM" - - - name: Setup PHP - if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' - run: | - if ! command -v php &> /dev/null; then - sudo apt-get update -qq - sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1 - fi - - - name: PHP syntax check - if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' - run: | - ERRORS=0 - while IFS= read -r -d '' file; do - if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then - ERRORS=$((ERRORS + 1)) - fi - done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0) - echo "PHP lint: ${ERRORS} error(s)" - [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; } - - - name: Joomla JEXEC guard check - if: steps.platform.outputs.platform == 'joomla' - run: | - ERRORS=0 - while IFS= read -r -d '' file; do - # Skip vendor, node_modules, and index.html stub files - case "$file" in ./vendor/*|./node_modules/*) continue ;; esac - # Check first 10 lines for JEXEC or JPATH guard - if ! head -20 "$file" | grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]"; then - echo "::error file=${file}::Missing JEXEC guard: ${file}" - ERRORS=$((ERRORS + 1)) - fi - done < <(find . -name "*.php" -path "*/src/*" -not -path "./.git/*" -not -path "./vendor/*" -print0) - if [ "$ERRORS" -gt 0 ]; then - echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard" - echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY - echo "${ERRORS} file(s) in src/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY - exit 1 - fi - echo "JEXEC guard: OK" - - - name: Joomla directory listing protection - if: steps.platform.outputs.platform == 'joomla' - run: | - MISSING=0 - SOURCE_DIR="src" - [ ! -d "$SOURCE_DIR" ] && exit 0 - while IFS= read -r dir; do - if [ ! -f "${dir}/index.html" ]; then - echo "::warning::Missing index.html in ${dir} (directory listing protection)" - MISSING=$((MISSING + 1)) - fi - done < <(find "$SOURCE_DIR" -type d -not -path "./.git/*" -not -path "*/vendor/*" -not -path "*/node_modules/*") - if [ "$MISSING" -gt 0 ]; then - echo "## Directory Protection" >> $GITHUB_STEP_SUMMARY - echo "${MISSING} director(ies) missing index.html" >> $GITHUB_STEP_SUMMARY - fi - echo "Directory protection: ${MISSING} missing (advisory)" - - - name: Joomla script file and asset checks - if: steps.platform.outputs.platform == 'joomla' - run: | - ERRORS=0 - MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) - [ -z "$MANIFEST" ] && exit 0 - MANIFEST_DIR=$(dirname "$MANIFEST") - - # Check scriptfile exists if declared - SCRIPTFILE=$(sed -n 's/.*\([^<]*\)<\/scriptfile>.*/\1/p' "$MANIFEST" 2>/dev/null) - if [ -n "$SCRIPTFILE" ]; then - if [ ! -f "${MANIFEST_DIR}/${SCRIPTFILE}" ]; then - echo "::error::Manifest declares ${SCRIPTFILE} but file not found at ${MANIFEST_DIR}/${SCRIPTFILE}" - ERRORS=$((ERRORS + 1)) - else - echo "Script file: ${MANIFEST_DIR}/${SCRIPTFILE} (OK)" - fi - fi - - # Require joomla.asset.json and validate it - ASSET_JSON=$(find "$MANIFEST_DIR" -name "joomla.asset.json" -not -path "./.git/*" 2>/dev/null | head -1) - if [ -z "$ASSET_JSON" ]; then - echo "::error::joomla.asset.json not found — Joomla asset system is required" - ERRORS=$((ERRORS + 1)) - else - if command -v php &> /dev/null; then - php -r "json_decode(file_get_contents('$ASSET_JSON')); if(json_last_error()!==JSON_ERROR_NONE){echo json_last_error_msg();exit(1);}" 2>&1 || { - echo "::error::joomla.asset.json is not valid JSON" - ERRORS=$((ERRORS + 1)) - } - fi - echo "joomla.asset.json: valid" - fi - - # Validate all XML files in src/ are well-formed - XML_ERRORS=0 - if command -v php &> /dev/null; then - while IFS= read -r -d '' xmlfile; do - if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$xmlfile'); if(!\$x){foreach(libxml_get_errors() as \$e) echo trim(\$e->message) . ' in $xmlfile'; exit(1);}" 2>&1; then - XML_ERRORS=$((XML_ERRORS + 1)) - fi - done < <(find "$MANIFEST_DIR" -name "*.xml" -not -path "./.git/*" -print0) - fi - if [ "$XML_ERRORS" -gt 0 ]; then - echo "::error::${XML_ERRORS} XML file(s) are malformed" - ERRORS=$((ERRORS + 1)) - else - echo "XML well-formedness: OK" - fi - - [ "$ERRORS" -gt 0 ] && exit 1 - echo "Joomla asset checks: OK" - - - name: Validate platform manifest - run: | - PLATFORM="${{ steps.platform.outputs.platform }}" - case "$PLATFORM" in - joomla) - MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) - if [ -z "$MANIFEST" ]; then - echo "::warning::No Joomla manifest found (WaaS site)" - exit 0 - fi - echo "Manifest: ${MANIFEST}" - if command -v php &> /dev/null; then - php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; } - fi - for ELEMENT in name version description; do - grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; } - done - # Block legacy raw/branch update server URLs on MokoGit - RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogit\|git\.mokoconsulting\.tech' || true) - if [ -n "$RAW_URLS" ]; then - echo "::error::Manifest contains legacy raw/branch update server URL on MokoGit. Use the MokoGit Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)" - echo "$RAW_URLS" - exit 1 - fi - echo "Joomla manifest valid" - ;; - dolibarr) - MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1) - if [ -z "$MOD_FILE" ]; then - echo "::error::No mod*.class.php found" - exit 1 - fi - echo "Dolibarr module: ${MOD_FILE}" - ;; - *) - echo "Generic platform — no manifest validation" - ;; - esac - - - name: Check update stream format - run: | - PLATFORM="${{ steps.platform.outputs.platform }}" - case "$PLATFORM" in - joomla) - if [ -f "updates.xml" ]; then - if command -v php &> /dev/null; then - php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; } - fi - echo "updates.xml valid" - fi - ;; - dolibarr) - [ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt" - ;; - esac - - - name: Validate Joomla language files - if: steps.platform.outputs.platform == 'joomla' - run: | - ERRORS=0 - WARNINGS=0 - - # Require both en-GB and en-US language directories - LANG_ROOT=$(find . -path "*/language" -type d -not -path "./.git/*" 2>/dev/null | head -1) - if [ -z "$LANG_ROOT" ]; then - echo "No language/ directory found — skipping" - exit 0 - fi - - if [ ! -d "$LANG_ROOT/en-GB" ]; then - echo "::error::Missing en-GB language directory (${LANG_ROOT}/en-GB)" - ERRORS=$((ERRORS + 1)) - fi - if [ ! -d "$LANG_ROOT/en-US" ]; then - echo "::error::Missing en-US language directory (${LANG_ROOT}/en-US)" - ERRORS=$((ERRORS + 1)) - fi - - # Check that en-GB and en-US have matching .ini files - if [ -d "$LANG_ROOT/en-GB" ] && [ -d "$LANG_ROOT/en-US" ]; then - for GB_INI in "$LANG_ROOT/en-GB"/*.ini; do - [ ! -f "$GB_INI" ] && continue - US_INI="$LANG_ROOT/en-US/$(basename "$GB_INI")" - if [ ! -f "$US_INI" ]; then - echo "::error::$(basename "$GB_INI") exists in en-GB but missing from en-US" - ERRORS=$((ERRORS + 1)) - fi - done - for US_INI in "$LANG_ROOT/en-US"/*.ini; do - [ ! -f "$US_INI" ] && continue - GB_INI="$LANG_ROOT/en-GB/$(basename "$US_INI")" - if [ ! -f "$GB_INI" ]; then - echo "::error::$(basename "$US_INI") exists in en-US but missing from en-GB" - ERRORS=$((ERRORS + 1)) - fi - done - fi - - # Find all .ini language files - INI_FILES=$(find . -path "*/language/*/*.ini" -not -path "./.git/*" 2>/dev/null) - if [ -z "$INI_FILES" ]; then - echo "No .ini language files found" - [ "$ERRORS" -gt 0 ] && exit 1 - exit 0 - fi - - echo "Found $(echo "$INI_FILES" | wc -l) language file(s)" - - for FILE in $INI_FILES; do - FNAME=$(basename "$FILE") - LINENUM=0 - SEEN_KEYS="" - - while IFS= read -r line || [ -n "$line" ]; do - LINENUM=$((LINENUM + 1)) - - # Skip empty lines and comments - [ -z "$line" ] && continue - echo "$line" | grep -qE '^\s*;' && continue - echo "$line" | grep -qE '^\s*$' && continue - - # Must match KEY="VALUE" format - if ! echo "$line" | grep -qE '^[A-Z_][A-Z0-9_]*=".*"$'; then - echo "::error file=${FILE},line=${LINENUM}::Malformed line: ${line}" - ERRORS=$((ERRORS + 1)) - continue - fi - - # Extract key and check for duplicates - KEY=$(echo "$line" | sed 's/=.*//') - if echo "$SEEN_KEYS" | grep -qx "$KEY"; then - echo "::error file=${FILE},line=${LINENUM}::Duplicate key: ${KEY}" - ERRORS=$((ERRORS + 1)) - fi - SEEN_KEYS="${SEEN_KEYS} - ${KEY}" - done < "$FILE" - - echo " ${FILE}: checked ${LINENUM} lines" - done - - # Cross-check en-GB vs en-US key consistency - GB_DIR=$(find . -path "*/language/en-GB" -type d -not -path "./.git/*" 2>/dev/null | head -1) - US_DIR=$(find . -path "*/language/en-US" -type d -not -path "./.git/*" 2>/dev/null | head -1) - - if [ -n "$GB_DIR" ] && [ -n "$US_DIR" ]; then - for GB_FILE in "$GB_DIR"/*.ini; do - [ ! -f "$GB_FILE" ] && continue - FNAME=$(basename "$GB_FILE") - US_FILE="$US_DIR/$FNAME" - [ ! -f "$US_FILE" ] && continue - - GB_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$GB_FILE" 2>/dev/null | sort) - US_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$US_FILE" 2>/dev/null | sort) - - # Keys in en-GB but not en-US - MISSING_US=$(comm -23 <(echo "$GB_KEYS") <(echo "$US_KEYS")) - if [ -n "$MISSING_US" ]; then - echo "::warning::Keys in en-GB/$FNAME but missing from en-US/$FNAME:" - echo "$MISSING_US" | while read -r k; do echo " - $k"; done - WARNINGS=$((WARNINGS + 1)) - fi - - # Keys in en-US but not en-GB - MISSING_GB=$(comm -13 <(echo "$GB_KEYS") <(echo "$US_KEYS")) - if [ -n "$MISSING_GB" ]; then - echo "::warning::Keys in en-US/$FNAME but missing from en-GB/$FNAME:" - echo "$MISSING_GB" | while read -r k; do echo " - $k"; done - WARNINGS=$((WARNINGS + 1)) - fi - done - fi - - { - echo "### Language File Validation" - echo "| Metric | Count |" - echo "|---|---|" - echo "| Files checked | $(echo "$INI_FILES" | wc -l) |" - echo "| Errors | ${ERRORS} |" - echo "| Warnings | ${WARNINGS} |" - } >> $GITHUB_STEP_SUMMARY - - if [ "$ERRORS" -gt 0 ]; then - echo "::error::Language validation failed with ${ERRORS} error(s)" - exit 1 - fi - echo "Language files: OK (${WARNINGS} warning(s))" - - - name: Check changelog has unreleased entry - run: | - if [ ! -f "CHANGELOG.md" ]; then - echo "::warning::No CHANGELOG.md found" - exit 0 - fi - # Check for content under [Unreleased] section - if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then - echo "::error::CHANGELOG.md missing [Unreleased] section" - exit 1 - fi - # Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased - UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true) - if [ "$UNRELEASED_CONTENT" -eq 0 ]; then - echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes." - echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY - echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY - echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]" - - - name: Verify package source - run: | - SOURCE_DIR="src" - [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" - if [ ! -d "$SOURCE_DIR" ]; then - echo "::warning::No src/ or htdocs/ directory" - exit 0 - fi - FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l) - echo "Source: ${FILE_COUNT} files" - [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; } - - # ── Pre-Release RC Build ───────────────────────────────────────────────── - pre-release: - name: Build RC Package - runs-on: ubuntu-latest - needs: [branch-policy, validate] - - steps: - - name: Trigger RC pre-release - env: - MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} - REPO: ${{ github.repository }} - BRANCH: ${{ github.head_ref }} - MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - run: | - curl -s -X POST "${MOKOGIT_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${MOKOGIT_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}" - echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY - echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY - - # ── Issue Reporter ────────────────────────────────────────────────────── - report-issues: - name: Report Issues - needs: [branch-policy, validate] - if: >- - always() && - needs.validate.result == 'failure' - uses: ./.mokogit/workflows/ci-issue-reporter.yml - with: - gate: "PR Validation" - workflow: "PR Check" - severity: error - details: "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed." - secrets: inherit diff --git a/.mokogit/workflows/pre-release.yml b/.mokogit/workflows/pre-release.yml deleted file mode 100644 index 9ba58c9249..0000000000 --- a/.mokogit/workflows/pre-release.yml +++ /dev/null @@ -1,273 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: MokoGit.Workflow -# INGROUP: mokocli.Release -# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli -# PATH: /templates/workflows/universal/pre-release.yml.template -# VERSION: 05.02.00 -# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches - -name: "Universal: Pre-Release" - -on: - push: - branches: - - dev - - 'fix/**' - - 'patch/**' - - 'hotfix/**' - - 'bugfix/**' - - 'chore/**' - - alpha - - beta - - rc - workflow_dispatch: - inputs: - stability: - description: 'Pre-release channel' - required: true - type: choice - options: - - development - - alpha - - beta - - release-candidate - -permissions: - contents: write - -env: - GIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GIT_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GIT_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} - -jobs: - build: - name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})" - runs-on: release - if: >- - github.event_name == 'workflow_dispatch' || - github.event_name == 'push' - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.MOKOGIT_TOKEN }} - ref: ${{ github.ref_name }} - submodules: recursive - - - name: Update submodules to main - run: | - git submodule foreach --quiet 'git checkout main && git pull --quiet origin main' 2>/dev/null || true - - - name: Setup mokocli tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - run: | - # Use pre-installed /opt/mokocli if available (updated by cron every 6h) - if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/cli/manifest_element.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then - echo Using pre-installed /opt/mokocli - echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV - else - echo Falling back to fresh clone - if ! command -v composer > /dev/null 2>&1; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1 - fi - rm -rf /tmp/mokocli - CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git - git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli - cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet - echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV - fi - - - name: Detect platform - id: platform - run: | - # Auto-detect and update platform if not set in manifest - php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true - php ${MOKO_CLI}/manifest_read.php --path . --github-output - - - name: Check platform eligibility (Joomla only) - id: eligibility - run: | - PLATFORM="${{ steps.platform.outputs.platform }}" - if [[ "$PLATFORM" == joomla* ]] || [[ "$PLATFORM" == "joomla" ]]; then - echo "proceed=true" >> "$GITHUB_OUTPUT" - else - echo "proceed=false" >> "$GITHUB_OUTPUT" - echo "::notice::Platform '$PLATFORM' — non-Joomla, skipping pre-release auto-bump" - fi - - - name: Resolve metadata and bump version - id: meta - if: steps.eligibility.outputs.proceed == 'true' - run: | - # Auto-detect stability from branch name on push, or use input on dispatch - if [ "${{ github.event_name }}" = "push" ]; then - case "${{ github.ref_name }}" in - rc) STABILITY="release-candidate" ;; - alpha) STABILITY="alpha" ;; - beta) STABILITY="beta" ;; - *) STABILITY="development" ;; - esac - else - STABILITY="${{ inputs.stability || 'development' }}" - fi - - case "$STABILITY" in - development) SUFFIX="-dev"; TAG="development" ;; - alpha) SUFFIX="-alpha"; TAG="alpha" ;; - beta) SUFFIX="-beta"; TAG="beta" ;; - release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; - esac - - # Bump version via CLI: patch for dev/alpha/beta, minor for RC - case "$STABILITY" in - release-candidate) BUMP="minor" ;; - *) BUMP="patch" ;; - esac - - php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true - - # Set stability suffix and verify consistency - VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01") - VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') - - php ${MOKO_CLI}/version_set_platform.php \ - --path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true - php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true - - # Ensure licensing tags (updateservers, dlid) if enabled in manifest.xml - php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true - - # Append suffix for output - if [ -n "$SUFFIX" ]; then - VERSION="${VERSION}${SUFFIX}" - fi - - # Commit version bump - git config --local user.email "mokogit-actions[bot]@mokoconsulting.tech" - git config --local user.name "mokogit-actions[bot]" - git remote set-url origin "https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - git add -A - git diff --cached --quiet || { - git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]" - git push origin HEAD 2>&1 - } - - # Auto-detect element via manifest_element.php - php ${MOKO_CLI}/manifest_element.php \ - --path . --version "$VERSION" --stability "$STABILITY" \ - --repo "${GIT_REPO}" --github-output - - # Read back element outputs - EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) - ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) - [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GIT_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') - [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip" - - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" - echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" - echo "tag=${TAG}" >> "$GITHUB_OUTPUT" - echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" - echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" - - echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ===" - - - name: Create release - id: release - if: steps.eligibility.outputs.proceed == 'true' - run: | - TAG="${{ steps.meta.outputs.tag }}" - VERSION="${{ steps.meta.outputs.version }}" - API_BASE="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" - php ${MOKO_CLI}/release_create.php \ - --path . --version "$VERSION" --tag "$TAG" \ - --token "${{ secrets.MOKOGIT_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GIT_REPO}" --branch "${{ github.ref_name }}" --prerelease - - - name: Update release notes from CHANGELOG.md - if: steps.eligibility.outputs.proceed == 'true' - run: | - TAG="${{ steps.meta.outputs.tag }}" - VERSION="${{ steps.meta.outputs.version }}" - API_BASE="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" - - # Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading) - if [ -f "CHANGELOG.md" ]; then - NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md) - [ -z "$NOTES" ] && NOTES="Release ${VERSION}" - else - NOTES="Release ${VERSION}" - fi - - # Update release body via API - RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGIT_TOKEN }}" \ - "${API_BASE}/releases/tags/${TAG}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) - - if [ -n "$RELEASE_ID" ]; then - python3 -c " - import json, urllib.request - body = open('/dev/stdin').read() - payload = json.dumps({'body': body}).encode() - req = urllib.request.Request( - '${API_BASE}/releases/${RELEASE_ID}', - data=payload, method='PATCH', - headers={ - 'Authorization': 'token ${{ secrets.MOKOGIT_TOKEN }}', - 'Content-Type': 'application/json' - }) - urllib.request.urlopen(req) - " <<< "$NOTES" - echo "Release notes updated from CHANGELOG.md" - fi - - - name: Build package and upload - id: package - if: steps.eligibility.outputs.proceed == 'true' - run: | - VERSION="${{ steps.meta.outputs.version }}" - TAG="${{ steps.meta.outputs.tag }}" - API_BASE="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" - php ${MOKO_CLI}/release_package.php \ - --path . --version "$VERSION" --tag "$TAG" \ - --token "${{ secrets.MOKOGIT_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GIT_REPO}" --output /tmp || true - - # updates.xml is generated dynamically by MokoGit license server - # No need to build, commit, or sync updates.xml from workflows - - - name: "Delete lesser pre-release channels (cascade)" - if: steps.eligibility.outputs.proceed == 'true' - continue-on-error: true - run: | - API_BASE="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" - TOKEN="${{ secrets.MOKOGIT_TOKEN }}" - - php ${MOKO_CLI}/release_cascade.php \ - --stability "${{ steps.meta.outputs.stability }}" \ - --token "${TOKEN}" \ - --api-base "${API_BASE}" - - - name: Summary - if: always() - run: | - VERSION="${{ steps.meta.outputs.version }}" - STABILITY="${{ steps.meta.outputs.stability }}" - ZIP_NAME="${{ steps.meta.outputs.zip_name }}" - SHA256="${{ steps.package.outputs.sha256_zip }}" - echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY - echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY - echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY - echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY diff --git a/.mokogit/workflows/rc-revert.yml b/.mokogit/workflows/rc-revert.yml deleted file mode 100644 index bfea6b8ad6..0000000000 --- a/.mokogit/workflows/rc-revert.yml +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: MokoGit.Workflow -# INGROUP: mokocli.Universal -# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli -# PATH: /.mokogit/workflows/rc-revert.yml -# VERSION: 09.23.00 -# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge - -name: "RC Revert" - -on: - pull_request: - types: [closed] - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - revert: - name: Rename rc/ back to dev/ - runs-on: ubuntu-latest - if: >- - github.event.pull_request.merged == false && - startsWith(github.event.pull_request.head.ref, 'rc/') - - steps: - - name: Rename branch - env: - BRANCH: ${{ github.event.pull_request.head.ref }} - REPO: ${{ github.repository }} - GIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - TOKEN: ${{ secrets.MOKOGIT_TOKEN }} - run: | - set -euo pipefail - # BRANCH is attacker-controlled (PR head ref). Strict allowlist before ANY use. - if ! printf '%s' "$BRANCH" | grep -Eq '^rc/[A-Za-z0-9._/-]+$'; then - echo "::error::Refusing unsafe branch name: $BRANCH"; exit 1 - fi - SUFFIX="${BRANCH#rc/}" - DEV_BRANCH="dev/${SUFFIX}" - API="${GIT_URL}/api/v1/repos/${REPO}/branches" - - # Create dev/ branch from rc/ branch - STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \ - -H "Authorization: token ${TOKEN}" \ - -H "Content-Type: application/json" \ - -d "{\"new_branch_name\": \"${DEV_BRANCH}\", \"old_branch_name\": \"${BRANCH}\"}" \ - "${API}" 2>/dev/null || true) - if [ "$STATUS" = "201" ]; then - echo "Created branch: ${DEV_BRANCH}" >> "$GITHUB_STEP_SUMMARY" - else - echo "::error::Failed to create ${DEV_BRANCH} from ${BRANCH} (HTTP ${STATUS})"; exit 1 - fi - - # Read BRANCH from the environment inside PHP (getenv, no string interpolation -> no PHP injection) - ENCODED=$(php -r 'echo rawurlencode(getenv("BRANCH"));') - STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \ - -H "Authorization: token ${TOKEN}" \ - "${API}/${ENCODED}" 2>/dev/null || true) - if [ "$STATUS" = "204" ]; then - echo "Deleted branch: ${BRANCH}" >> "$GITHUB_STEP_SUMMARY" - else - echo "::warning::Failed to delete ${BRANCH} (HTTP ${STATUS})" - fi - - echo "### RC Reverted" >> "$GITHUB_STEP_SUMMARY" - echo "${BRANCH} → ${DEV_BRANCH}" >> "$GITHUB_STEP_SUMMARY" diff --git a/.mokogit/workflows/repo-health.yml b/.mokogit/workflows/repo-health.yml deleted file mode 100644 index dae198bea4..0000000000 --- a/.mokogit/workflows/repo-health.yml +++ /dev/null @@ -1,700 +0,0 @@ -# ============================================================================ -# Copyright (C) 2025 Moko Consulting -# -# This file is part of a Moko Consulting project. -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: MokoGit.Workflow -# INGROUP: mokocli.Validation -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli -# PATH: /templates/workflows/joomla/repo_health.yml.template -# VERSION: 09.23.00 -# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts. -# ============================================================================ - -name: "Generic: Repo Health" - -defaults: - run: - shell: bash - -on: - workflow_dispatch: - inputs: - profile: - description: 'Validation profile: all, scripts, or repo' - required: true - default: all - type: choice - options: - - all - - scripts - - repo - pull_request: - branches: - - main - -permissions: - contents: read - -env: - # Scripts governance policy - SCRIPTS_REQUIRED_DIRS: - SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate - - # Repo health policy - REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.mokogit/workflows/ - REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/ - REPO_DISALLOWED_DIRS: - REPO_DISALLOWED_FILES: TODO.md,todo.md - - # Extended checks toggles - EXTENDED_CHECKS: "true" - - # File / directory variables - DOCS_INDEX: docs/docs-index.md - SCRIPT_DIR: scripts - WORKFLOWS_DIR: .mokogit/workflows - SHELLCHECK_PATTERN: '*.sh' - SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml' - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - access_check: - name: Access control - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: - contents: read - - outputs: - allowed: ${{ steps.perm.outputs.allowed }} - permission: ${{ steps.perm.outputs.permission }} - - steps: - - name: Check actor permission (admin only) - id: perm - env: - TOKEN: ${{ secrets.MOKOGIT_TOKEN || github.token }} - REPO: ${{ github.repository }} - ACTOR: ${{ github.actor }} - run: | - set -euo pipefail - ALLOWED=false - PERMISSION=unknown - METHOD="" - - # Hardcoded authorized users — always allowed - case "$ACTOR" in - jmiller|mokogit-actions[bot]) - ALLOWED=true - PERMISSION=admin - METHOD="hardcoded allowlist" - ;; - *) - # Detect platform and check permissions via API - API_BASE="${GITHUB_API_URL:-${GIT_API_URL:-https://api.github.com}}" - RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}') - PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown") - if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "maintain" ] || [ "$PERMISSION" = "owner" ]; then - ALLOWED=true - fi - METHOD="collaborator API" - ;; - esac - - echo "permission=${PERMISSION}" >> "$GITHUB_OUTPUT" - echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT" - - { - echo "## Access Authorization" - echo "" - echo "| Field | Value |" - echo "|-------|-------|" - echo "| **Actor** | \`${ACTOR}\` |" - echo "| **Repository** | \`${REPO}\` |" - echo "| **Permission** | \`${PERMISSION}\` |" - echo "| **Method** | ${METHOD} |" - echo "| **Authorized** | ${ALLOWED} |" - echo "" - if [ "$ALLOWED" = "true" ]; then - echo "${ACTOR} authorized (${METHOD})" - else - echo "${ACTOR} is NOT authorized. Requires admin or maintain role." - fi - } >> "${GITHUB_STEP_SUMMARY}" - - - name: Deny execution when not permitted - if: ${{ steps.perm.outputs.allowed != 'true' }} - run: | - set -euo pipefail - printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}" - exit 1 - - 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|scripts|repo) ;; - *) - printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" - exit 1 - ;; - esac - - if [ "${profile}" = 'repo' ]; then - { - printf '%s\n' '### Scripts governance' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' 'Status: SKIPPED' - printf '%s\n' 'Reason: profile excludes scripts governance' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - exit 0 - fi - - if [ ! -d "${SCRIPT_DIR}" ]; then - { - printf '%s\n' '### Scripts governance' - printf '%s\n' 'Status: OK (advisory)' - printf '%s\n' 'scripts/ directory not present. No scripts governance enforced.' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - exit 0 - fi - - if [ -n "${SCRIPTS_REQUIRED_DIRS:-}" ]; then IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"; else required_dirs=(); fi - IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}" - - missing_dirs=() - unapproved_dirs=() - - for d in "${required_dirs[@]}"; do - req="${d%/}" - [ ! -d "${req}" ] && missing_dirs+=("${req}/") - done - - while IFS= read -r d; do - allowed=false - for a in "${allowed_dirs[@]}"; do - a_norm="${a%/}" - [ "${d%/}" = "${a_norm}" ] && allowed=true - done - [ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/") - done < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##') - - { - printf '%s\n' '### Scripts governance' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' '| Area | Status | Notes |' - printf '%s\n' '|---|---|---|' - - if [ "${#missing_dirs[@]}" -gt 0 ]; then - printf '%s\n' '| Required directories | Warning | Missing required subfolders |' - else - printf '%s\n' '| Required directories | OK | All required subfolders present |' - fi - - if [ "${#unapproved_dirs[@]}" -gt 0 ]; then - printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |' - else - printf '%s\n' '| Directory policy | OK | No unapproved directories |' - fi - - printf '%s\n' '| Enforcement mode | Advisory | scripts folder is optional |' - printf '\n' - - if [ "${#missing_dirs[@]}" -gt 0 ]; then - printf '%s\n' 'Missing required script directories:' - for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done - printf '\n' - else - printf '%s\n' 'Missing required script directories: none.' - printf '\n' - fi - - if [ "${#unapproved_dirs[@]}" -gt 0 ]; then - printf '%s\n' 'Unapproved script directories detected:' - for m in "${unapproved_dirs[@]}"; do printf '%s\n' "- ${m}"; done - printf '\n' - else - printf '%s\n' 'Unapproved script directories detected: none.' - printf '\n' - fi - - printf '%s\n' 'Scripts governance completed in advisory mode.' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - - repo_health: - name: Repository health - needs: access_check - if: ${{ needs.access_check.outputs.allowed == 'true' }} - runs-on: ubuntu-latest - timeout-minutes: 20 - permissions: - contents: read - - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - fetch-depth: 0 - - - name: Repository health checks - env: - PROFILE_RAW: ${{ github.event.inputs.profile }} - run: | - set -euo pipefail - - profile="${PROFILE_RAW:-all}" - case "${profile}" in - all|scripts|repo) ;; - *) - printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" - exit 1 - ;; - esac - - if [ "${profile}" = 'scripts' ]; then - { - printf '%s\n' '### Repository health' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' 'Status: SKIPPED' - printf '%s\n' 'Reason: profile excludes repository health' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - exit 0 - fi - - IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}" - IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}" - if [ -n "${REPO_DISALLOWED_DIRS:-}" ]; then IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"; else disallowed_dirs=(); fi - IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES:-}" - - missing_required=() - missing_optional=() - - # Source directory: src/ or htdocs/ (either is valid for extension repos) - SOURCE_DIR="" - if [ -d "src" ]; then - SOURCE_DIR="src" - elif [ -d "htdocs" ]; then - SOURCE_DIR="htdocs" - elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then - # Platform/tooling repos don't need src/ - SOURCE_DIR="" - else - missing_required+=("src/ or htdocs/ (source directory required)") - fi - - for item in "${required_artifacts[@]}"; do - if printf '%s' "${item}" | grep -q '/$'; then - d="${item%/}" - [ ! -d "${d}" ] && missing_required+=("${item}") - else - [ ! -f "${item}" ] && missing_required+=("${item}") - fi - done - - for f in "${optional_files[@]}"; do - if printf '%s' "${f}" | grep -q '/$'; then - d="${f%/}" - [ ! -d "${d}" ] && missing_optional+=("${f}") - else - [ ! -f "${f}" ] && missing_optional+=("${f}") - fi - done - - for d in "${disallowed_dirs[@]}"; do - d_norm="${d%/}" - [ -d "${d_norm}" ] && missing_required+=("${d_norm}/ (disallowed)") - done - - for f in "${disallowed_files[@]}"; do - [ -f "${f}" ] && missing_required+=("${f} (disallowed)") - done - - git fetch origin --prune - - dev_paths=() - dev_branches=() - - while IFS= read -r b; do - name="${b#origin/}" - if [ "${name}" = 'dev' ]; then - dev_branches+=("${name}") - else - dev_paths+=("${name}") - fi - done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//') - - if [ "${#dev_paths[@]}" -eq 0 ] && [ "${#dev_branches[@]}" -eq 0 ]; then - missing_required+=("dev or dev/* branch") - fi - - content_warnings=() - - if [ -f 'CHANGELOG.md' ] && ! grep -Eq '^# Changelog' CHANGELOG.md; then - content_warnings+=("CHANGELOG.md missing '# Changelog' header") - fi - - if [ -f 'CHANGELOG.md' ] && grep -Eq '^[# ]*Unreleased' CHANGELOG.md; then - content_warnings+=("CHANGELOG.md contains Unreleased section (review release readiness)") - fi - - if [ -f 'LICENSE' ] && ! grep -qiE 'GNU GENERAL PUBLIC LICENSE|GPL' LICENSE; then - content_warnings+=("LICENSE does not look like a GPL text") - fi - - if [ -f 'README.md' ] && ! grep -qiE 'moko|Moko' README.md; then - content_warnings+=("README.md missing expected brand keyword") - fi - - export PROFILE_RAW="${profile}" - export MISSING_REQUIRED="$(printf '%s\n' "${missing_required[@]:-}")" - export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")" - export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")" - - report_json=$(printf '{"profile":"%s","missing_required":%d,"missing_optional":%d,"content_warnings":%d}' "$profile" "${#missing_required[@]}" "${#missing_optional[@]}" "${#content_warnings[@]}") - - { - printf '%s\n' '### Repository health' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' '| Metric | Value |' - printf '%s\n' '|---|---|' - printf '%s\n' "| Missing required | ${#missing_required[@]} |" - printf '%s\n' "| Missing optional | ${#missing_optional[@]} |" - printf '%s\n' "| Content warnings | ${#content_warnings[@]} |" - printf '\n' - - printf '%s\n' '### Guardrails report (JSON)' - printf '%s\n' '```json' - printf '%s\n' "${report_json}" - printf '%s\n' '```' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - - if [ "${#missing_required[@]}" -gt 0 ]; then - { - printf '%s\n' '### Missing required repo artifacts' - for m in "${missing_required[@]}"; do printf '%s\n' "- ${m}"; done - printf '%s\n' 'ERROR: Guardrails failed. Missing required repository artifacts.' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - exit 1 - fi - - if [ "${#missing_optional[@]}" -gt 0 ]; then - { - printf '%s\n' '### Missing optional repo artifacts' - for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - if [ "${#content_warnings[@]}" -gt 0 ]; then - { - printf '%s\n' '### Repo content warnings' - for m in "${content_warnings[@]}"; do printf '%s\n' "- ${m}"; done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - # -- Joomla-specific checks -- - joomla_findings=() - - MANIFEST="$(find . -maxdepth 2 -name '*.xml' -exec grep -l '/dev/null | head -1 || true)" - if [ -z "${MANIFEST}" ]; then - joomla_findings+=("Joomla XML manifest not found (no *.xml with tag)") - else - if ! grep -qP '' "${MANIFEST}"; then - joomla_findings+=("XML manifest: tag missing") - fi - if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then - joomla_findings+=("XML manifest: type attribute missing or invalid") - fi - if ! grep -qP '' "${MANIFEST}"; then - joomla_findings+=("XML manifest: tag missing") - fi - if ! grep -qP '' "${MANIFEST}"; then - joomla_findings+=("XML manifest: tag missing") - fi - if ! grep -qP ' missing (required for Joomla 5+)") - fi - fi - - INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)" - if [ "${INI_COUNT}" -eq 0 ]; then - joomla_findings+=("No .ini language files found") - fi - - if [ ! -f 'updates.xml' ]; then - joomla_findings+=("updates.xml missing in root (required for Joomla update server)") - fi - - if [ -n "${SOURCE_DIR}" ]; then - INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site") - for dir in "${INDEX_DIRS[@]}"; do - if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then - joomla_findings+=("${dir}/index.html missing (directory listing protection)") - fi - done - fi - - if [ "${#joomla_findings[@]}" -gt 0 ]; then - { - printf '%s\n' '### Joomla extension checks' - printf '%s\n' '| Check | Status |' - printf '%s\n' '|---|---|' - for f in "${joomla_findings[@]}"; do - printf '%s\n' "| ${f} | Warning |" - done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - else - { - printf '%s\n' '### Joomla extension checks' - printf '%s\n' 'All Joomla-specific checks passed.' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - extended_enabled="${EXTENDED_CHECKS:-true}" - extended_findings=() - - if [ "${extended_enabled}" = 'true' ]; then - if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then - : - else - extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)") - fi - - if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then - bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)" - if [ -n "${bad_refs}" ]; then - extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt") - { - printf '%s\n' '### Workflow pinning advisory' - printf '%s\n' 'Found uses: entries pinned to main/master:' - printf '%s\n' '```' - printf '%s\n' "${bad_refs}" - printf '%s\n' '```' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - fi - - if [ -f "${DOCS_INDEX}" ]; then - missing_links="" - while IFS= read -r docline; do - for link in $(echo "$docline" | grep -oE '\]\([^)]+\)' | sed 's/\](//' | sed 's/)$//' || true); do - case "$link" in http://*|https://*|"#"*|mailto:*) continue ;; esac - linkpath="${link%%#*}" - linkpath="${linkpath%%\?*}" - [ -z "$linkpath" ] && continue - if [ "${linkpath:0:1}" = "/" ]; then - testpath="${linkpath#/}" - else - testpath="$(dirname "${DOCS_INDEX}")/${linkpath}" - fi - [ ! -e "$testpath" ] && missing_links="${missing_links}${testpath} " - done - done < "${DOCS_INDEX}" - if [ -n "${missing_links}" ]; then - extended_findings+=("docs/docs-index.md contains broken relative links") - { - printf '%s\n' '### Docs index link integrity' - printf '%s\n' 'Broken relative links:' - for bl in ${missing_links}; do - printf '%s\n' "- ${bl}" - done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - fi - - if [ -d "${SCRIPT_DIR}" ]; then - if ! command -v shellcheck >/dev/null 2>&1; then - sudo apt-get update -qq - sudo apt-get install -y shellcheck >/dev/null - fi - - sc_out='' - while IFS= read -r shf; do - [ -z "${shf}" ] && continue - out_one="$(shellcheck -S warning -x "${shf}" 2>/dev/null || true)" - if [ -n "${out_one}" ]; then - sc_out="${sc_out}${out_one}\n" - fi - done < <(find "${SCRIPT_DIR}" -type f -name "${SHELLCHECK_PATTERN}" 2>/dev/null | sort) - - if [ -n "${sc_out}" ]; then - extended_findings+=("ShellCheck warnings detected (advisory)") - sc_head="$(printf '%s' "${sc_out}" | head -n 200)" - { - printf '%s\n' '### ShellCheck (advisory)' - printf '%s\n' '```' - printf '%s\n' "${sc_head}" - printf '%s\n' '```' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - fi - - spdx_missing=() - IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}" - spdx_args=() - for g in "${spdx_globs[@]}"; do spdx_args+=("${g}"); done - - while IFS= read -r f; do - [ -z "${f}" ] && continue - if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then - spdx_missing+=("${f}") - fi - done < <(git ls-files "${spdx_args[@]}" 2>/dev/null || true) - - if [ "${#spdx_missing[@]}" -gt 0 ]; then - extended_findings+=("SPDX header missing in some tracked files (advisory)") - { - printf '%s\n' '### SPDX header advisory' - printf '%s\n' 'Files missing SPDX-License-Identifier (first 40 lines scan):' - for f in "${spdx_missing[@]}"; do printf '%s\n' "- ${f}"; done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - stale_cutoff_days=180 - stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 > days*86400) print $1}' | head -50)" - if [ -n "${stale_branches}" ]; then - extended_findings+=("Stale remote branches detected (advisory)") - { - printf '%s\n' '### Git hygiene advisory' - printf '%s\n' "Branches with last commit older than ${stale_cutoff_days} days (sample up to 50):" - while IFS= read -r b; do [ -n "${b}" ] && printf '%s\n' "- ${b}"; done <<< "${stale_branches}" - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - fi - - { - printf '%s\n' '### Guardrails coverage matrix' - printf '%s\n' '| Domain | Status | Notes |' - printf '%s\n' '|---|---|---|' - printf '%s\n' '| Access control | OK | Admin-only execution gate |' - printf '%s\n' '| Release policy | N/A | Releases handled by MokoGit |' - printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |' - printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |' - printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |' - if [ "${extended_enabled}" = 'true' ]; then - if [ "${#extended_findings[@]}" -gt 0 ]; then - printf '%s\n' '| Extended checks | Warning | See extended findings below |' - else - printf '%s\n' '| Extended checks | OK | No findings |' - fi - else - printf '%s\n' '| Extended checks | SKIPPED | EXTENDED_CHECKS disabled |' - fi - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - - if [ "${extended_enabled}" = 'true' ] && [ "${#extended_findings[@]}" -gt 0 ]; then - { - printf '%s\n' '### Extended findings (advisory)' - for f in "${extended_findings[@]}"; do printf '%s\n' "- ${f}"; done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}" - - - site-health: - name: Site Health - runs-on: ubuntu-latest - if: github.event_name == 'workflow_dispatch' - steps: - - uses: actions/checkout@v4 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.3' - - - name: Uptime check - if: env.URLS != '' - run: | - echo "$URLS" > /tmp/urls.txt - php monitoring/uptime-probe.php --urls /tmp/urls.txt --timeout 15 || echo "::warning::Some sites are down" - rm -f /tmp/urls.txt - env: - URLS: ${{ vars.MONITORED_URLS }} - - - name: SSL certificate check - if: env.DOMAINS != '' - run: | - echo "$DOMAINS" > /tmp/domains.txt - php monitoring/ssl-check.php --domains /tmp/domains.txt --warn-days 30 || echo "::warning::SSL certificates expiring soon" - rm -f /tmp/domains.txt - env: - DOMAINS: ${{ vars.MONITORED_DOMAINS }} - - - name: Summary - if: always() - run: | - echo "### Site Health" >> $GITHUB_STEP_SUMMARY - echo "Uptime and SSL checks completed." >> $GITHUB_STEP_SUMMARY - - # ═══════════════════════════════════════════════════════════════════════ - # Issue Reporter — file issues for failed gates - # ═══════════════════════════════════════════════════════════════════════ - report-scripts: - name: "Report: Scripts Governance" - needs: [access_check, scripts_governance] - if: >- - always() && - needs.scripts_governance.result == 'failure' - uses: ./.mokogit/workflows/ci-issue-reporter.yml - with: - gate: "Scripts Governance" - workflow: "Repo Health" - severity: error - details: "Scripts directory policy violations detected. Review required and allowed directories." - secrets: inherit - - report-health: - name: "Report: Repository Health" - needs: [access_check, repo_health] - if: >- - always() && - needs.repo_health.result == 'failure' - uses: ./.mokogit/workflows/ci-issue-reporter.yml - with: - gate: "Repository Health" - workflow: "Repo Health" - severity: error - details: "Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary." - secrets: inherit diff --git a/.mokogit/workflows/standards-compliance.yml b/.mokogit/workflows/standards-compliance.yml deleted file mode 100644 index 4a8d9d6c98..0000000000 --- a/.mokogit/workflows/standards-compliance.yml +++ /dev/null @@ -1,2613 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# SPDX-License-Identifier: GPL-3.0-or-later -# FILE INFORMATION -# DEFGROUP: GitHub.Workflow -# INGROUP: MokoCLI.Compliance -# REPO: https://github.com/mokoconsulting-tech/mokocli -# PATH: /.mokogit/workflows/standards-compliance.yml -# VERSION: 04.06.00 -# BRIEF: MokoCLI compliance validation workflow -# NOTE: Validates repository structure, documentation, and coding standards - -name: "Generic: 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 - -# MokoCLI 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 MokoCLI-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=1 - - 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 .mokogit; 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 ".mokogit" ] && echo "- Create .mokogit directory: \`mkdir -p .mokogit/workflows\`" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "📚 Reference: [MokoCLI Repository Structure](https://github.com/mokoconsulting-tech/mokocli/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 MokoCLI 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/mokocli/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/mokocli/tree/main/templates/docs/required/CONTRIBUTING.md)" >> $GITHUB_STEP_SUMMARY - [ ! -f "SECURITY.md" ] && echo "- Create SECURITY.md: Use [template](https://github.com/mokoconsulting-tech/mokocli/tree/main/templates/docs/required/SECURITY.md)" >> $GITHUB_STEP_SUMMARY - [ ! -f ".editorconfig" ] && echo "- Add .editorconfig: Use [template](https://github.com/mokoconsulting-tech/mokocli/tree/main/templates/.editorconfig)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "📚 Reference: [MokoCLI File Requirements](https://github.com/mokoconsulting-tech/mokocli/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 MokoCLI 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 MokoCLI 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 || true) - - 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 || true) - - 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 "MokoCLI 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 MokoCLI 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/mokocli.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 MokoCLI 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 "⏭️ MokoCLI 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=".mokogit/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:** .mokogit/workflows directory is required for CI/CD automation" >> $GITHUB_STEP_SUMMARY - echo "**Action Required:** Create .mokogit/workflows directory and add GitHub Actions workflows" >> $GITHUB_STEP_SUMMARY - echo "" - echo "❌ ERROR: .mokogit/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 MokoCLI-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 MokoCLI)" >> $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 .mokogit/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('.mokogit/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('.mokogit/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=".mokogit/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 MokoCLI-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 MokoCLI-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 || true - fi - - # Run yamllint and count line-length warnings - YAML_OUTPUT=$(yamllint .mokogit/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 || true) - - 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 MokoCLI tools" - if [ ! -d "/tmp/mokostandards" ]; then - git clone --depth 1 --branch version/04 --quiet \ - "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/mokocli.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 MokoCLI tools" - if [ ! -d "/tmp/mokostandards" ]; then - git clone --depth 1 --branch version/04 --quiet \ - "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/mokocli.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 .mokogit/config.tf location (not root override files) - echo "### Override Configuration Check" >> $GITHUB_STEP_SUMMARY - LEGACY_OVERRIDES=$(find . -maxdepth 1 -name "*override*.tf" -o -name "MokoCLI.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**: .mokogit/config.tf" >> $GITHUB_STEP_SUMMARY - echo "**Legacy files found**: $LEGACY_OVERRIDES" >> $GITHUB_STEP_SUMMARY - WARNINGS=$((WARNINGS + 1)) - else - if [ -f ".mokogit/config.tf" ]; then - echo "✅ Override configuration in correct location (.mokogit/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 "# 📊 MokoCLI 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 MokoCLI!" >> $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 "- [MokoCLI Documentation](https://github.com/mokoconsulting-tech/mokocli)" >> $GITHUB_STEP_SUMMARY - echo "- [Repository Structure Guide](https://github.com/mokoconsulting-tech/mokocli/tree/main/docs/policy/core-structure.md)" >> $GITHUB_STEP_SUMMARY - echo "- [Documentation Standards](https://github.com/mokoconsulting-tech/mokocli/tree/main/docs/policy/document-formatting.md)" >> $GITHUB_STEP_SUMMARY - echo "- [Coding Standards](https://github.com/mokoconsulting-tech/mokocli/tree/main/docs/policy/coding-style-guide.md)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "_Generated by MokoCLI 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 MokoCLI requirements" >> $GITHUB_STEP_SUMMARY - echo "" - echo "✅ SUCCESS: Repository is fully MokoCLI compliant" - - - name: Create or reopen tracking issue for standards violations - if: failure() - env: - GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} - run: | - REPO="${{ github.repository }}" - RUN_URL="${{ github.server_url }}/${REPO}/actions/runs/${{ github.run_id }}" - DATE=$(date -u '+%Y-%m-%d') - SHA="${{ github.sha }}" - ACTOR="${{ github.actor }}" - BRANCH="${{ github.ref_name }}" - - # Collect failed checks - FAILED="" - [ "${{ needs.repository-structure.result }}" != "success" ] && FAILED="${FAILED}\n- Repository Structure" - [ "${{ needs.documentation-quality.result }}" != "success" ] && FAILED="${FAILED}\n- Documentation Quality" - [ "${{ needs.coding-standards.result }}" != "success" ] && FAILED="${FAILED}\n- Coding Standards" - [ "${{ needs.license-compliance.result }}" != "success" ] && FAILED="${FAILED}\n- License Compliance" - [ "${{ needs.git-hygiene.result }}" != "success" ] && FAILED="${FAILED}\n- Git Hygiene" - [ "${{ needs.workflow-validation.result }}" != "success" ] && FAILED="${FAILED}\n- Workflow Validation" - [ "${{ needs.version-consistency.result }}" != "success" ] && FAILED="${FAILED}\n- Version Consistency" - [ "${{ needs.script-integrity.result }}" != "success" ] && FAILED="${FAILED}\n- Script Integrity" - [ "${{ needs.secret-scanning.result }}" != "success" ] && FAILED="${FAILED}\n- Secret Scanning" - [ "${{ needs.line-length-validation.result }}" != "success" ] && FAILED="${FAILED}\n- Line Length" - [ "${{ needs.file-size-limits.result }}" != "success" ] && FAILED="${FAILED}\n- File Size Limits" - [ "${{ needs.readme-completeness.result }}" != "success" ] && FAILED="${FAILED}\n- README Completeness" - - if [ -z "$FAILED" ]; then - echo "No failed checks to report" - exit 0 - fi - - TITLE="[Standards] Compliance violations — ${DATE}" - BODY="## Standards Compliance Violations - - | Field | Value | - |-------|-------| - | **Branch** | \`${BRANCH}\` | - | **Commit** | \`${SHA:0:7}\` | - | **Actor** | @${ACTOR} | - | **Run** | [View workflow](${RUN_URL}) | - - ### Failed Checks - $(printf '%b' "$FAILED") - - ### Required Actions - 1. Review the [workflow run](${RUN_URL}) for details - 2. Fix each failed check - 3. Push to trigger a new scan - - --- - *Auto-created by standards-compliance workflow*" - - BODY=$(echo "$BODY" | sed 's/^ //') - LABEL="standards-violation" - - gh label create "$LABEL" --repo "$REPO" --color "D73A4A" --description "Standards compliance failure" --force 2>/dev/null || true - - EXISTING=$(gh api "repos/${REPO}/issues?labels=${LABEL}&state=all&per_page=1&sort=created&direction=desc" \ - --jq '.[0].number' 2>/dev/null) - - if [ -n "$EXISTING" ] && [ "$EXISTING" != "null" ]; then - gh api "repos/${REPO}/issues/${EXISTING}" -X PATCH \ - -f title="$TITLE" -f body="$BODY" -f state="open" --silent - echo "Updated issue #${EXISTING}" - else - gh issue create --repo "$REPO" --title "$TITLE" --body "$BODY" \ - --label "$LABEL" --assignee "jmiller" - fi - -# CUSTOMIZATION: -# -# 1. Adjust severity of checks (convert warnings to errors or vice versa) -# 2. Add project-specific validation rules -# 3. Integrate with custom linting tools -# 4. Add notification steps for compliance failures -# 5. Customize required files/directories for your project type - diff --git a/.mokogit/workflows/version-set.yml b/.mokogit/workflows/version-set.yml deleted file mode 100644 index c3093b246c..0000000000 --- a/.mokogit/workflows/version-set.yml +++ /dev/null @@ -1,130 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: MokoGit.Workflow.Template -# INGROUP: MokoStandards.CI -# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla -# PATH: /.mokogit/workflows/version-set.yml -# VERSION: 01.00.00 -# BRIEF: Set or reset the extension version across all version-bearing files - -name: "Joomla: Set Version" - -on: - workflow_dispatch: - inputs: - version: - description: "Version number (e.g. 01.00.00)" - required: true - type: string - branch: - description: "Branch to update (default: current)" - required: false - type: string - -permissions: - contents: write - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - set-version: - name: Set Version to ${{ inputs.version }} - runs-on: ubuntu-latest - - steps: - - name: Validate version format - run: | - VERSION="${{ inputs.version }}" - if ! echo "$VERSION" | grep -qP '^\d{2}\.\d{2}\.\d{2}$'; then - echo "::error::Invalid version format '${VERSION}' — expected XX.YY.ZZ (e.g. 01.00.00)" - exit 1 - fi - echo "VERSION=${VERSION}" >> "$GITHUB_ENV" - - - name: Checkout - uses: actions/checkout@v4 - with: - token: ${{ secrets.MOKOGIT_TOKEN || github.token }} - ref: ${{ inputs.branch || github.ref }} - fetch-depth: 1 - - - name: Update manifest version - run: | - MANIFEST="" - for XML_FILE in $(find . -maxdepth 3 -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 "::warning::No Joomla extension manifest found — skipping manifest update" - else - OLD_VER=$(grep -oP '\K[^<]+' "$MANIFEST" | head -1) - sed -i "s|${OLD_VER}|${VERSION}|" "$MANIFEST" - echo "Manifest: ${OLD_VER} → ${VERSION} (${MANIFEST})" - fi - - - name: Update README.md version - run: | - if [ -f "README.md" ]; then - if grep -qP '^\s*VERSION:\s*\d' README.md; then - sed -i -E "s/(VERSION:\s*)[0-9]{2}\.[0-9]{2}\.[0-9]{2}/\1${VERSION}/" README.md - echo "README.md version updated to ${VERSION}" - else - echo "::warning::No VERSION line found in README.md — skipping" - fi - fi - - - name: Update CHANGELOG.md - run: | - if [ -f "CHANGELOG.md" ]; then - DATE=$(date +%Y-%m-%d) - # Check if this version already has an entry - if grep -q "^\#\# \[${VERSION}\]" CHANGELOG.md; then - echo "CHANGELOG.md already has entry for ${VERSION} — skipping" - else - # Insert new version entry after [Unreleased] or at the top after header - if grep -q '^\#\# \[Unreleased\]' CHANGELOG.md; then - sed -i "/^\#\# \[Unreleased\]/a\\\\n## [${VERSION}] --- ${DATE}" CHANGELOG.md - else - sed -i "/^\# Changelog/a\\\\n## [Unreleased]\n\n## [${VERSION}] --- ${DATE}" CHANGELOG.md - fi - echo "CHANGELOG.md: added entry for ${VERSION}" - fi - else - echo "::warning::No CHANGELOG.md found — skipping" - fi - - - name: Update FILE INFORMATION blocks - run: | - # Update VERSION in file header blocks (# VERSION: XX.YY.ZZ) - find . -maxdepth 1 -type f \( -name "*.yml" -o -name "*.yaml" -o -name "*.php" -o -name "*.md" \) \ - -not -path "./.git/*" -not -path "./vendor/*" -print0 2>/dev/null | \ - while IFS= read -r -d '' FILE; do - if head -20 "$FILE" | grep -qP '^\s*#?\s*VERSION:\s*\d{2}\.\d{2}\.\d{2}'; then - sed -i -E "s/(#?\s*VERSION:\s*)[0-9]{2}\.[0-9]{2}\.[0-9]{2}/\1${VERSION}/" "$FILE" - echo "Updated FILE INFORMATION VERSION in ${FILE}" - fi - done - - - name: Commit and push - run: | - git config user.name "Moko Consulting [bot]" - git config user.email "hello@mokoconsulting.tech" - git add -A - if git diff --cached --quiet; then - echo "No version changes detected — nothing to commit" - else - git commit -m "chore: set version to ${VERSION} [skip bump] - -Authored-by: Moko Consulting" - git push - echo "### Version Set" >> $GITHUB_STEP_SUMMARY - echo "Version updated to \`${VERSION}\` on branch \`${GITHUB_REF_NAME}\`" >> $GITHUB_STEP_SUMMARY - fi -- 2.52.0