Deploy lane · cPanel × GitHub · June 2026

How iris — and every PHP app on cPanel — ships itself

The deep-dive companion to the GitHub plan: how the idata repo auto-deploys to cPanel. No FileZilla, no drag-and-drop, no chmod 0644 ritual — a merged pull request publishes the site.

Edit on a branch Open PR Merge to main GitHub Action FTPS-syncs the diff Live on idata.ienergia.cl
1 merge = 1 deploy
No manual upload step
Diff only
Changed files ship, not all
0 .bak files
Git replaces hand-versioning
$0 added
Reuses existing FTP creds
The shift

Stop replacing FileZilla. Remove the upload step entirely.

Today the iris pages are PHP files hand-uploaded to a cPanel server. The deploy docs (Iris_Kepler_Deploy_Playbook, DEPLOY_2026-05-31.md) are full of "upload file X → set 0644 → repeat". Once the app lives in GitHub, nobody uploads anything — deploy becomes a side-effect of merging.

⚠️ The manual ritual today

  • Edit locally in Dropbox/…/iData/agent/web/*.php.
  • Open FileZilla, drag changed files to /public_html/ — guessing which ones actually changed.
  • Hand-set 0644 on every file — File Manager defaults to 0700 and Apache returns 403. This bug recurs in every deploy doc.
  • Translate paths by hand via the "local X → server Y" tables in each DEPLOY_*.md.
  • Hope Dropbox isn't holding a .bak-truncated or stale copy.

✓ With the cPanel × GitHub lane

  • Edit on a branch — your work is isolated, nobody else's edits collide.
  • Open a PR — Daniel or David glance-reviews customer-facing pages before they go live.
  • Merge — a GitHub Action FTPS-syncs only the files whose hash changed.
  • Permissions persist — you only ever overwrite existing files, so the 0644 problem disappears.
  • One audit trail — every change is a commit; roll back instantly.
The pipeline

Merge → Action → server. Four moving parts.

GitHub Actions does the FTP on the server's behalf, in the cloud, the instant a PR lands on main.

Push to a branch

Each task is sven/iris-rules, daniel/prism-cover. Claude works its own branch and opens the PR.

Pull request + review

main is protected. One approval is the deploy gate — a bad iris_master.php can't reach customers unreviewed.

Merge fires the Action

A path filter means only **.php / **.html changes trigger it — editing a README never redeploys.

FTPS diff-sync to cPanel

The action keeps a state file on the server and uploads only changed files. Credentials live in GitHub Secrets, never on a laptop.

Optimization #1

Make the repo root mirror the server docroot — exactly

A 1:1 mapping is the single highest-leverage decision. It deletes the path-translation tables, the .bak sprawl, and the permissions bug in one move — deploy becomes a dumb mirror with zero translation.

ienergia/idata — repo root maps straight onto /home/ienergia/public_html/
idata/                          # repo root            →  /public_html/
├─ iris_master.php              →  /public_html/iris_master.php
├─ lens.php  prism_*.php  header.inc.php
├─ _agent/
│  ├─ modes/eval_watchlist.php  →  /public_html/_agent/modes/...
│  ├─ modes/refresh_inventory.php
│  └─ migrate.php               # applies pending sql/ files, idempotent
├─ sql/28_iris_kepler.sql        # versioned, NOT auto-run blindly
├─ .gitignore                   # *.bak  *.zip  config.php  .idata/  __pycache__/
└─ .github/workflows/deploy.yml
🔐

Secrets are already in the right place

Live config sits outside the docroot at /home/ienergia/.idata/config.php and /home/ienergia/.auth/config.php. That means no credential ever touches git — the repo holds code only. Keep it that way; .gitignore any local config copy.

The workflow file

One file, copy-paste ready

FTPS, diff-only, secrets in GitHub. The same file (parameterized) deploys Auth, Prism, BEAM and every other cPanel app — one reusable template across the org.

.github/workflows/deploy.yml
name: Deploy iData / iris to cPanel
on:
  push:
    branches: [main]
    paths: ['**.php', '**.html', '**.htaccess']   # docs-only changes don't redeploy
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: SamKirkland/FTP-Deploy-Action@v4.3.5
        with:
          server:   ${{ secrets.CPANEL_FTP_HOST }}
          username: ${{ secrets.CPANEL_FTP_USER }}
          password: ${{ secrets.CPANEL_FTP_PASS }}
          protocol: ftps
          local-dir:  ./
          server-dir: /public_html/
          exclude: ['**/.git*', '**/*.bak', '**/*.zip', 'sql/**', 'README*']
      # optional: apply pending DB migrations after files land
      - run: curl -fsS "https://idata.ienergia.cl/_agent/migrate.php?key=${{ secrets.MIGRATE_KEY }}"

Why this action: it stores .ftp-deploy-sync-state.json on the server and compares hashes, so after the first run it uploads only what changed — faster than any full mirror, and the FTP password lives in GitHub Secrets rather than a .bat on someone's desk.

The optimizations that matter

Seven decisions that make this fast and safe

Tuned to this exact setup — shared cPanel host, a Windows team of mixed technical depth, files currently scattered across Dropbox with hand-made backups.

1 Repo = docroot, 1:1

No path translation, no per-file mapping table. Deploy is a straight mirror. Kills the DEPLOY_*.md "local→server" tables.

2 Diff-only FTPS sync

State file on the server → only changed-hash files upload. A one-line edit ships in seconds, not a full re-mirror.

3 Secrets stay out of git

Config already lives outside docroot in .idata/ & .auth/. FTP creds go to GitHub Secrets. The repo is code-only.

4 Migrations: versioned + guarded

Numbered sql/NN_*.sql applied by an idempotent migrate.php?key= the workflow curls. Removes the manual phpMyAdmin step.

5 Branch protection = the gate

main protected, 1 approval required. The PR review is the deploy gate for customer-facing iris pages.

6 Optional staging lane

developidata-staging.ienergia.cl; main → prod. Cheap insurance before a page reaches Daniel's morning review.

7 One template, every app

A reusable workflow_call parameterized by server-dir. Auth, Prism, BEAM all deploy identically. That's the real leverage.

+ Crons stay in cPanel

But keep a versioned crons.txt in the repo so the schedule is reviewable even though cPanel holds the live copy.

Which mechanism

Three ways to deliver to cPanel — pick one

All three end with "merge auto-deploys". They differ in where the moving parts sit. The reachability probe already confirmed the host has Terminal/SSH + crontab, so all three are technically available.

MechanismHow it worksTrade-offFit
FTPS via GitHub Actions
recommended
Action FTPS-syncs the diff using your existing FTP creds, stored as GitHub Secrets. Credentials centralized in GitHub; full CI gate before deploy; no server-side setup. Best for this team
cPanel Git Version Control The server pulls from GitHub and runs .cpanel.yml copy rules. No FTP creds in CI, but the server needs outbound GitHub access and you lose the pre-deploy CI gate. Workable
SSH rsync Action rsyncs over SSH with a deploy key — cleanest diffs, preserves permissions natively. Most efficient, but requires generating + managing an SSH deploy key per repo. Power option
🎯

Recommendation

Go FTPS via GitHub Actions. For a team of mixed technical depth it has the least server-side magic, centralizes the credentials in GitHub, works today with the FTP login you already use in FileZilla, and keeps the full CI gate. Graduate to rsync only if deploys ever feel slow.

Standing it up

From scratch, in the right order

⚠️

Seed the repo from the LIVE server — not from Dropbox

This is the one dangerous step. The Dropbox copy holds .bak-truncated files and may be ahead or behind production. One-time lftp mirror (download) the live /public_html, commit that as the baseline, then reconcile your local edits on top. Otherwise the first auto-deploy could push stale Dropbox files over good production.

Create ienergia/idata

Under the org that already exists. Lay the root out to mirror the docroot 1:1.

Seed from live, commit baseline

lftp mirror down the real server → commit as "production baseline" before touching anything.

Add secrets + workflow

Store CPANEL_FTP_* in GitHub Secrets; drop in deploy.yml; add the .gitignore.

Protect main, invite the team

Require 1 PR approval. Add David, Sven, Daniel, Sebastián. First test PR → watch it deploy.

🧭

How it slots into the bigger plan

This is the cPanel lane of the repos-by-deploy-target model: idata → cPanel (this page), spark-dashboards & Solus → Cloudflare Pages, data-pipelines → local/scheduled, and the 2.9 GB of data → Cloudflare R2, never git. Same git-for-code rule, one deploy mechanism per target. See the full proposal →