Generate small reusable component partitions with explicit provider dependencies, GitHub-hosted concurrency, fail-closed evidence, and exact mobile store gates. Include the literal-i18n fix from #206 so validation covers the exact post-merge tree.
15 KiB
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:
- Rechecks that the tested commit is still the head of
master. - Checks out that exact commit without persisting GitHub credentials.
- Installs dependencies, runs source checks, and builds
distonce. - Runs the local-production Playwright gate against that existing
dist. - Creates an immutable ZIP, SHA-256 sidecar, and file inventory, then verifies a local archive round trip.
- Uploads the package as a required GitHub Actions artifact.
- Enters the protected
frontend-productionGitHub environment and rechecksmasterimmediately before deployment. - Uploads the ZIP and checksum over certificate-verified explicit FTPS. The
uploaded
.partfiles are downloaded and hashed before they are renamed. - 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
currentwith a local single-filesystem rename. - 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_HOSTPRODUCTION_FTP_USERPRODUCTION_FTP_PASSWORDPRODUCTION_FTP_PATHPRODUCTION_ACTIVATION_KEYPRODUCTION_CPANEL_USERPRODUCTION_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 the token for fail-closed directory and release-state inspection. It does
not use legacy Fileman mutation calls to replace symlinks: on this server those
calls can follow the target instead of renaming the link itself. Revoke and
rotate the token if it is ever exposed.
Add these environment variables:
PRODUCTION_CPANEL_API_URL:https://server.red-block.com:2083PRODUCTION_CPANEL_PATH:frontend-deploymentsPRODUCTION_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
- Open Files -> FTP Accounts in the
truckwashcPanel account. - Create
github-pleno-vue@truckwash.iowith a generated, unique password. - Set its directory to
frontend-deployments, which cPanel resolves to/home/truckwash/frontend-deployments, and leave quota unlimited. - Add
server.red-block.comasPRODUCTION_FTP_HOST. Do not usetruckwash.io: the FTPS certificate is issued to the server hostname. - Add the full account login as
PRODUCTION_FTP_USER, the generated password asPRODUCTION_FTP_PASSWORD, and/asPRODUCTION_FTP_PATH./is the root of this jailed FTP account, not the cPanel account home. - 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.
- Sign in to the cPanel account that owns the frontend deployment root.
- Record the exact cPanel account username shown in General Information.
Add it to the
frontend-productionenvironment as thePRODUCTION_CPANEL_USERsecret. - Open Security -> Manage API Tokens. If the item is missing, ask the hosting provider to enable API Tokens in WHM Feature Manager.
- 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. - 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. - Confirm Yes, I Saved My Token, then close any local plaintext copy after the GitHub secret has been saved.
- Before merging, run the deployment audit against the configured API origin.
It must be able to list
PRODUCTION_CPANEL_PATH,current, and immutable releases. 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. The GitHub-hosted deployment job installs
lftp job-locally when needed and installs Playwright Chromium. The workflow
also uses Node 22, 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 /. PRODUCTION_CPANEL_PATH names that same directory
relative to the cPanel account home. The helper creates this layout below it:
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:
- Back up the existing cPanel webroot and confirm the frontend hostname does not serve API/PHP files from this location.
- Create
archives,releases, andstagingbelow the dedicated deployment root. - Put one complete, validated frontend build at
releases/<commit>-<build-id>/dist. Itsrelease-manifest.jsonmust contain that full 40-character commit and the same build ID used in the directory name. - Create
currentas a relative symlink to that release'sdistdirectory. - Make the frontend domain document root resolve to the stable
currentpath. For a cPanel primary domain whose configured document root remains/home/truckwash/public_html, makepublic_htmla symlink tofrontend-deployments/current. Exchange the old directory and prepared symlink atomically, and retain the old directory as a recovery copy. - Confirm the release
.htaccesscontainsDirectoryIndex index.htmlso a symlinked primary-domain root serves the Vue shell instead of a directory listing. - Confirm
/release-manifest.json,/release-entry.json, a deep Vue route, and the API health request work atPRODUCTION_FRONTEND_URL. - Test the cPanel token against the exact host and port. The workflow performs
read-only state checks and refuses deployment if
currentor the captured rollback release is missing. - Generate a dedicated 32-byte random activation key. Store its 64-character
hexadecimal form in the protected
frontend-productionenvironment asPRODUCTION_ACTIVATION_KEY. On the server, install the same value at/etc/pleno-release-activator/truckwash.key, owned byroot:truckwashand mode0440. The FTPS jail must not expose this key. - As
root, installscripts/release/cpanel-activate.shout of band at/usr/local/sbin/truckwash-release-activate.sh, owned byroot:rootand mode0755. The FTPS principal must not be able to replace or modify this executable. Then install this onetruckwashaccount cron entry without replacing any other account cron lines:
* * * * * /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
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 and both the active
webroot and selected recovery are physical directories, 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. Restore also rejects symbolic-link roots and
recoveries because legacy cPanel Fileman may follow their targets rather than
rename the links. It renames the current physical 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
masterhas 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:
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.