# Copyright (C) 2026 Moko Consulting # # This file is part of a Moko Consulting project. # # SPDX-License-Identifier: GPL-3.0-or-later # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: MokoStandards.Deploy # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API # PATH: /templates/workflows/shared/deploy-dev.yml.template # VERSION: 04.06.00 # BRIEF: SFTP deployment workflow for development server — synced to all governed repos # NOTE: Synced via bulk-repo-sync to .github/workflows/deploy-dev.yml in all governed repos. # Port is resolved in order: DEV_FTP_PORT variable → :port suffix in DEV_FTP_HOST → 22. name: Deploy to Dev Server (SFTP) # Deploys the contents of the src/ directory to the development server via SFTP. # Triggers on every pull_request to development branches (so the dev server always # reflects the latest PR state) and on push/merge to main branches. # # Required org-level variables: DEV_FTP_HOST, DEV_FTP_PATH, DEV_FTP_USERNAME # Optional org-level variable: DEV_FTP_PORT (auto-detected from host or defaults to 22) # Optional org/repo variable: DEV_FTP_SUFFIX — when set, appended to DEV_FTP_PATH to form the # full remote destination: DEV_FTP_PATH/DEV_FTP_SUFFIX # Ignore rules: Place a .ftpignore file in the src/ directory. Each non-empty, # non-comment line is a glob pattern tested against the relative path # of each file (e.g. "subdir/file.txt"). The .gitignore is NOT used. # Required org-level secret: DEV_FTP_KEY (preferred) or DEV_FTP_PASSWORD # # Access control: only users with admin or maintain role on the repository may deploy. on: pull_request: types: [closed] branches: - 'dev/**' - 'rc/**' - develop - development paths: - 'src/**' - 'htdocs/**' workflow_dispatch: inputs: clear_remote: description: 'Delete all files inside the remote destination folder before uploading' required: false default: false type: boolean permissions: contents: read pull-requests: write env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true jobs: check-permission: name: Verify Deployment Permission runs-on: ubuntu-latest steps: - name: Check actor permission env: # Prefer the org-scoped GH_TOKEN secret (needed for the org membership # fallback). Falls back to the built-in github.token so the collaborator # endpoint still works even if GH_TOKEN is not configured. GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} run: | ACTOR="${{ github.actor }}" REPO="${{ github.repository }}" ORG="${{ github.repository_owner }}" METHOD="" AUTHORIZED="false" # Hardcoded authorized users — always allowed to deploy AUTHORIZED_USERS="jmiller gitea-actions[bot]" for user in $AUTHORIZED_USERS; do if [ "$ACTOR" = "$user" ]; then AUTHORIZED="true" METHOD="hardcoded allowlist" PERMISSION="admin" break fi done # For other actors, check repo/org permissions via API if [ "$AUTHORIZED" != "true" ]; then PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/collaborators/${ACTOR}/permission" 2>/dev/null \ 2>/dev/null | jq -r '.permission') METHOD="repo collaborator API" if [ -z "$PERMISSION" ]; then ORG_ROLE=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/orgs/${ORG}/memberships/${ACTOR}" \ 2>/dev/null | jq -r '.role') METHOD="org membership API" if [ "$ORG_ROLE" = "owner" ]; then PERMISSION="admin" else PERMISSION="none" fi fi case "$PERMISSION" in admin|maintain) AUTHORIZED="true" ;; esac fi # Write detailed summary { echo "## 🔐 Deploy Authorization" echo "" echo "| Field | Value |" echo "|-------|-------|" echo "| **Actor** | \`${ACTOR}\` |" echo "| **Repository** | \`${REPO}\` |" echo "| **Permission** | \`${PERMISSION}\` |" echo "| **Method** | ${METHOD} |" echo "| **Authorized** | ${AUTHORIZED} |" echo "| **Trigger** | \`${{ github.event_name }}\` |" echo "| **Branch** | \`${{ github.ref_name }}\` |" echo "" } >> "$GITHUB_STEP_SUMMARY" if [ "$AUTHORIZED" = "true" ]; then echo "✅ ${ACTOR} authorized to deploy (${METHOD})" >> "$GITHUB_STEP_SUMMARY" else echo "❌ ${ACTOR} is NOT authorized to deploy." >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" echo "Deployment requires one of:" >> "$GITHUB_STEP_SUMMARY" echo "- Being in the hardcoded allowlist" >> "$GITHUB_STEP_SUMMARY" echo "- Having \`admin\` or \`maintain\` role on the repository" >> "$GITHUB_STEP_SUMMARY" exit 1 fi deploy: name: SFTP Deploy → Dev runs-on: ubuntu-latest needs: [check-permission] if: >- github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Resolve source directory id: source run: | # Resolve source directory: src/ preferred, htdocs/ as fallback if [ -d "src" ]; then SRC="src" elif [ -d "htdocs" ]; then SRC="htdocs" else echo "⚠️ No src/ or htdocs/ directory found — skipping deployment" echo "skip=true" >> "$GITHUB_OUTPUT" exit 0 fi COUNT=$(find "$SRC" -type f | wc -l) echo "✅ Source: ${SRC}/ (${COUNT} file(s))" echo "skip=false" >> "$GITHUB_OUTPUT" echo "dir=${SRC}" >> "$GITHUB_OUTPUT" - name: Preview files to deploy if: steps.source.outputs.skip == 'false' env: SOURCE_DIR: ${{ steps.source.outputs.dir }} run: | # ── Convert a ftpignore-style glob line to an ERE pattern ────────────── ftpignore_to_regex() { local line="$1" local anchored=false # Strip inline comments and whitespace line=$(printf '%s' "$line" | sed 's/[[:space:]]*#.*$//' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') [ -z "$line" ] && return # Skip negation patterns (not supported) [[ "$line" == !* ]] && return # Trailing slash = directory marker; strip it line="${line%/}" # Leading slash = anchored to root; strip it if [[ "$line" == /* ]]; then anchored=true line="${line#/}" fi # Escape ERE special chars, then restore glob semantics local regex regex=$(printf '%s' "$line" \ | sed 's/[.+^${}()|[\\]/\\&/g' \ | sed 's/\\\*\\\*/\x01/g' \ | sed 's/\\\*/[^\/]*/g' \ | sed 's/\x01/.*/g' \ | sed 's/\\\?/[^\/]/g') if $anchored; then printf '^%s(/|$)' "$regex" else printf '(^|/)%s(/|$)' "$regex" fi } # ── Read .ftpignore (ftpignore-style globs) ───────────────────────── IGNORE_PATTERNS=() IGNORE_SOURCES=() if [ -f "${SOURCE_DIR}/.ftpignore" ]; then while IFS= read -r line; do [[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]] && continue regex=$(ftpignore_to_regex "$line") [ -n "$regex" ] && IGNORE_PATTERNS+=("$regex") && IGNORE_SOURCES+=("$line") done < "${SOURCE_DIR}/.ftpignore" fi # ── Walk src/ and classify every file ──────────────────────────────── WILL_UPLOAD=() IGNORED_FILES=() while IFS= read -r -d '' file; do rel="${file#${SOURCE_DIR}/}" SKIP=false for i in "${!IGNORE_PATTERNS[@]}"; do if echo "$rel" | grep -qE "${IGNORE_PATTERNS[$i]}" 2>/dev/null; then IGNORED_FILES+=("$rel | .ftpignore \`${IGNORE_SOURCES[$i]}\`") SKIP=true; break fi done $SKIP && continue WILL_UPLOAD+=("$rel") done < <(find "$SOURCE_DIR" -type f -print0 | sort -z) UPLOAD_COUNT="${#WILL_UPLOAD[@]}" IGNORE_COUNT="${#IGNORED_FILES[@]}" echo "ℹ️ ${UPLOAD_COUNT} file(s) will be uploaded, ${IGNORE_COUNT} ignored" # ── Write deployment preview to step summary ────────────────────────── { echo "## 📋 Deployment Preview" echo "" echo "| Field | Value |" echo "|---|---|" echo "| Source | \`${SOURCE_DIR}/\` |" echo "| Files to upload | **${UPLOAD_COUNT}** |" echo "| Files ignored | **${IGNORE_COUNT}** |" echo "" if [ "${UPLOAD_COUNT}" -gt 0 ]; then echo "### 📂 Files that will be uploaded" echo '```' printf '%s\n' "${WILL_UPLOAD[@]}" echo '```' echo "" fi if [ "${IGNORE_COUNT}" -gt 0 ]; then echo "### ⏭️ Files excluded" echo "| File | Reason |" echo "|---|---|" for entry in "${IGNORED_FILES[@]}"; do f="${entry% | *}"; r="${entry##* | }" echo "| \`${f}\` | ${r} |" done echo "" fi } >> "$GITHUB_STEP_SUMMARY" - name: Resolve SFTP host and port if: steps.source.outputs.skip == 'false' id: conn env: HOST_RAW: ${{ vars.DEV_FTP_HOST }} PORT_VAR: ${{ vars.DEV_FTP_PORT }} run: | HOST="$HOST_RAW" PORT="$PORT_VAR" # Priority 1 — explicit DEV_FTP_PORT variable if [ -n "$PORT" ]; then echo "ℹ️ Using explicit DEV_FTP_PORT=${PORT}" # Priority 2 — port embedded in DEV_FTP_HOST (host:port) elif [[ "$HOST" == *:* ]]; then PORT="${HOST##*:}" HOST="${HOST%:*}" echo "ℹ️ Extracted port ${PORT} from DEV_FTP_HOST" # Priority 3 — SFTP default else PORT="22" echo "ℹ️ No port specified — defaulting to SFTP port 22" fi echo "host=${HOST}" >> "$GITHUB_OUTPUT" echo "port=${PORT}" >> "$GITHUB_OUTPUT" echo "SFTP target: ${HOST}:${PORT}" - name: Build remote path if: steps.source.outputs.skip == 'false' id: remote env: DEV_FTP_PATH: ${{ vars.DEV_FTP_PATH }} DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }} run: | BASE="$DEV_FTP_PATH" if [ -z "$BASE" ]; then echo "❌ DEV_FTP_PATH is not set." echo " Configure it as an org-level variable (Settings → Variables) and" echo " ensure this repository has been granted access to it." exit 1 fi # DEV_FTP_SUFFIX is required — it identifies the remote subdirectory for this repo. # Without it we cannot safely determine the deployment target. if [ -z "$DEV_FTP_SUFFIX" ]; then echo "⏭️ DEV_FTP_SUFFIX variable is not set — skipping deployment." echo " Set DEV_FTP_SUFFIX as a repo or org variable to enable deploy-dev." echo "skip=true" >> "$GITHUB_OUTPUT" echo "path=" >> "$GITHUB_OUTPUT" exit 0 fi REMOTE="${BASE%/}/${DEV_FTP_SUFFIX#/}" # ── Platform-specific path safety guards ────────────────────────────── PLATFORM="" MOKO_FILE=".github/.mokostandards"; [ ! -f "$MOKO_FILE" ] && MOKO_FILE=".mokostandards"; if [ -f "$MOKO_FILE" ]; then PLATFORM=$(grep -oP '^platform:.*' "$MOKO_FILE" 2>/dev/null || true) fi if [ "$PLATFORM" = "crm-module" ]; then # Dolibarr modules must deploy under htdocs/custom/ — guard against # accidentally overwriting server root or unrelated directories. if [[ "$REMOTE" != *custom* ]]; then echo "❌ Safety check failed: Dolibarr (crm-module) remote path must contain 'custom'." echo " Current path: ${REMOTE}" echo " Set DEV_FTP_SUFFIX to the module's htdocs/custom/ subdirectory." exit 1 fi fi if [ "$PLATFORM" = "waas-component" ]; then # Joomla extensions may only deploy to the server's tmp/ directory. if [[ "$REMOTE" != *tmp* ]]; then echo "❌ Safety check failed: Joomla (waas-component) remote path must contain 'tmp'." echo " Current path: ${REMOTE}" echo " Set DEV_FTP_SUFFIX to a path under the server tmp/ directory." exit 1 fi fi echo "ℹ️ Remote path: ${REMOTE}" echo "path=${REMOTE}" >> "$GITHUB_OUTPUT" - name: Detect SFTP authentication method if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true' id: auth env: HAS_KEY: ${{ secrets.DEV_FTP_KEY }} HAS_PASSWORD: ${{ secrets.DEV_FTP_PASSWORD }} run: | if [ -n "$HAS_KEY" ] && [ -n "$HAS_PASSWORD" ]; then # Both set: key auth with password as passphrase; falls back to password-only if key fails echo "method=key" >> "$GITHUB_OUTPUT" echo "use_passphrase=true" >> "$GITHUB_OUTPUT" echo "has_password=true" >> "$GITHUB_OUTPUT" echo "ℹ️ Primary: SSH key + passphrase (DEV_FTP_KEY / DEV_FTP_PASSWORD)" echo "ℹ️ Fallback: password-only auth if key authentication fails" elif [ -n "$HAS_KEY" ]; then # Key only: no passphrase, no password fallback echo "method=key" >> "$GITHUB_OUTPUT" echo "use_passphrase=false" >> "$GITHUB_OUTPUT" echo "has_password=false" >> "$GITHUB_OUTPUT" echo "ℹ️ Using SSH key authentication (DEV_FTP_KEY, no passphrase, no fallback)" elif [ -n "$HAS_PASSWORD" ]; then # Password only: direct SFTP password auth echo "method=password" >> "$GITHUB_OUTPUT" echo "use_passphrase=false" >> "$GITHUB_OUTPUT" echo "has_password=true" >> "$GITHUB_OUTPUT" echo "ℹ️ Using password authentication (DEV_FTP_PASSWORD)" else echo "❌ No SFTP credentials configured." echo " Set DEV_FTP_KEY (preferred) or DEV_FTP_PASSWORD as an org-level secret." exit 1 fi - name: Setup PHP if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true' run: | php -v && composer --version - name: Setup MokoStandards deploy tools if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true' env: GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }} COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}' run: | git clone --depth 1 --branch {{standards_branch}} --quiet \ "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \ /tmp/mokostandards-api cd /tmp/mokostandards-api composer install --no-dev --no-interaction --quiet - name: Clear remote destination folder (manual only) if: >- steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true' && inputs.clear_remote == true env: SFTP_HOST: ${{ steps.conn.outputs.host }} SFTP_PORT: ${{ steps.conn.outputs.port }} SFTP_USER: ${{ vars.DEV_FTP_USERNAME }} SFTP_KEY: ${{ secrets.DEV_FTP_KEY }} SFTP_PASSWORD: ${{ secrets.DEV_FTP_PASSWORD }} AUTH_METHOD: ${{ steps.auth.outputs.method }} USE_PASSPHRASE: ${{ steps.auth.outputs.use_passphrase }} HAS_PASSWORD: ${{ steps.auth.outputs.has_password }} REMOTE_PATH: ${{ steps.remote.outputs.path }} run: | cat > /tmp/moko_clear.php << 'PHPEOF' login($username, $key)) { if ($password !== '') { echo "⚠️ Key auth failed — falling back to password\n"; if (!$sftp->login($username, $password)) { fwrite(STDERR, "❌ Both key and password authentication failed\n"); exit(1); } echo "✅ Connected via password authentication (key fallback)\n"; } else { fwrite(STDERR, "❌ Key authentication failed and no password fallback is available\n"); exit(1); } } else { echo "✅ Connected via SSH key authentication\n"; } } else { if (!$sftp->login($username, (string) getenv('SFTP_PASSWORD'))) { fwrite(STDERR, "❌ Password authentication failed\n"); exit(1); } echo "✅ Connected via password authentication\n"; } // ── Recursive delete ──────────────────────────────────────────── function rmrf(SFTP $sftp, string $path): void { $entries = $sftp->nlist($path); if ($entries === false) { return; // path does not exist — nothing to clear } foreach ($entries as $name) { if ($name === '.' || $name === '..') { continue; } $entry = "{$path}/{$name}"; if ($sftp->is_dir($entry)) { rmrf($sftp, $entry); $sftp->rmdir($entry); echo " 🗑️ Removed dir: {$entry}\n"; } else { $sftp->delete($entry); echo " 🗑️ Removed file: {$entry}\n"; } } } // ── Create remote directory tree ──────────────────────────────── function sftpMakedirs(SFTP $sftp, string $path): void { $parts = array_values(array_filter(explode('/', $path), fn(string $p) => $p !== '')); $current = str_starts_with($path, '/') ? '' : ''; foreach ($parts as $part) { $current .= '/' . $part; $sftp->mkdir($current); // silently returns false if already exists } } rmrf($sftp, $remotePath); sftpMakedirs($sftp, $remotePath); echo "✅ Remote folder ready: {$remotePath}\n"; PHPEOF php /tmp/moko_clear.php - name: Deploy via SFTP if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true' env: SFTP_HOST: ${{ steps.conn.outputs.host }} SFTP_PORT: ${{ steps.conn.outputs.port }} SFTP_USER: ${{ vars.DEV_FTP_USERNAME }} SFTP_KEY: ${{ secrets.DEV_FTP_KEY }} SFTP_PASSWORD: ${{ secrets.DEV_FTP_PASSWORD }} AUTH_METHOD: ${{ steps.auth.outputs.method }} USE_PASSPHRASE: ${{ steps.auth.outputs.use_passphrase }} REMOTE_PATH: ${{ steps.remote.outputs.path }} SOURCE_DIR: ${{ steps.source.outputs.dir }} run: | # ── Write SSH key to temp file (key auth only) ──────────────────────── if [ "$AUTH_METHOD" = "key" ]; then printf '%s' "$SFTP_KEY" > /tmp/deploy_key chmod 600 /tmp/deploy_key fi # ── Generate sftp-config.json safely via jq ─────────────────────────── if [ "$AUTH_METHOD" = "key" ]; then jq -n \ --arg host "$SFTP_HOST" \ --argjson port "${SFTP_PORT:-22}" \ --arg user "$SFTP_USER" \ --arg path "$REMOTE_PATH" \ --arg key "/tmp/deploy_key" \ '{host:$host, port:$port, user:$user, remote_path:$path, ssh_key_file:$key}' \ > /tmp/sftp-config.json else jq -n \ --arg host "$SFTP_HOST" \ --argjson port "${SFTP_PORT:-22}" \ --arg user "$SFTP_USER" \ --arg path "$REMOTE_PATH" \ --arg pass "$SFTP_PASSWORD" \ '{host:$host, port:$port, user:$user, remote_path:$path, password:$pass}' \ > /tmp/sftp-config.json fi # Dev deploys skip minified files — use unminified sources for debugging echo "*.min.js" >> "${SOURCE_DIR}/.ftpignore" echo "*.min.css" >> "${SOURCE_DIR}/.ftpignore" # ── Run deploy-sftp.php from MokoStandards ──────────────────────────── DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json) if [ "$USE_PASSPHRASE" = "true" ]; then DEPLOY_ARGS+=(--key-passphrase "$SFTP_PASSWORD") fi # Set platform version to "development" before deploy (Dolibarr + Joomla) php /tmp/mokostandards-api/cli/version_set_platform.php --path . --version development # Write update files — dev/** = development, rc/** = rc PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true) REPO="${{ github.repository }}" BRANCH="${{ github.ref_name }}" # Determine stability tag from branch prefix STABILITY="development" VERSION_LABEL="development" if [[ "$BRANCH" == rc/* ]]; then STABILITY="rc" VERSION_LABEL=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "${BRANCH#rc/}")-rc fi if [ "$PLATFORM" = "crm-module" ]; then printf '%s' "$VERSION_LABEL" > update.txt fi if [ "$PLATFORM" = "waas-component" ]; then MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1 || true) if [ -n "$MANIFEST" ]; then EXT_NAME=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "${{ github.event.repository.name }}") EXT_TYPE=$(grep -oP ']+type="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "component") EXT_ELEMENT=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || basename "$MANIFEST" .xml) EXT_CLIENT=$(grep -oP ']+client="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "") EXT_FOLDER=$(grep -oP ']+group="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "") TARGET_PLATFORM=$(grep -oP '/dev/null | head -1 || true) [ -n "$TARGET_PLATFORM" ] && TARGET_PLATFORM="${TARGET_PLATFORM}>" [ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '' "/") CLIENT_TAG="" if [ -n "$EXT_CLIENT" ]; then CLIENT_TAG="${EXT_CLIENT}" elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then CLIENT_TAG="site" fi FOLDER_TAG="" if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then FOLDER_TAG="${EXT_FOLDER}" fi DOWNLOAD_URL="https://git.mokoconsulting.tech/${{ github.repository }}/archive/refs/heads/${BRANCH}.zip" { printf '%s\n' '' printf '%s\n' '' printf '%s\n' ' ' printf '%s\n' " ${EXT_NAME}" printf '%s\n' " ${EXT_NAME} ${STABILITY} build" printf '%s\n' " ${EXT_ELEMENT}" printf '%s\n' " ${EXT_TYPE}" printf '%s\n' " ${VERSION_LABEL}" [ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}" [ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}" printf '%s\n' ' ' printf '%s\n' " ${STABILITY}" printf '%s\n' ' ' printf '%s\n' " https://git.mokoconsulting.tech/${{ github.repository }}/tree/${BRANCH}" printf '%s\n' ' ' printf '%s\n' " ${DOWNLOAD_URL}" printf '%s\n' ' ' printf '%s\n' " ${TARGET_PLATFORM}" printf '%s\n' ' Moko Consulting' printf '%s\n' ' https://mokoconsulting.tech' printf '%s\n' ' ' printf '%s\n' '' } > updates.xml sed -i '/^[[:space:]]*$/d' updates.xml fi fi # Use Joomla-aware deploy for waas-component (routes files to correct Joomla dirs) # Use standard SFTP deploy for everything else PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true) if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then php /tmp/mokostandards-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}" else php /tmp/mokostandards-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}" fi # (both scripts handle dotfile skipping and .ftpignore natively) # Remove temp files that should never be left behind rm -f /tmp/deploy_key /tmp/sftp-config.json # Dev deploys fail silently — no issue creation. # Demo and RS deploys create failure issues (production-facing). - name: Deployment summary if: always() run: | if [ "${{ steps.source.outputs.skip }}" == "true" ]; then echo "### ⏭️ Deployment Skipped" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" echo "No \`src/\` directory found in this repository." >> "$GITHUB_STEP_SUMMARY" elif [ "${{ job.status }}" == "success" ]; then echo "" >> "$GITHUB_STEP_SUMMARY" echo "### ✅ Dev Deployment Successful" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" echo "| Field | Value |" >> "$GITHUB_STEP_SUMMARY" echo "|-------|-------|" >> "$GITHUB_STEP_SUMMARY" echo "| Host | \`${{ steps.conn.outputs.host }}:${{ steps.conn.outputs.port }}\` |" >> "$GITHUB_STEP_SUMMARY" echo "| Remote path | \`${{ steps.remote.outputs.path }}\` |" >> "$GITHUB_STEP_SUMMARY" echo "| Source | \`src/\` |" >> "$GITHUB_STEP_SUMMARY" echo "| Trigger | ${{ github.event_name }} |" >> "$GITHUB_STEP_SUMMARY" echo "| Auth | ${{ steps.auth.outputs.method }} |" >> "$GITHUB_STEP_SUMMARY" echo "| Clear remote | ${{ inputs.clear_remote || 'false' }} |" >> "$GITHUB_STEP_SUMMARY" else echo "### ❌ Dev Deployment Failed" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" echo "Check the job log above for error details." >> "$GITHUB_STEP_SUMMARY" fi