224 lines
12 KiB
Markdown
224 lines
12 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. 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/
|
|
<commit>-<github-run>-<attempt>/
|
|
dist/
|
|
staging/
|
|
current -> releases/<release-id>/dist
|
|
```
|
|
|
|
The domain's document root must resolve to
|
|
`<cPanel account home>/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/<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. 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/<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.
|