chore: Sync MokoStandards 04.00.27 #88

Merged
jmiller-moko merged 27 commits from chore/sync-mokostandards-v04.00.27 into main 2026-03-24 21:50:04 +00:00
Showing only changes of commit aa7f0d63f9 - Show all commits

View File

@@ -22,7 +22,7 @@
# INGROUP: MokoStandards.Deploy # INGROUP: MokoStandards.Deploy
# REPO: https://github.com/mokoconsulting-tech/MokoStandards # REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/shared/deploy-dev.yml # PATH: /templates/workflows/shared/deploy-dev.yml
# VERSION: 04.00.25 # VERSION: 04.00.27
# BRIEF: SFTP deployment workflow for development server — synced to all governed repos # BRIEF: SFTP deployment workflow for development server — synced to all governed repos
# NOTE: Synced via bulk-repo-sync to .github/workflows/deploy-dev.yml in all governed repos. # NOTE: Synced via bulk-repo-sync to .github/workflows/deploy-dev.yml in all governed repos.
# Port is resolved in order: DEV_FTP_PORT variable → :port suffix in DEV_FTP_HOST → 22. # Port is resolved in order: DEV_FTP_PORT variable → :port suffix in DEV_FTP_HOST → 22.
@@ -35,14 +35,11 @@ name: Deploy to Dev Server (SFTP)
# #
# Required org-level variables: DEV_FTP_HOST, DEV_FTP_PATH, DEV_FTP_USERNAME # Required org-level variables: DEV_FTP_HOST, DEV_FTP_PATH, DEV_FTP_USERNAME
# Optional org-level variable: DEV_FTP_PORT (auto-detected from host or defaults to 22) # Optional org-level variable: DEV_FTP_PORT (auto-detected from host or defaults to 22)
# Optional org/repo variable: DEV_FTP_PATH_SUFFIX # Optional org/repo variable: DEV_FTP_SUFFIX — when set, appended to DEV_FTP_PATH to form the
# Optional org/repo variable: CUSTOM_FOLDER — when set, appended to the remote path after # full remote destination: DEV_FTP_PATH/DEV_FTP_SUFFIX
# DEV_FTP_PATH_SUFFIX; used automatically for Dolibarr modules # Ignore rules: Place a .ftp_ignore file in the repository root. Each non-empty,
# Optional org/repo variable: FTP_IGNORE — comma-delimited list of regex patterns, each enclosed in # non-comment line is a regex pattern tested against the relative path
# double quotes, for files/paths to exclude from upload, e.g.: # of each file (e.g. "subdir/file.txt"). The .gitignore is also
# "\.git*", "\.DS_Store", "configuration\.php", "\.ps1"
# Patterns are tested against the forward-slash relative path of each
# file (e.g. "subdir/file.txt"). The repository .gitignore is also
# respected automatically. # respected automatically.
# Required org-level secret: DEV_FTP_KEY (preferred) or DEV_FTP_PASSWORD # Required org-level secret: DEV_FTP_KEY (preferred) or DEV_FTP_PASSWORD
# #
@@ -53,18 +50,16 @@ on:
branches: branches:
- main - main
- master - master
- 'dev/**'
- develop - develop
- dev
- development - development
paths:
- 'src/**'
pull_request: pull_request:
types: [opened, synchronize, reopened, closed] types: [opened, synchronize, reopened, closed]
branches: branches:
- main - main
- master - master
- 'dev/**'
- develop - develop
- dev
- development - development
workflow_dispatch: workflow_dispatch:
inputs: inputs:
@@ -163,15 +158,47 @@ jobs:
if: steps.source.outputs.skip == 'false' if: steps.source.outputs.skip == 'false'
env: env:
SOURCE_DIR: ${{ steps.source.outputs.dir }} SOURCE_DIR: ${{ steps.source.outputs.dir }}
FTP_IGNORE: ${{ vars.FTP_IGNORE }}
run: | run: |
# ── Parse FTP_IGNORE ───────────────────────────────────────────────── # ── Convert a gitignore-style glob line to an ERE pattern ──────────────
ftp_ignore_to_regex() {
local line="$1"
local anchored=false
# Strip inline comments and whitespace
line=$(printf '%s' "$line" | sed 's/[[:space:]]*#.*$//' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
[ -z "$line" ] && return
# Skip negation patterns (not supported)
[[ "$line" == !* ]] && return
# Trailing slash = directory marker; strip it
line="${line%/}"
# Leading slash = anchored to root; strip it
if [[ "$line" == /* ]]; then
anchored=true
line="${line#/}"
fi
# Escape ERE special chars, then restore glob semantics
local regex
regex=$(printf '%s' "$line" \
| sed 's/[.+^${}()|[\\]/\\&/g' \
| sed 's/\\\*\\\*/\x01/g' \
| sed 's/\\\*/[^\/]*/g' \
| sed 's/\x01/.*/g' \
| sed 's/\\\?/[^\/]/g')
if $anchored; then
printf '^%s(/|$)' "$regex"
else
printf '(^|/)%s(/|$)' "$regex"
fi
}
# ── Read .ftp_ignore (gitignore-style globs) ─────────────────────────
IGNORE_PATTERNS=() IGNORE_PATTERNS=()
if [ -n "$FTP_IGNORE" ]; then IGNORE_SOURCES=()
while IFS= read -r -d ',' token; do if [ -f ".ftp_ignore" ]; then
pattern=$(printf '%s' "$token" | sed 's/^[[:space:]]*"//;s/"[[:space:]]*$//') while IFS= read -r line; do
[ -n "$pattern" ] && IGNORE_PATTERNS+=("$pattern") [[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]] && continue
done <<< "${FTP_IGNORE}," regex=$(ftp_ignore_to_regex "$line")
[ -n "$regex" ] && IGNORE_PATTERNS+=("$regex") && IGNORE_SOURCES+=("$line")
done < ".ftp_ignore"
fi fi
# ── Walk src/ and classify every file ──────────────────────────────── # ── Walk src/ and classify every file ────────────────────────────────
@@ -180,9 +207,9 @@ jobs:
while IFS= read -r -d '' file; do while IFS= read -r -d '' file; do
rel="${file#${SOURCE_DIR}/}" rel="${file#${SOURCE_DIR}/}"
SKIP=false SKIP=false
for pat in "${IGNORE_PATTERNS[@]}"; do for i in "${!IGNORE_PATTERNS[@]}"; do
if echo "$rel" | grep -qE "$pat" 2>/dev/null; then if echo "$rel" | grep -qE "${IGNORE_PATTERNS[$i]}" 2>/dev/null; then
IGNORED_FILES+=("$rel | FTP_IGNORE \`$pat\`") IGNORED_FILES+=("$rel | .ftp_ignore \`${IGNORE_SOURCES[$i]}\`")
SKIP=true; break SKIP=true; break
fi fi
done done
@@ -262,13 +289,10 @@ jobs:
if: steps.source.outputs.skip == 'false' if: steps.source.outputs.skip == 'false'
id: remote id: remote
env: env:
DEV_FTP_PATH: ${{ vars.DEV_FTP_PATH }} DEV_FTP_PATH: ${{ vars.DEV_FTP_PATH }}
DEV_FTP_PATH_SUFFIX: ${{ vars.DEV_FTP_PATH_SUFFIX }} DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
CUSTOM_FOLDER: ${{ vars.CUSTOM_FOLDER }}
run: | run: |
BASE="$DEV_FTP_PATH" BASE="$DEV_FTP_PATH"
SUFFIX="$DEV_FTP_PATH_SUFFIX"
CUSTOM="$CUSTOM_FOLDER"
if [ -z "$BASE" ]; then if [ -z "$BASE" ]; then
echo "❌ DEV_FTP_PATH is not set." echo "❌ DEV_FTP_PATH is not set."
@@ -277,24 +301,50 @@ jobs:
exit 1 exit 1
fi fi
# Always append suffix when set — path is BASE/SUFFIX # DEV_FTP_SUFFIX is required — it identifies the remote subdirectory for this repo.
if [ -n "$SUFFIX" ]; then # Without it we cannot safely determine the deployment target.
REMOTE="${BASE%/}/${SUFFIX#/}" if [ -z "$DEV_FTP_SUFFIX" ]; then
else echo "⏭️ DEV_FTP_SUFFIX variable is not set — skipping deployment."
REMOTE="$BASE" echo " Set DEV_FTP_SUFFIX as a repo or org variable to enable deploy-dev."
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "path=" >> "$GITHUB_OUTPUT"
exit 0
fi fi
# Append CUSTOM_FOLDER when set — makes Dolibarr module paths automatic REMOTE="${BASE%/}/${DEV_FTP_SUFFIX#/}"
if [ -n "$CUSTOM" ]; then
REMOTE="${REMOTE%/}/${CUSTOM#/}" # ── Platform-specific path safety guards ──────────────────────────────
echo " CUSTOM_FOLDER appended: ${CUSTOM}" PLATFORM=""
if [ -f ".moko-standards" ]; then
PLATFORM=$(grep -E '^platform:' .moko-standards | sed 's/.*:[[:space:]]*//' | tr -d '"')
fi fi
if [ "$PLATFORM" = "crm-module" ]; then
# Dolibarr modules must deploy under htdocs/custom/ — guard against
# accidentally overwriting server root or unrelated directories.
if [[ "$REMOTE" != *custom* ]]; then
echo "❌ Safety check failed: Dolibarr (crm-module) remote path must contain 'custom'."
echo " Current path: ${REMOTE}"
echo " Set DEV_FTP_SUFFIX to the module's htdocs/custom/ subdirectory."
exit 1
fi
fi
if [ "$PLATFORM" = "waas-component" ]; then
# Joomla extensions may only deploy to the server's tmp/ directory.
if [[ "$REMOTE" != *tmp* ]]; then
echo "❌ Safety check failed: Joomla (waas-component) remote path must contain 'tmp'."
echo " Current path: ${REMOTE}"
echo " Set DEV_FTP_SUFFIX to a path under the server tmp/ directory."
exit 1
fi
fi
echo " Remote path: ${REMOTE}"
echo "path=${REMOTE}" >> "$GITHUB_OUTPUT" echo "path=${REMOTE}" >> "$GITHUB_OUTPUT"
echo "Remote path: ${REMOTE}"
- name: Detect SFTP authentication method - name: Detect SFTP authentication method
if: steps.source.outputs.skip == 'false' if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
id: auth id: auth
env: env:
HAS_KEY: ${{ secrets.DEV_FTP_KEY }} HAS_KEY: ${{ secrets.DEV_FTP_KEY }}
@@ -326,14 +376,14 @@ jobs:
fi fi
- name: Setup PHP - name: Setup PHP
if: steps.source.outputs.skip == 'false' if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.31.0 uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.31.0
with: with:
php-version: '8.1' php-version: '8.1'
tools: composer tools: composer
- name: Setup MokoStandards deploy tools - name: Setup MokoStandards deploy tools
if: steps.source.outputs.skip == 'false' if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
env: env:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}'
@@ -345,9 +395,7 @@ jobs:
composer install --no-dev --no-interaction --quiet composer install --no-dev --no-interaction --quiet
- name: Clear remote destination folder - name: Clear remote destination folder
if: >- if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
steps.source.outputs.skip == 'false' &&
inputs.clear_remote == true
env: env:
SFTP_HOST: ${{ steps.conn.outputs.host }} SFTP_HOST: ${{ steps.conn.outputs.host }}
SFTP_PORT: ${{ steps.conn.outputs.port }} SFTP_PORT: ${{ steps.conn.outputs.port }}
@@ -449,7 +497,7 @@ jobs:
php /tmp/moko_clear.php php /tmp/moko_clear.php
- name: Deploy via SFTP - name: Deploy via SFTP
if: steps.source.outputs.skip == 'false' if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
env: env:
SFTP_HOST: ${{ steps.conn.outputs.host }} SFTP_HOST: ${{ steps.conn.outputs.host }}
SFTP_PORT: ${{ steps.conn.outputs.port }} SFTP_PORT: ${{ steps.conn.outputs.port }}