Full Gitea Actions CI/CD with cPanel git deployment
CI - Test & Lint / lint (push) Failing after 1m11s
CI - Test & Lint / test-schema (push) Failing after 8s
🚀 Deploy to cPanel via Git / ci-check (push) Failing after 8s
🚀 Deploy to cPanel via Git / deploy-to-cpanel (push) Has been skipped
🚀 Deploy to cPanel via Git / deploy-fallback (push) Has been skipped
🚀 Deploy to cPanel via Git / verify (push) Has been skipped
🌊 Sync cPanel Git + WordPress / sync-cpanel (push) Failing after 9s
CI - Test & Lint / security (push) Successful in 1m42s
🌊 Sync cPanel Git + WordPress / sync-nextcloud (push) Failing after 10s
CI - Test & Lint / lint (push) Failing after 1m11s
CI - Test & Lint / test-schema (push) Failing after 8s
🚀 Deploy to cPanel via Git / ci-check (push) Failing after 8s
🚀 Deploy to cPanel via Git / deploy-to-cpanel (push) Has been skipped
🚀 Deploy to cPanel via Git / deploy-fallback (push) Has been skipped
🚀 Deploy to cPanel via Git / verify (push) Has been skipped
🌊 Sync cPanel Git + WordPress / sync-cpanel (push) Failing after 9s
CI - Test & Lint / security (push) Successful in 1m42s
🌊 Sync cPanel Git + WordPress / sync-nextcloud (push) Failing after 10s
- CI workflow: PHP lint, security scan, schema validation - Deploy workflow: SSH -> cPanel git push + WP REST API fallback + verify - Sync workflow: cPanel sync + Nextcloud artifact upload - Health workflow: every 30min plugin health check - Scripts: verify-deploy, deploy-report, check-namespace, health-check, setup-secrets - README with full architecture documentation - Gitea Actions runner registered on Docker Swarm (contabo-swarm-runner)
This commit is contained in:
@@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Verify hermes/v1 namespace is registered in WP REST API."""
|
||||||
|
import json, sys
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.load(sys.stdin)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print("FAIL: Cannot parse WP index response")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
ns = data.get("namespaces", [])
|
||||||
|
if "hermes/v1" in ns:
|
||||||
|
print("HEALTHY: hermes/v1 namespace registered")
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
print(f"WARN: hermes/v1 NOT found. Namespaces: {ns}")
|
||||||
|
sys.exit(1)
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Extract and print plugin info from WP REST API response."""
|
||||||
|
import json, sys
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.load(sys.stdin)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print("FAIL: Cannot parse response")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if isinstance(data, dict):
|
||||||
|
code = data.get("code", "")
|
||||||
|
msg = data.get("message", str(data))
|
||||||
|
print(f"API response: [{code}] {msg[:200]}")
|
||||||
|
if code and "rest_plugin_install_error" not in code:
|
||||||
|
sys.exit(0 if "existing_plugin" in code else 1)
|
||||||
|
elif isinstance(data, list):
|
||||||
|
for p in data:
|
||||||
|
if isinstance(p, dict):
|
||||||
|
print(f"{p.get('name','?')} v{p.get('version','?')} status={p.get('status','?')}")
|
||||||
|
else:
|
||||||
|
print(f"Unexpected: {type(data).__name__}")
|
||||||
|
sys.exit(1)
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Health check for Hermes cPanel Agent via WP REST API.
|
||||||
|
Usage: cat plugins.json | python3 health-check.py
|
||||||
|
"""
|
||||||
|
import json, sys
|
||||||
|
|
||||||
|
def check():
|
||||||
|
try:
|
||||||
|
data = json.load(sys.stdin)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print("FAIL: Cannot parse WP API response")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if isinstance(data, dict) and "code" in data:
|
||||||
|
print(f"FAIL: WP API error - {data.get('message', 'unknown')}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not isinstance(data, list):
|
||||||
|
print(f"WARN: Unexpected response format")
|
||||||
|
return True
|
||||||
|
|
||||||
|
found = False
|
||||||
|
for p in data:
|
||||||
|
if isinstance(p, dict) and p.get("textdomain") == "hermes-ai-bridge":
|
||||||
|
status = p.get("status", "unknown")
|
||||||
|
ver = p.get("version", "?")
|
||||||
|
name = p.get("name", "?")
|
||||||
|
found = True
|
||||||
|
if status == "active":
|
||||||
|
print(f"HEALTHY: {name} v{ver} status={status}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"DEGRADED: {name} v{ver} status={status}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
print("FAIL: hermes-cpanel-agent plugin not found on WordPress")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
ok = check()
|
||||||
|
sys.exit(0 if ok else 1)
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Setup script: initializes required secrets and cPanel git remote.
|
||||||
|
Run interactively. Output is safe to log.
|
||||||
|
"""
|
||||||
|
import json, os, sys
|
||||||
|
|
||||||
|
REQUIRED_SECRETS = {
|
||||||
|
"WP_APP_PASSWORD": "WordPress Application Password for wmj user",
|
||||||
|
"WP_URL": "WordPress site URL (default: https://ummah.falahos.my)",
|
||||||
|
"WP_APP_USER": "WordPress username for app password (default: wmj)",
|
||||||
|
"CPANEL_SSH_KEY": "cPanel Git SSH private key",
|
||||||
|
"CPANEL_GIT_REMOTE": "cPanel git remote URL (e.g., ssh://user@host:port/path)",
|
||||||
|
"CPANEL_HOST": "cPanel hostname for SSH key scan",
|
||||||
|
"CPANEL_DEPLOY_HOOK_URL": "(Optional) cPanel deployment hook URL",
|
||||||
|
"NEXTCLOUD_HERMES_PASS": "Nextcloud password for hermes user",
|
||||||
|
}
|
||||||
|
|
||||||
|
SECRETS_TO_SET = [
|
||||||
|
"WP_APP_PASSWORD",
|
||||||
|
"WP_URL",
|
||||||
|
"WP_APP_USER",
|
||||||
|
"CPANEL_SSH_KEY",
|
||||||
|
"CPANEL_GIT_REMOTE",
|
||||||
|
"CPANEL_HOST",
|
||||||
|
"NEXTCLOUD_HERMES_PASS",
|
||||||
|
]
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=== Hermes cPanel Agent: Gitea Secrets Setup ===")
|
||||||
|
print()
|
||||||
|
print("The following secrets are required for CI/CD workflows:\n")
|
||||||
|
for name, desc in REQUIRED_SECRETS.items():
|
||||||
|
print(f" {name:30s} - {desc}")
|
||||||
|
|
||||||
|
print("\n---")
|
||||||
|
print("To set each secret via Gitea API:")
|
||||||
|
print()
|
||||||
|
|
||||||
|
repo = "wmj/hermes-cpanel-agent"
|
||||||
|
for secret in SECRETS_TO_SET:
|
||||||
|
print(f" # Set {secret}")
|
||||||
|
print(f' curl -s -X PUT -u "wmj:YOUR_PASSWORD" \\')
|
||||||
|
print(f' -H "Content-Type: application/json" \\')
|
||||||
|
print(f' -d \'{{"data":"VALUE_BASE64"}}\' \\')
|
||||||
|
print(f' "https://git.falahos.my/api/v1/repos/{repo}/actions/secrets/{secret}"')
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("=== Alternative: Set via Gitea Web UI ===")
|
||||||
|
print(f" Go to: https://git.falahos.my/{repo}/settings/actions/secrets")
|
||||||
|
print()
|
||||||
|
|
||||||
|
if len(sys.argv) > 1 and sys.argv[1] == "--init":
|
||||||
|
print("WARNING: --init is interactive. Only run in a terminal session.")
|
||||||
|
do_init(repo)
|
||||||
|
|
||||||
|
def do_init(repo):
|
||||||
|
"""Interactively set secrets (requires terminal)."""
|
||||||
|
import getpass
|
||||||
|
pw = getpass.getpass("Gitea password for wmj: ")
|
||||||
|
for secret in SECRETS_TO_SET:
|
||||||
|
val = input(f"{secret} [{REQUIRED_SECRETS.get(secret, '')}]: ")
|
||||||
|
if val:
|
||||||
|
import base64
|
||||||
|
encoded = base64.b64encode(val.encode()).decode()
|
||||||
|
cmd = (
|
||||||
|
f'curl -s -X PUT -u "wmj:{pw}" '
|
||||||
|
f'-H "Content-Type: application/json" '
|
||||||
|
f'-d \'{{"data":"{encoded}"}}\' '
|
||||||
|
f'"https://git.falahos.my/api/v1/repos/{repo}/actions/secrets/{secret}"'
|
||||||
|
)
|
||||||
|
result = os.popen(cmd).read()
|
||||||
|
print(f" → {result[:100]}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Verify Hermes cPanel Agent deployment and REST API availability."""
|
||||||
|
import json, sys, os
|
||||||
|
|
||||||
|
def check_plugins():
|
||||||
|
"""Verify plugin is installed and active via WP REST API."""
|
||||||
|
try:
|
||||||
|
data = json.load(sys.stdin)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return False, "Cannot parse WP REST API response"
|
||||||
|
|
||||||
|
if isinstance(data, dict) and "code" in data:
|
||||||
|
return False, f"WP API error: {data.get('message', str(data))}"
|
||||||
|
|
||||||
|
if not isinstance(data, list):
|
||||||
|
return False, f"Unexpected response: {type(data).__name__}"
|
||||||
|
|
||||||
|
for p in data:
|
||||||
|
if isinstance(p, dict) and p.get("textdomain") == "hermes-ai-bridge":
|
||||||
|
status = p.get("status", "unknown")
|
||||||
|
name = p.get("name", "?")
|
||||||
|
ver = p.get("version", "?")
|
||||||
|
return True, f"OK: {name} v{ver} status={status}"
|
||||||
|
|
||||||
|
return False, "Plugin not found in WP plugins list"
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
ok, msg = check_plugins()
|
||||||
|
print(msg)
|
||||||
|
sys.exit(0 if ok else 1)
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
name: CI - Test & Lint
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main, develop]
|
||||||
|
push:
|
||||||
|
branches: [main, develop]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: PHP Syntax Check
|
||||||
|
run: |
|
||||||
|
ERR=0
|
||||||
|
while IFS= read -r f; do
|
||||||
|
php -l "$f" || ERR=1
|
||||||
|
done < <(find . -name "*.php" -not -path "./.gitea/*")
|
||||||
|
exit $ERR
|
||||||
|
|
||||||
|
- name: Check for banned functions
|
||||||
|
run: |
|
||||||
|
BANNED="exec\|shell_exec\|system\|passthru\|popen\|proc_open\|eval\|assert\|pcntl_fork"
|
||||||
|
if grep -rn "$BANNED" --include="*.php" . 2>/dev/null | grep -v "BANNED="; then
|
||||||
|
echo "FAILED: Banned function found!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "OK: No banned functions — safe for RoketServer cPanel"
|
||||||
|
|
||||||
|
- name: Validate WP plugin headers
|
||||||
|
run: |
|
||||||
|
for HEADER in "Plugin Name" "Version" "Description" "Author"; do
|
||||||
|
grep -q "$HEADER:" hermes-ai-bridge.php || { echo "Missing: $HEADER"; exit 1; }
|
||||||
|
done
|
||||||
|
echo "OK: All required plugin headers present"
|
||||||
|
|
||||||
|
security:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Scan for hardcoded secrets
|
||||||
|
run: |
|
||||||
|
# Check for accidental credential commits
|
||||||
|
if grep -rn "password\|secret\|api_key\|auth_token" --include="*.php" \
|
||||||
|
--include="*.json" . 2>/dev/null | grep -v "WP_APP_PASSWORD\|get_option\|secrets\.\|noreply"; then
|
||||||
|
echo "WARNING: Possible secrets in code:"
|
||||||
|
grep -rn "password\|secret\|api_key\|auth_token" --include="*.php" \
|
||||||
|
--include="*.json" . | grep -v "WP_APP_PASSWORD\|get_option\|secrets\.\|noreply"
|
||||||
|
fi
|
||||||
|
echo "OK: Secret scan complete"
|
||||||
|
|
||||||
|
- name: Check file permissions
|
||||||
|
run: |
|
||||||
|
find . -perm /o+w -not -path "./.git/*" -not -path "./.gitea/*" \
|
||||||
|
-not -path "./.github/*" 2>/dev/null | while read f; do
|
||||||
|
echo "WARNING: World-writable: $f"
|
||||||
|
done
|
||||||
|
|
||||||
|
test-schema:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Verify REST API namespace is declared
|
||||||
|
run: |
|
||||||
|
if grep -q "namespace.*hermes/v1" includes/rest-controller.php; then
|
||||||
|
echo "OK: REST namespace hermes/v1 found"
|
||||||
|
else
|
||||||
|
echo "FAILED: REST namespace hermes/v1 missing"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Verify autoloader loads all classes
|
||||||
|
run: |
|
||||||
|
for CLASS in Core Installer RestController AiProvider Scheduler NextcloudBridge; do
|
||||||
|
if grep -q "$CLASS" includes/class-autoloader.php; then
|
||||||
|
echo "OK: $CLASS mapped in autoloader"
|
||||||
|
else
|
||||||
|
echo "FAILED: $CLASS not in autoloader"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
name: 🚀 Deploy to cPanel via Git
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ci-check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: PHP Lint
|
||||||
|
run: |
|
||||||
|
ERROR=0
|
||||||
|
for f in $(find . -name "*.php" -not -path "./.gitea/*"); do
|
||||||
|
php -l "$f" || ERROR=1
|
||||||
|
done
|
||||||
|
exit $ERROR
|
||||||
|
|
||||||
|
- name: Security scan
|
||||||
|
run: |
|
||||||
|
if grep -rn "exec\|shell_exec\|system\|passthru\|eval\|assert" \
|
||||||
|
--include="*.php" . | grep -v "BANNED=\|_exec\|exec_\|get_option"; then
|
||||||
|
echo "FAILED: Banned function found!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "OK: Security check passed"
|
||||||
|
|
||||||
|
deploy-to-cpanel:
|
||||||
|
needs: [ci-check]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup SSH for cPanel Git
|
||||||
|
env:
|
||||||
|
CPANEL_SSH_KEY: ${{ secrets.CPANEL_SSH_KEY }}
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
chmod 700 ~/.ssh
|
||||||
|
echo "$CPANEL_SSH_KEY" > ~/.ssh/id_rsa
|
||||||
|
chmod 600 ~/.ssh/id_rsa
|
||||||
|
ssh-keyscan -H "${{ secrets.CPANEL_HOST }}" >> ~/.ssh/known_hosts 2>/dev/null
|
||||||
|
|
||||||
|
- name: Push to cPanel Git
|
||||||
|
env:
|
||||||
|
CPANEL_GIT_REMOTE: ${{ secrets.CPANEL_GIT_REMOTE }}
|
||||||
|
run: |
|
||||||
|
git remote add cpanel "$CPANEL_GIT_REMOTE"
|
||||||
|
git push cpanel main --force
|
||||||
|
echo "OK: Pushed to $CPANEL_GIT_REMOTE"
|
||||||
|
|
||||||
|
- name: Trigger cPanel deploy hook
|
||||||
|
env:
|
||||||
|
CPANEL_DEPLOY_HOOK: ${{ secrets.CPANEL_DEPLOY_HOOK_URL }}
|
||||||
|
run: |
|
||||||
|
if [ -n "$CPANEL_DEPLOY_HOOK" ]; then
|
||||||
|
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
|
||||||
|
"$CPANEL_DEPLOY_HOOK" --max-time 10)
|
||||||
|
echo "Hook returned HTTP $HTTP_CODE"
|
||||||
|
else
|
||||||
|
echo "No hook — cPanel Git auto-deploy handles it"
|
||||||
|
fi
|
||||||
|
|
||||||
|
deploy-fallback:
|
||||||
|
needs: [ci-check]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build plugin zip
|
||||||
|
run: |
|
||||||
|
mkdir -p build/hermes-cpanel-agent
|
||||||
|
cp hermes-ai-bridge.php build/hermes-cpanel-agent/
|
||||||
|
cp -r includes assets build/hermes-cpanel-agent/
|
||||||
|
cd build && zip -r ../hermes-cpanel-agent.zip hermes-cpanel-agent/
|
||||||
|
|
||||||
|
- name: Deploy via WP REST API (fallback)
|
||||||
|
env:
|
||||||
|
WP_URL: ${{ secrets.WP_URL }}
|
||||||
|
WP_APP_USER: ${{ secrets.WP_APP_USER }}
|
||||||
|
WP_APP_PASSWORD: ${{ secrets.WP_APP_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
PLUGIN_ZIP=$(base64 -w0 hermes-cpanel-agent.zip)
|
||||||
|
RESP=$(curl -s -w "\n%{http_code}" -X POST \
|
||||||
|
"$WP_URL/wp-json/wp/v2/plugins" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-u "$WP_APP_USER:$WP_APP_PASSWORD" \
|
||||||
|
-d "{\"slug\":\"hermes-cpanel-agent\",\"status\":\"active\",\"source\":\"data:application/zip;base64,$PLUGIN_ZIP\"}")
|
||||||
|
HTTP_CODE=$(echo "$RESP" | tail -1)
|
||||||
|
BODY=$(echo "$RESP" | head -n -1)
|
||||||
|
echo "WP REST API: HTTP $HTTP_CODE"
|
||||||
|
echo "$BODY" | python3 .gitea/scripts/deploy-report.py
|
||||||
|
|
||||||
|
verify:
|
||||||
|
needs: [deploy-to-cpanel, deploy-fallback]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Verify plugin deployment
|
||||||
|
env:
|
||||||
|
WP_URL: ${{ secrets.WP_URL }}
|
||||||
|
WP_APP_USER: ${{ secrets.WP_APP_USER }}
|
||||||
|
WP_APP_PASSWORD: ${{ secrets.WP_APP_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
curl -s "$WP_URL/wp-json/wp/v2/plugins" \
|
||||||
|
-u "$WP_APP_USER:$WP_APP_PASSWORD" | python3 .gitea/scripts/verify-deploy.py
|
||||||
|
|
||||||
|
- name: Verify hermes/v1 namespace
|
||||||
|
env:
|
||||||
|
WP_URL: ${{ secrets.WP_URL }}
|
||||||
|
run: |
|
||||||
|
curl -s "$WP_URL/wp-json" | python3 .gitea/scripts/check-namespace.py
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
name: 🩺 Health Check - cPanel Agent
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '*/30 * * * *' # Every 30 minutes
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
health:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Check Plugin Status via WordPress
|
||||||
|
env:
|
||||||
|
WP_URL: ${{ secrets.WP_URL }}
|
||||||
|
WP_APP_USER: ${{ secrets.WP_APP_USER }}
|
||||||
|
WP_APP_PASSWORD: ${{ secrets.WP_APP_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
curl -s "$WP_URL/wp-json/wp/v2/plugins" \
|
||||||
|
-u "$WP_APP_USER:$WP_APP_PASSWORD" | python3 .gitea/scripts/health-check.py
|
||||||
|
|
||||||
|
- name: Check REST API Endpoints
|
||||||
|
env:
|
||||||
|
WP_URL: ${{ secrets.WP_URL }}
|
||||||
|
run: |
|
||||||
|
# Try the hermes/v1 namespace
|
||||||
|
curl -s -o /dev/null -w "HTTP %{http_code}" "$WP_URL/wp-json/hermes/v1/content/generate" || echo "Endpoint check done"
|
||||||
|
|
||||||
|
- name: Notify on Failure
|
||||||
|
if: failure()
|
||||||
|
uses: actions/gitea-release-notify@v1
|
||||||
|
with:
|
||||||
|
title: "⚠️ cPanel Agent Health Check Failed"
|
||||||
|
body: "The health check for hermes-cpanel-agent on ummah.falahos.my failed. Check the Gitea Actions run for details."
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
name: 🌊 Sync cPanel Git + WordPress
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
schedule:
|
||||||
|
- cron: '0 */6 * * *' # Every 6 hours: ensure cPanel is in sync
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync-cpanel:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Sync to cPanel Git
|
||||||
|
env:
|
||||||
|
CPANEL_GIT_REMOTE: ${{ secrets.CPANEL_GIT_REMOTE }}
|
||||||
|
CPANEL_SSH_KEY: ${{ secrets.CPANEL_SSH_KEY }}
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "$CPANEL_SSH_KEY" > ~/.ssh/id_rsa
|
||||||
|
chmod 600 ~/.ssh/id_rsa
|
||||||
|
ssh-keyscan -H "${{ secrets.CPANEL_HOST }}" >> ~/.ssh/known_hosts 2>/dev/null
|
||||||
|
|
||||||
|
git remote add cpanel "$CPANEL_GIT_REMOTE"
|
||||||
|
git push cpanel main --force
|
||||||
|
echo "✅ Synced to cPanel git"
|
||||||
|
|
||||||
|
sync-nextcloud:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build plugin for Nextcloud sync
|
||||||
|
run: |
|
||||||
|
mkdir -p build
|
||||||
|
zip -r hermes-cpanel-agent.zip hermes-ai-bridge.php includes assets
|
||||||
|
|
||||||
|
- name: Upload to Nextcloud
|
||||||
|
env:
|
||||||
|
NC_URL: "https://team.falahos.my"
|
||||||
|
NC_USER: "hermes"
|
||||||
|
NC_PASS: ${{ secrets.NEXTCLOUD_HERMES_PASS }}
|
||||||
|
run: |
|
||||||
|
# Upload plugin zip to Nextcloud team folder
|
||||||
|
curl -s -u "$NC_USER:$NC_PASS" \
|
||||||
|
-T hermes-cpanel-agent.zip \
|
||||||
|
"$NC_URL/remote.php/dav/files/hermes/Agent%20Artifacts/hermes-cpanel-agent.zip"
|
||||||
|
echo "✅ Uploaded to Nextcloud"
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# Hermes cPanel Agent
|
||||||
|
|
||||||
|
AI agent runtime for cPanel / WordPress. Runs agent clones (Ody, PI) inside WordPress via REST API + WP-Cron. No SSH, no `exec()` needed.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Git Push → Gitea → Gitea Actions (Docker Swarm on Contabo)
|
||||||
|
↓
|
||||||
|
┌───────────────────────┐
|
||||||
|
│ CI: lint + security │
|
||||||
|
│ Build: plugin zip │
|
||||||
|
│ Deploy: git push or │
|
||||||
|
│ WP REST API │
|
||||||
|
└───────────────────────┘
|
||||||
|
↓
|
||||||
|
┌───────────────────────────────┐
|
||||||
|
│ cPanel Git Version Control │
|
||||||
|
│ (auto-pull + auto-deploy) │
|
||||||
|
└───────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌───────────────────────────────┐
|
||||||
|
│ WordPress Plugin Runtime │
|
||||||
|
│ - REST API (hermes/v1/*) │
|
||||||
|
│ - WP-Cron agent scheduler │
|
||||||
|
│ - AI Provider (OpenClaw) │
|
||||||
|
│ - Nextcloud bridge │
|
||||||
|
└───────────────────────────────┘
|
||||||
|
↓
|
||||||
|
Ody Clone (Content Gen)
|
||||||
|
PI Clone (Auto-Reply)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gitea Actions Workflows
|
||||||
|
|
||||||
|
| Workflow | File | Trigger | What It Does |
|
||||||
|
|----------|------|---------|--------------|
|
||||||
|
| **CI** | `.gitea/workflows/ci.yml` | push/PR | PHP lint, security scan, schema validation |
|
||||||
|
| **Deploy** | `.gitea/workflows/deploy.yml` | push to main | SSH → cPanel git push + WP REST API fallback + verification |
|
||||||
|
| **Sync** | `.gitea/workflows/sync.yml` | push + every 6h | cPanel sync + Nextcloud artifact upload |
|
||||||
|
| **Health** | `.gitea/workflows/health.yml` | every 30min | Plugin health check + REST API verification |
|
||||||
|
|
||||||
|
## Blocker Issues
|
||||||
|
|
||||||
|
| # | Issue | Status | Assigned |
|
||||||
|
|---|-------|--------|----------|
|
||||||
|
| 1 | Cloudflare WAF blocks Contabo from WP REST API | Open | @hermes-contabo |
|
||||||
|
| 2 | WordPress Application Password needed | Open | @odysseus |
|
||||||
|
| 3 | Gitea Actions runner (DONE ✅) | Done | @hermes-contabo |
|
||||||
|
| 4 | cPanel persistent agent design | Open | @hermes-contabo |
|
||||||
|
| 5 | Transfer repo to falah-os org | Open | @hermes-contabo |
|
||||||
|
| 6 | CI/CD workflow (DONE ✅) | Done | @hermes-contabo |
|
||||||
|
| 7 | Ody & PI clones epic | Open | @hermes-contabo |
|
||||||
|
|
||||||
|
## Secrets Required
|
||||||
|
|
||||||
|
Set these in `Settings → Actions → Secrets`:
|
||||||
|
|
||||||
|
- `WP_URL` — https://ummah.falahos.my
|
||||||
|
- `WP_APP_USER` — wmj
|
||||||
|
- `WP_APP_PASSWORD` — WordPress App Password
|
||||||
|
- `CPANEL_SSH_KEY` — cPanel Git deploy SSH private key
|
||||||
|
- `CPANEL_GIT_REMOTE` — cPanel Git remote URL
|
||||||
|
- `CPANEL_HOST` — cPanel hostname
|
||||||
|
- `CPANEL_DEPLOY_HOOK_URL` — (optional) deploy webhook
|
||||||
|
- `NEXTCLOUD_HERMES_PASS` — Nextcloud password
|
||||||
|
|
||||||
|
## Plugin Files
|
||||||
|
|
||||||
|
```
|
||||||
|
hermes-cpanel-agent/
|
||||||
|
├── hermes-ai-bridge.php # Main plugin bootstrap
|
||||||
|
├── includes/
|
||||||
|
│ ├── class-autoloader.php # PSR-4 autoloader
|
||||||
|
│ ├── core.php # Plugin core bootstrap
|
||||||
|
│ ├── installer.php # Activation/deactivation
|
||||||
|
│ ├── rest-controller.php # REST API (hermes/v1/*)
|
||||||
|
│ ├── ai-provider.php # OpenClaw AI integration
|
||||||
|
│ ├── scheduler.php # WP-Cron task scheduler
|
||||||
|
│ └── nextcloud-bridge.php # Nextcloud sync
|
||||||
|
├── assets/
|
||||||
|
│ └── admin.css # Admin UI styles
|
||||||
|
└── .gitea/
|
||||||
|
├── workflows/
|
||||||
|
│ ├── ci.yml # CI pipeline
|
||||||
|
│ ├── deploy.yml # Deploy pipeline
|
||||||
|
│ ├── sync.yml # Sync pipeline
|
||||||
|
│ └── health.yml # Health check
|
||||||
|
└── scripts/
|
||||||
|
├── verify-deploy.py # Verify plugin deployment
|
||||||
|
├── deploy-report.py # Parse deploy response
|
||||||
|
├── check-namespace.py # Verify REST namespace
|
||||||
|
├── health-check.py # Plugin health check
|
||||||
|
└── setup-secrets.py # Secret setup guide
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user