Build and archive the tested frontend, upload it through dedicated FTPS credentials, and atomically activate it through cPanel after CI succeeds.
248 lines
13 KiB
Markdown
248 lines
13 KiB
Markdown
# 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. Uses the cPanel Fileman API to extract into a new inactive release. The
|
|
extracted tree is downloaded and compared byte-for-byte with the validated
|
|
inventory, then `master` is checked again through the read-only workflow
|
|
token.
|
|
10. Replaces the `current` symlink with a single server-side rename. Public
|
|
manifest, asset-integrity, cache-header, API-ping, and role gates run after
|
|
activation. A failed public or role gate restores the previous symlink.
|
|
|
|
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_CPANEL_USER`
|
|
- `PRODUCTION_CPANEL_API_TOKEN`
|
|
|
|
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 cPanel token is separate from the FTP password. Create it in cPanel under
|
|
**Security -> Manage API Tokens** for `PRODUCTION_CPANEL_USER`. The deployment
|
|
uses cPanel API2 `Fileman::fileop` because cPanel does not provide a UAPI
|
|
replacement for the required extract, symlink, and rename operations. Revoke
|
|
and rotate the token if it is ever exposed.
|
|
|
|
Add these environment **variables**:
|
|
|
|
- `PRODUCTION_CPANEL_API_URL`: `https://server.red-block.com:2083`
|
|
- `PRODUCTION_CPANEL_PATH`: `frontend-deployments`
|
|
- `PRODUCTION_FRONTEND_URL`: `https://truckwash.io`
|
|
|
|
Only `PRODUCTION_FRONTEND_URL` has the requested `https://truckwash.io`
|
|
fallback. The cPanel URL and path deliberately fail closed when absent. The
|
|
production environment must keep the explicit
|
|
`https://server.red-block.com:2083` cPanel origin: the public origin serves
|
|
frontend HTML at `/json-api/cpanel`, while the dedicated TLS origin exposes the
|
|
cPanel JSON API.
|
|
|
|
### 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`.
|
|
|
|
### Create the missing cPanel credentials
|
|
|
|
The API `.env` supplies only the four FTP values. Create the two cPanel secrets
|
|
separately; do not reuse the FTP password as an API token.
|
|
|
|
1. Sign in to the cPanel account that owns the frontend deployment root.
|
|
2. Record the exact cPanel account username shown in **General Information**.
|
|
Add it to the `frontend-production` environment as the
|
|
`PRODUCTION_CPANEL_USER` secret.
|
|
3. Open **Security -> Manage API Tokens**. If the item is missing, ask the
|
|
hosting provider to enable API Tokens in WHM Feature Manager.
|
|
4. Click **Create**, name the token `github-pleno-vue-production`, and choose an
|
|
expiration date that matches the team's rotation policy. Expiration cannot
|
|
be edited later, so add a reminder before that date.
|
|
5. Click **Create**, copy the token immediately, and add it to the same GitHub
|
|
environment as `PRODUCTION_CPANEL_API_TOKEN`. cPanel will not show the token
|
|
again after leaving the page.
|
|
6. Confirm **Yes, I Saved My Token**, then close any local plaintext copy after
|
|
the GitHub secret has been saved.
|
|
7. Before merging, run the deployment preflight against the configured API
|
|
origin. It must be able to call cPanel API2 `Fileman::fileop` for extract,
|
|
symlink, and rename operations inside `PRODUCTION_CPANEL_PATH`. If the provider
|
|
restricts those operations, request the required account feature access;
|
|
do not broaden the token or deployment root beyond this cPanel account.
|
|
|
|
The current production token is named `github-pleno-vue-production` and
|
|
expires on 20 July 2027 at 23:59:59 server time. Rotate the GitHub environment
|
|
secret before that date, then revoke the replaced token in cPanel.
|
|
|
|
In GitHub, navigate to **Settings -> Environments -> frontend-production**.
|
|
Use **Add secret** for credentials and **Add variable** for the two URLs and the
|
|
cPanel deployment path.
|
|
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. GitHub-hosted deploy runners install
|
|
`lftp` and Playwright Chromium during the job; the existing self-hosted build
|
|
runner still needs Node 22, npm, `zip`, `unzip`, GNU `find`, `stat`, and
|
|
`sha256sum`.
|
|
|
|
## cPanel layout and one-time bootstrap
|
|
|
|
The production FTP account is jailed directly to the deployment root, so its
|
|
`PRODUCTION_FTP_PATH` is `/`. `PRODUCTION_CPANEL_PATH` names that same directory
|
|
relative to the cPanel account home. The helper creates this layout below it:
|
|
|
|
```text
|
|
archives/
|
|
releases/
|
|
<commit>-<github-run>-<attempt>/
|
|
dist/
|
|
staging/
|
|
current -> releases/<release-id>/dist
|
|
```
|
|
|
|
The domain's document root must resolve to
|
|
`<cPanel account home>/<PRODUCTION_CPANEL_PATH>/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/<commit>-<build-id>/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. Test the cPanel token against the exact host and port. The workflow performs
|
|
a disposable symlink-replacement preflight and refuses deployment if the
|
|
filesystem or hosting policy cannot replace a symlink atomically.
|
|
|
|
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
|
|
|
|
Use the protected **cPanel Root Audit and Restore** workflow if the primary
|
|
domain starts showing a directory index or returns 404 for files that cPanel
|
|
lists in `public_html`. The `audit` mode is read-only: it reports the exact
|
|
`public_html` entry, whether the internal `current` link can serve the required
|
|
release files, domain document roots, and retained recovery candidates without
|
|
printing the cPanel token. API2 does not expose a documented symlink-target
|
|
field, so the audit deliberately reports `rootTargetVerified: false` instead
|
|
of claiming that an arbitrary `public_html` link follows `current`; the live
|
|
HTTP checks remain the source of truth for service health. The audit fails
|
|
closed if any domain record lacks an identity or document root, and restore is
|
|
blocked while an addon or subdomain is rooted below `public_html`.
|
|
|
|
If the regression followed the one-time webroot exchange, select `restore`
|
|
and copy one exact recovery entry from the audit, including the retained
|
|
`public_html.before-atomic-*` entry created by the bootstrap when applicable.
|
|
The workflow requires the
|
|
typed phrase `RESTORE <recovery> TO public_html STATE <state-token>`, using the
|
|
exact token string from that audit. The token is an optimistic-concurrency
|
|
guard over the cPanel metadata visible to the audit; it is not a content hash
|
|
or a substitute for validating the selected recovery. Restore also rejects an
|
|
unreadable physical directory. An unreadable root is eligible only when the
|
|
independent account-home listing identifies it as a symbolic link. It renames
|
|
the current entry to a run-specific `public_html.failed-*` path, restores the retained entry, and
|
|
checks `/`, `/index.html`, `/release-manifest.json`, and a deep Vue route. If
|
|
any mutation response is lost or any check fails, it reconciles the observed
|
|
account-home entries and reinstates the pre-restore cPanel state. It never
|
|
deletes the recovery or displaced webroot, and reports manual intervention if
|
|
the expected entries cannot be proven after compensation.
|
|
|
|
## 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 at least
|
|
the active and rollback releases and keeps five recent release directories by
|
|
default (`RELEASE_RETAIN_COUNT` can be set from 2 through 25). Once a release
|
|
falls outside that validated retention set, its directory and matching ZIP and
|
|
checksum are removed over FTPS. Cleanup failure is reported without rolling
|
|
back an otherwise verified deployment.
|
|
|
|
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/<release-id>/dist npm run release:deploy:cpanel:rollback
|
|
```
|
|
|
|
Never point this command outside `releases/<release-id>/dist`; the helper rejects
|
|
path traversal and operations outside the configured deployment root.
|