From e416499764a7495709eb972bb3aab2d1a886165c Mon Sep 17 00:00:00 2001 From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com> Date: Sat, 4 Apr 2026 15:19:47 -0500 Subject: [PATCH] ci: sync workflows from main Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/.mokostandards | 1 + .github/workflows/auto-release.yml | 2 +- .github/workflows/deploy-demo.yml | 37 ++++---- .github/workflows/deploy-dev.yml | 44 +++++----- .github/workflows/deploy-rs.yml | 37 ++++---- .github/workflows/repo_health.yml | 95 +++++++++++++++++---- .github/workflows/standards-compliance.yml | 8 +- .github/workflows/sync-version-on-merge.yml | 2 +- 8 files changed, 144 insertions(+), 82 deletions(-) create mode 100644 .github/.mokostandards diff --git a/.github/.mokostandards b/.github/.mokostandards new file mode 100644 index 0000000..09b7828 --- /dev/null +++ b/.github/.mokostandards @@ -0,0 +1 @@ +platform: waas-component diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index 8951664..5462926 100644 --- a/.github/workflows/auto-release.yml +++ b/.github/workflows/auto-release.yml @@ -64,7 +64,7 @@ jobs: 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.04 --quiet \ + git clone --depth 1 --branch version/04.05 --quiet \ "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ /tmp/mokostandards cd /tmp/mokostandards diff --git a/.github/workflows/deploy-demo.yml b/.github/workflows/deploy-demo.yml index 4da8dab..7f785bf 100644 --- a/.github/workflows/deploy-demo.yml +++ b/.github/workflows/deploy-demo.yml @@ -36,10 +36,9 @@ name: Deploy to Demo Server (SFTP) # Optional org-level variable: DEMO_FTP_PORT (auto-detected from host or defaults to 22) # Optional org/repo variable: DEMO_FTP_SUFFIX — when set, appended to DEMO_FTP_PATH to form the # full remote destination: DEMO_FTP_PATH/DEMO_FTP_SUFFIX -# Ignore rules: Place a .ftp_ignore file in the repository root. Each non-empty, -# non-comment line is a regex pattern tested against the relative path -# of each file (e.g. "subdir/file.txt"). The .gitignore is also -# respected automatically. +# Ignore rules: Place a .ftpignore file in the repository root. Each non-empty, +# non-comment line is a glob pattern tested against the relative path +# of each file (e.g. "subdir/file.txt"). The .gitignore is NOT used. # Required org-level secret: DEMO_FTP_KEY (preferred) or DEMO_FTP_PASSWORD # # Access control: only users with admin or maintain role on the repository may deploy. @@ -195,8 +194,8 @@ jobs: env: SOURCE_DIR: ${{ steps.source.outputs.dir }} run: | - # ── Convert a gitignore-style glob line to an ERE pattern ────────────── - ftp_ignore_to_regex() { + # ── Convert a ftpignore-style glob line to an ERE pattern ────────────── + ftpignore_to_regex() { local line="$1" local anchored=false # Strip inline comments and whitespace @@ -226,15 +225,15 @@ jobs: fi } - # ── Read .ftp_ignore (gitignore-style globs) ───────────────────────── + # ── Read .ftpignore (ftpignore-style globs) ───────────────────────── IGNORE_PATTERNS=() IGNORE_SOURCES=() - if [ -f ".ftp_ignore" ]; then + if [ -f ".ftpignore" ]; then while IFS= read -r line; do [[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]] && continue - regex=$(ftp_ignore_to_regex "$line") + regex=$(ftpignore_to_regex "$line") [ -n "$regex" ] && IGNORE_PATTERNS+=("$regex") && IGNORE_SOURCES+=("$line") - done < ".ftp_ignore" + done < ".ftpignore" fi # ── Walk src/ and classify every file ──────────────────────────────── @@ -245,17 +244,11 @@ jobs: SKIP=false for i in "${!IGNORE_PATTERNS[@]}"; do if echo "$rel" | grep -qE "${IGNORE_PATTERNS[$i]}" 2>/dev/null; then - IGNORED_FILES+=("$rel | .ftp_ignore \`${IGNORE_SOURCES[$i]}\`") + IGNORED_FILES+=("$rel | .ftpignore \`${IGNORE_SOURCES[$i]}\`") SKIP=true; break fi done $SKIP && continue - if [ -f ".gitignore" ]; then - git check-ignore -q "$rel" 2>/dev/null && { - IGNORED_FILES+=("$rel | .gitignore") - continue - } || true - fi WILL_UPLOAD+=("$rel") done < <(find "$SOURCE_DIR" -type f -print0 | sort -z) @@ -426,7 +419,7 @@ jobs: 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.04 --quiet \ + git clone --depth 1 --branch version/04.05 --quiet \ "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ /tmp/mokostandards cd /tmp/mokostandards @@ -637,8 +630,12 @@ jobs: DEPLOY_ARGS+=(--key-passphrase "$SFTP_PASSWORD") fi - php /tmp/mokostandards/api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}" - # (deploy-sftp.php handles dotfile skipping and .ftp_ignore natively) + PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true) + if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards/api/deploy/deploy-joomla.php" ]; then + php /tmp/mokostandards/api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}" + else + php /tmp/mokostandards/api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}" + fi # Remove temp files that should never be left behind rm -f /tmp/deploy_key /tmp/sftp-config.json diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index 9efa525..7019628 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -37,10 +37,9 @@ name: Deploy to Dev Server (SFTP) # Optional org-level variable: DEV_FTP_PORT (auto-detected from host or defaults to 22) # Optional org/repo variable: DEV_FTP_SUFFIX — when set, appended to DEV_FTP_PATH to form the # full remote destination: DEV_FTP_PATH/DEV_FTP_SUFFIX -# Ignore rules: Place a .ftp_ignore file in the repository root. Each non-empty, -# non-comment line is a regex pattern tested against the relative path -# of each file (e.g. "subdir/file.txt"). The .gitignore is also -# respected automatically. +# Ignore rules: Place a .ftpignore file in the repository root. Each non-empty, +# non-comment line is a glob pattern tested against the relative path +# of each file (e.g. "subdir/file.txt"). The .gitignore is NOT used. # Required org-level secret: DEV_FTP_KEY (preferred) or DEV_FTP_PASSWORD # # Access control: only users with admin or maintain role on the repository may deploy. @@ -200,8 +199,8 @@ jobs: env: SOURCE_DIR: ${{ steps.source.outputs.dir }} run: | - # ── Convert a gitignore-style glob line to an ERE pattern ────────────── - ftp_ignore_to_regex() { + # ── Convert a ftpignore-style glob line to an ERE pattern ────────────── + ftpignore_to_regex() { local line="$1" local anchored=false # Strip inline comments and whitespace @@ -231,15 +230,15 @@ jobs: fi } - # ── Read .ftp_ignore (gitignore-style globs) ───────────────────────── + # ── Read .ftpignore (ftpignore-style globs) ───────────────────────── IGNORE_PATTERNS=() IGNORE_SOURCES=() - if [ -f ".ftp_ignore" ]; then + if [ -f ".ftpignore" ]; then while IFS= read -r line; do [[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]] && continue - regex=$(ftp_ignore_to_regex "$line") + regex=$(ftpignore_to_regex "$line") [ -n "$regex" ] && IGNORE_PATTERNS+=("$regex") && IGNORE_SOURCES+=("$line") - done < ".ftp_ignore" + done < ".ftpignore" fi # ── Walk src/ and classify every file ──────────────────────────────── @@ -250,17 +249,11 @@ jobs: SKIP=false for i in "${!IGNORE_PATTERNS[@]}"; do if echo "$rel" | grep -qE "${IGNORE_PATTERNS[$i]}" 2>/dev/null; then - IGNORED_FILES+=("$rel | .ftp_ignore \`${IGNORE_SOURCES[$i]}\`") + IGNORED_FILES+=("$rel | .ftpignore \`${IGNORE_SOURCES[$i]}\`") SKIP=true; break fi done $SKIP && continue - if [ -f ".gitignore" ]; then - git check-ignore -q "$rel" 2>/dev/null && { - IGNORED_FILES+=("$rel | .gitignore") - continue - } || true - fi WILL_UPLOAD+=("$rel") done < <(find "$SOURCE_DIR" -type f -print0 | sort -z) @@ -431,7 +424,7 @@ jobs: 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.04 --quiet \ + git clone --depth 1 --branch version/04.05 --quiet \ "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ /tmp/mokostandards cd /tmp/mokostandards @@ -583,8 +576,8 @@ jobs: fi # Dev deploys skip minified files — use unminified sources for debugging - echo "*.min.js" >> .ftp_ignore - echo "*.min.css" >> .ftp_ignore + echo "*.min.js" >> .ftpignore + echo "*.min.css" >> .ftpignore # ── Run deploy-sftp.php from MokoStandards ──────────────────────────── DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json) @@ -666,8 +659,15 @@ jobs: fi fi - php /tmp/mokostandards/api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}" - # (deploy-sftp.php handles dotfile skipping and .ftp_ignore natively) + # Use Joomla-aware deploy for waas-component (routes files to correct Joomla dirs) + # Use standard SFTP deploy for everything else + PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true) + if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards/api/deploy/deploy-joomla.php" ]; then + php /tmp/mokostandards/api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}" + else + php /tmp/mokostandards/api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}" + fi + # (both scripts handle dotfile skipping and .ftpignore natively) # Remove temp files that should never be left behind rm -f /tmp/deploy_key /tmp/sftp-config.json diff --git a/.github/workflows/deploy-rs.yml b/.github/workflows/deploy-rs.yml index 4cf3ed7..bf9a779 100644 --- a/.github/workflows/deploy-rs.yml +++ b/.github/workflows/deploy-rs.yml @@ -36,10 +36,9 @@ name: Deploy to RS Server (SFTP) # Optional org-level variable: RS_FTP_PORT (auto-detected from host or defaults to 22) # Optional org/repo variable: RS_FTP_SUFFIX — when set, appended to RS_FTP_PATH to form the # full remote destination: RS_FTP_PATH/RS_FTP_SUFFIX -# Ignore rules: Place a .ftp_ignore file in the repository root. Each non-empty, -# non-comment line is a regex pattern tested against the relative path -# of each file (e.g. "subdir/file.txt"). The .gitignore is also -# respected automatically. +# Ignore rules: Place a .ftpignore file in the repository root. Each non-empty, +# non-comment line is a glob pattern tested against the relative path +# of each file (e.g. "subdir/file.txt"). The .gitignore is NOT used. # Required org-level secret: RS_FTP_KEY (preferred) or RS_FTP_PASSWORD # # Access control: only users with admin or maintain role on the repository may deploy. @@ -195,8 +194,8 @@ jobs: env: SOURCE_DIR: ${{ steps.source.outputs.dir }} run: | - # ── Convert a gitignore-style glob line to an ERE pattern ────────────── - ftp_ignore_to_regex() { + # ── Convert a ftpignore-style glob line to an ERE pattern ────────────── + ftpignore_to_regex() { local line="$1" local anchored=false # Strip inline comments and whitespace @@ -226,15 +225,15 @@ jobs: fi } - # ── Read .ftp_ignore (gitignore-style globs) ───────────────────────── + # ── Read .ftpignore (ftpignore-style globs) ───────────────────────── IGNORE_PATTERNS=() IGNORE_SOURCES=() - if [ -f ".ftp_ignore" ]; then + if [ -f ".ftpignore" ]; then while IFS= read -r line; do [[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]] && continue - regex=$(ftp_ignore_to_regex "$line") + regex=$(ftpignore_to_regex "$line") [ -n "$regex" ] && IGNORE_PATTERNS+=("$regex") && IGNORE_SOURCES+=("$line") - done < ".ftp_ignore" + done < ".ftpignore" fi # ── Walk src/ and classify every file ──────────────────────────────── @@ -245,17 +244,11 @@ jobs: SKIP=false for i in "${!IGNORE_PATTERNS[@]}"; do if echo "$rel" | grep -qE "${IGNORE_PATTERNS[$i]}" 2>/dev/null; then - IGNORED_FILES+=("$rel | .ftp_ignore \`${IGNORE_SOURCES[$i]}\`") + IGNORED_FILES+=("$rel | .ftpignore \`${IGNORE_SOURCES[$i]}\`") SKIP=true; break fi done $SKIP && continue - if [ -f ".gitignore" ]; then - git check-ignore -q "$rel" 2>/dev/null && { - IGNORED_FILES+=("$rel | .gitignore") - continue - } || true - fi WILL_UPLOAD+=("$rel") done < <(find "$SOURCE_DIR" -type f -print0 | sort -z) @@ -407,7 +400,7 @@ jobs: 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.04 --quiet \ + git clone --depth 1 --branch version/04.05 --quiet \ "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ /tmp/mokostandards cd /tmp/mokostandards @@ -564,8 +557,12 @@ jobs: DEPLOY_ARGS+=(--key-passphrase "$SFTP_PASSWORD") fi - php /tmp/mokostandards/api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}" - # (deploy-sftp.php handles dotfile skipping and .ftp_ignore natively) + PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true) + if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards/api/deploy/deploy-joomla.php" ]; then + php /tmp/mokostandards/api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}" + else + php /tmp/mokostandards/api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}" + fi # Remove temp files that should never be left behind rm -f /tmp/deploy_key /tmp/sftp-config.json diff --git a/.github/workflows/repo_health.yml b/.github/workflows/repo_health.yml index d3daece..8433039 100644 --- a/.github/workflows/repo_health.yml +++ b/.github/workflows/repo_health.yml @@ -10,7 +10,7 @@ # INGROUP: MokoStandards.Validation # REPO: https://github.com/mokoconsulting-tech/MokoStandards # PATH: /.github/workflows/repo_health.yml -# VERSION: 04.01.00 +# VERSION: 04.05.00 # BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts. # NOTE: Field is user-managed. # ============================================================================ @@ -29,7 +29,7 @@ on: workflow_dispatch: inputs: profile: - description: Which configuration profile to validate. release checks SFTP variables used by release pipeline. scripts checks baseline script prerequisites. repo runs repository health only. al[...] + description: 'Validation profile: all, release, scripts, or repo' required: true default: all type: choice @@ -39,19 +39,7 @@ on: - scripts - repo pull_request: - paths: - - .github/workflows/** - - scripts/** - - docs/** - - dev/** push: - branches: - - main - paths: - - .github/workflows/** - - scripts/** - - docs/** - - dev/** permissions: contents: read @@ -68,7 +56,7 @@ env: # Repo health policy # Files are listed as-is; directories must end with a trailing slash. - REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.github/workflows/,src/ + REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.github/workflows/ REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/ REPO_DISALLOWED_DIRS: REPO_DISALLOWED_FILES: TODO.md,todo.md @@ -82,6 +70,7 @@ env: WORKFLOWS_DIR: .github/workflows SHELLCHECK_PATTERN: '*.sh' SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml' + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true jobs: access_check: @@ -412,6 +401,15 @@ jobs: exit 0 fi + # Source directory: src/ or htdocs/ (either is valid) + if [ -d "src" ]; then + SOURCE_DIR="src" + elif [ -d "htdocs" ]; then + SOURCE_DIR="htdocs" + else + missing_required+=("src/ or htdocs/ (source directory required)") + fi + IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}" IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}" IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}" @@ -561,6 +559,73 @@ jobs: } >> "${GITHUB_STEP_SUMMARY}" fi + # ── Joomla-specific checks ─────────────────────────────────────── + joomla_findings=() + + # XML manifest: find any XML file containing tag)") + else + # Check tag exists + if ! grep -qP '' "${MANIFEST}"; then + joomla_findings+=("XML manifest: tag missing") + fi + # Check extension type attribute + if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then + joomla_findings+=("XML manifest: type attribute missing or invalid") + fi + # Check tag + if ! grep -qP '' "${MANIFEST}"; then + joomla_findings+=("XML manifest: tag missing") + fi + # Check tag + if ! grep -qP '' "${MANIFEST}"; then + joomla_findings+=("XML manifest: tag missing") + fi + # Check for Joomla 5+ + if ! grep -qP ' missing (required for Joomla 5+)") + fi + fi + + # Language files: check for at least one .ini file + INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)" + if [ "${INI_COUNT}" -eq 0 ]; then + joomla_findings+=("No .ini language files found") + fi + + # update.xml must exist in root (Joomla update server) + if [ ! -f 'update.xml' ]; then + joomla_findings+=("update.xml missing in root (required for Joomla update server)") + fi + + # index.html files for directory listing protection + INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site") + for dir in "${INDEX_DIRS[@]}"; do + if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then + joomla_findings+=("${dir}/index.html missing (directory listing protection)") + fi + done + + if [ "${#joomla_findings[@]}" -gt 0 ]; then + { + printf '%s\n' '### Joomla extension checks' + printf '%s\n' '| Check | Status |' + printf '%s\n' '|---|---|' + for f in "${joomla_findings[@]}"; do + printf '%s\n' "| ${f} | Warning |" + done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + else + { + printf '%s\n' '### Joomla extension checks' + printf '%s\n' 'All Joomla-specific checks passed.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + extended_enabled="${EXTENDED_CHECKS:-true}" extended_findings=() diff --git a/.github/workflows/standards-compliance.yml b/.github/workflows/standards-compliance.yml index 9031292..df8413c 100644 --- a/.github/workflows/standards-compliance.yml +++ b/.github/workflows/standards-compliance.yml @@ -165,7 +165,9 @@ jobs: 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 '