# cPanel frontend deployment This runbook covers the production deployment of `pleno-vue` only. The API is not uploaded to cPanel and continues to use its existing release process and hosts. ## Release flow `.github/workflows/release.yml` starts only after the `Automated Tests` workflow succeeds for a push to `master` in this repository. It then: 1. Rechecks that the tested commit is still the head of `master`. 2. Checks out that exact commit without persisting GitHub credentials. 3. Installs dependencies, runs source checks, and builds `dist` once. 4. Runs the local-production Playwright gate against that existing `dist`. 5. Creates an immutable ZIP, SHA-256 sidecar, and file inventory, then verifies a local archive round trip. 6. Uploads the package as a required GitHub Actions artifact. 7. Enters the protected `frontend-production` GitHub environment and rechecks `master` immediately before deployment. 8. Uploads the ZIP and checksum over certificate-verified explicit FTPS. The uploaded `.part` files are downloaded and hashed before they are renamed. 9. Uploads an authenticated, bounded-lifetime request into the jailed deployment directory. A root-owned account-scoped activator validates the request and archive, extracts an inactive release, verifies its manifest identity and required files, and replaces `current` with a local single-filesystem rename. 10. Downloads the extracted tree and compares it byte-for-byte with the validated inventory. Public manifest, asset-integrity, cache-header, API-ping, and role gates then run against the active release. A failed gate asks the same activator to restore the previous immutable target. The fixed `frontend-production` concurrency group is not cancellable. A newer push therefore cannot interrupt an in-progress switch or rollback. ## GitHub environment Create the environment `frontend-production`, restrict deployment branches to protected branches, and keep `master` protected by the required CI checks. Production approvals can be added as an environment protection rule. Add these environment **secrets**: - `PRODUCTION_FTP_HOST` - `PRODUCTION_FTP_USER` - `PRODUCTION_FTP_PASSWORD` - `PRODUCTION_FTP_PATH` - `PRODUCTION_ACTIVATION_KEY` The API `.env` contains legacy values under the first four names, but production frontend deployment uses a dedicated cPanel FTP account jailed to `/home/truckwash/frontend-deployments`. Leave the API `.env` and the API deployment unchanged. The hosted release path deliberately does not call the remote cPanel API. Imunify360 blocks standard GitHub-hosted runner addresses, so release safety is provided by the jailed FTPS transport, the HMAC-authenticated account-scoped activator, exact inventory comparison, and public manifest verification. Add these environment **variables**: - `PRODUCTION_FRONTEND_URL`: `https://truckwash.io` `PRODUCTION_FRONTEND_URL` has the requested `https://truckwash.io` fallback. ### Create the dedicated FTP credentials 1. Open **Files -> FTP Accounts** in the `truckwash` cPanel account. 2. Create `github-pleno-vue@truckwash.io` with a generated, unique password. 3. Set its directory to `frontend-deployments`, which cPanel resolves to `/home/truckwash/frontend-deployments`, and leave quota unlimited. 4. Add `server.red-block.com` as `PRODUCTION_FTP_HOST`. Do not use `truckwash.io`: the FTPS certificate is issued to the server hostname. 5. Add the full account login as `PRODUCTION_FTP_USER`, the generated password as `PRODUCTION_FTP_PASSWORD`, and `/` as `PRODUCTION_FTP_PATH`. `/` is the root of this jailed FTP account, not the cPanel account home. 6. Verify explicit FTPS login and directory listing before merging. Never copy these frontend-only credentials back into the API `.env`. In GitHub, navigate to **Settings -> Environments -> frontend-production**. Use **Add secret** for credentials and **Add variable** for the frontend URL. Environment values are available only to the deployment job that names this environment, and configured protection rules are evaluated before its secrets are released. The existing live-test, Release Manager, and server-version secrets used by `release.yml` must remain configured. The GitHub-hosted deployment job installs `lftp` job-locally when needed, configures Node 22, and installs Playwright Chromium. The hosted image must provide npm, `zip`, `unzip`, GNU `find`, `stat`, and `sha256sum`. The cPanel account host needs `/bin/sh`, `flock`, `unzip`, `jq`, and `sha256sum` for the account-scoped activator. ## cPanel layout and one-time bootstrap The production FTP account is jailed directly to the deployment root, so its `PRODUCTION_FTP_PATH` is `/`. On cPanel that jail maps to the `frontend-deployments` directory below the account home. The helper creates this layout below it: ```text archives/ releases/ --/ dist/ staging/ current -> releases//dist ``` The domain's document root must resolve to `/frontend-deployments/current`, not to the deployment root itself. This stable document-root path is what makes replacing `current` atomic: every HTTP request resolves either the complete old release or the complete new release, never a partly uploaded directory. Before merging the workflow change, perform a one-time bootstrap in cPanel: 1. Back up the existing cPanel webroot and confirm the frontend hostname does not serve API/PHP files from this location. 2. Create `archives`, `releases`, and `staging` below the dedicated deployment root. 3. Put one complete, validated frontend build at `releases/-/dist`. Its `release-manifest.json` must contain that full 40-character commit and the same build ID used in the directory name. 4. Create `current` as a relative symlink to that release's `dist` directory. 5. Make the frontend domain document root resolve to the stable `current` path. For a cPanel primary domain whose configured document root remains `/home/truckwash/public_html`, make `public_html` a symlink to `frontend-deployments/current`. Exchange the old directory and prepared symlink atomically, and retain the old directory as a recovery copy. 6. Confirm the release `.htaccess` contains `DirectoryIndex index.html` so a symlinked primary-domain root serves the Vue shell instead of a directory listing. 7. Confirm `/release-manifest.json`, `/release-entry.json`, a deep Vue route, and the API health request work at `PRODUCTION_FRONTEND_URL`. 8. The server-side activator, rather than the hosted runner, validates that `current` and the captured rollback release exist before every switch. 9. Generate a dedicated 32-byte random activation key. Store its 64-character hexadecimal form in the protected `frontend-production` environment as `PRODUCTION_ACTIVATION_KEY`. On the server, install the same value at `/etc/pleno-release-activator/truckwash.key`, owned by `root:truckwash` and mode `0440`. The FTPS jail must not expose this key. 10. As `root`, install `scripts/release/cpanel-activate.sh` out of band at `/usr/local/sbin/truckwash-release-activate.sh`, owned by `root:root` and mode `0755`. The FTPS principal must not be able to replace or modify this executable. Then install this one `truckwash` account cron entry without replacing any other account cron lines: ```cron * * * * * /bin/flock -n /home/truckwash/frontend-deployments/.activation.lock /usr/bin/env CPANEL_ACTIVATION_ROOT=/home/truckwash/frontend-deployments CPANEL_ACTIVATION_KEY_FILE=/etc/pleno-release-activator/truckwash.key /bin/sh /usr/local/sbin/truckwash-release-activate.sh >/dev/null 2>&1 ``` The workflow can upload release data and bounded-lifetime request files, but it cannot replace the root-owned executable or read the activation key. The script authenticates each bounded-lifetime request with HMAC-SHA-256, accepts only strict filename components and hashes, validates the archive and manifest, runs a disposable local symlink preflight, journals the prior pointer for crash recovery, and writes a request-specific result. It runs as `truckwash`; it does not need root or a shell credential in GitHub. The host must provide `/bin/sh`, `flock`, `openssl`, `unzip`, `jq`, and `sha256sum`. The automatic deployer intentionally refuses to create the first `current` pointer. This prevents a missing or misconfigured bootstrap from turning the first automated run into an unreviewed production cutover. ### Auditing or restoring the primary webroot There is no GitHub Actions root-audit or root-restore job. Imunify360 blocks standard GitHub-hosted runner addresses, and this GitHub Team organization cannot assign static egress to a larger hosted runner. Keeping a configurable runner label would risk sending production cPanel secrets to a self-hosted runner, so that workflow has been removed. If the primary domain starts showing a directory index or returns 404 for files visible in `public_html`, inspect and recover it through the cPanel web interface or the hosting provider. Before replacing anything, confirm the exact `public_html` entry, the `frontend-deployments/current` link and required release files, all domain document roots, and retained `public_html.recovery-*`, `public_html.backup-*`, or `public_html.before-atomic-*` candidates. Do not replace the root while an addon or subdomain document root is nested below it. Restore only a verified physical directory, retain the displaced webroot, and verify `/`, `/index.html`, `/release-manifest.json`, and a deep Vue route. Normal releases do not depend on remote cPanel API access. ## Caching and compatibility The release `.htaccess` gives exact eight-character Vite-fingerprinted assets a one-year immutable policy. `index.html`, release metadata, web manifests, and service-worker control files always revalidate. The deployer retains every immutable release while hosted runners cannot query reliable cPanel modification metadata. Each successful run reports that retention cleanup is deferred. Periodically review disk usage in cPanel and remove only inactive releases and their matching archives; never remove the active or recorded rollback target. Because the document root switches as one symlink, an already-loaded page may still request an asset from its previous release after activation. The current implementation keeps previous release directories for rollback, but does not publish their asset paths through the new `current` pointer. Treat long-lived open-tab compatibility as a separate CDN/shared-assets enhancement if product usage requires it; the deployment itself does not serve mixed files. ## Failure and rollback behavior - Any error before the symlink rename leaves the current release untouched. - The deploy helper immediately verifies the public release after the rename. A failure restores the captured previous release. - A later public or credentialed Playwright failure runs the explicit rollback step with the previous immutable target emitted by the deploy step. - A stale workflow run exits before activation when `master` has advanced. - Release Manager is record-only (`auto_sync: false`); it no longer deploys the frontend through the API/Coolify path. For manual rollback from a controlled runner, provide the same GitHub environment settings plus the target recorded in the successful deployment: ```bash RELEASE_ROLLBACK_TARGET=releases//dist npm run release:deploy:cpanel:rollback ``` Never point this command outside `releases//dist`; the helper rejects path traversal and operations outside the configured deployment root.