Build and deployment
How code gets from a branch to WP Engine: Bitbucket Pipelines running
git ftp push over SFTP, with compiled assets committed to the repository and
no build or test step in CI.
Why it exists
Section titled “Why it exists”WP Engine’s standard deployment is a git push to their own remote, but this
project deploys by SFTP from Bitbucket instead, using StrategiQ’s shared
deployomatic Docker image. Because there is no build step on the server or
in the pipeline, the compiled CSS and JS have to be committed.
Entry points
Section titled “Entry points”| Path | What it does |
|---|---|
bitbucket-pipelines.yml | The whole deployment definition |
.gitignore | Decides what is even in the repo — WordPress core and most plugins are excluded |
wp-content/themes/supplychainschool/gulpfile.js | Main theme build (Laravel Elixir / Gulp 3) |
wp-content/themes/carboncalculator/gulpfile.js | Carbon theme build (Gulp + Tailwind + PostCSS) |
wp-content/themes/supplychainschool/functions.php:126 | Cache busting keyed off .git-ftp.log |
How it works
Section titled “How it works”Pipelines
Section titled “Pipelines”Image: strategiq/deployomatic:curl.
| Trigger | Target |
|---|---|
Push to development | $DEVELOPMENT_SFTP_* |
Push to staging | $STAGING_SFTP_* |
Manual deploy-production pipeline | $PRODUCTION_SFTP_* |
Each step is two commands: git checkout $BITBUCKET_COMMIT, then
git ftp push --insecure --auto-init --user … --passwd … --remote-root … <host>.
Production is manual only — there is no branch pipeline for it, so
production deploys are a deliberate button press in the Bitbucket UI.
master has no pipeline at all.
git-ftp records what it uploaded in a .git-ftp.log file on the server
containing the deployed commit hash, and --auto-init creates that file on a
first deploy.
Cache busting
Section titled “Cache busting”enqueue_scripts() (themes/supplychainschool/functions.php:126) picks the
asset query string three ways:
| Condition | Query string |
|---|---|
$_SERVER['IS_WPE'] not set (local/other host) | ?v=<time()> — never cached |
On WP Engine and .git-ftp.log exists | ?v=<deployed commit hash> |
On WP Engine, no .git-ftp.log | none |
So the deployed commit hash is the cache key. get_cache_key()
(functions.php:621) exposes the same logic. Note file_exists('.git-ftp.log')
is a relative path, so it resolves against the PHP working directory rather
than the site root.
What is in the repository
Section titled “What is in the repository”.gitignore excludes WordPress core files individually, wp-config.php,
wp-content/uploads/, all logs (*.log), and — importantly —
wp-content/plugins/* and wp-content/mu-plugins/*, with a single
exception:
!wp-content/plugins/carboncalcSo the only plugin under version control is the custom carboncalc plugin.
The 27 third-party plugins present locally (ACF Pro, CF7, Wordfence, Yoast,
Autoptimize, Redirection, Smush, WP Crontrol, Sucuri, Google Apps Login,
Juicer, …) are not deployed by the pipeline and are managed directly on
each environment.
Both themes are tracked, including their node_modules absence and their
committed assets/ output.
Local development
Section titled “Local development”Local by Flywheel. The local host is supplychainschool.build, with
carbon.supplychainschool.build for the Carbon subsite — these hostnames are
in the staging allow-list in LMSClient.php:5, which is what makes local
point at staging Moodle. local-adminer-*.php and local-xdebuginfo.php are
Local’s own helpers and are gitignored.
Branching
Section titled “Branching”Long-lived development and staging branches, master as the main branch,
and a large number of feature/*, hotfix/* and per-developer
development-<name> branches. Deployment is per branch, not per tag.
Configuration
Section titled “Configuration”Bitbucket repository variables (secured, never in the repo) — the file’s own header documents them:
DEVELOPMENT_SFTP_USER, DEVELOPMENT_SFTP_PASS, DEVELOPMENT_SFTP_HOST,
DEVELOPMENT_SFTP_ROOT, and the same four with STAGING_ and PRODUCTION_
prefixes.
Environment detection at runtime: $_SERVER['IS_WPE'] (set by WP Engine),
WP_ENVIRONMENT_TYPE in wp-config.php, and the hostname allow-list in
LMSClient.php.
Invariants and gotchas
Section titled “Invariants and gotchas”- There is no build step in CI.
git ftp pushuploads the repository as it is. Forgettingnpm run prodbefore committing means SCSS and JS changes never reach the server, and nothing warns you. - There are no tests in CI, and no linting —
phpcs.xml.distexists in the main theme but is not run. - Third-party plugins are not deployed. Updating a plugin means doing it in each environment’s wp-admin. Environments can therefore drift, and a plugin present locally may be absent on staging.
--insecureskips SFTP host key verification.git ftp pushdeploys the diff between the last deployed commit and now, as recorded in.git-ftp.logon the server. If that file is deleted or wrong, the next deploy either re-uploads everything or skips files. Do not hand-edit it.- Deleting
.git-ftp.logsilently turns off cache busting on WP Engine (functions.php:131) — assets are served with no query string at all. - Production deploys from whatever commit the manual pipeline is run against,
which need not be on
master. - The Carbon theme uses Tailwind, whose content scan covers
**/*.phpandresources/**/*.{js,jsx}only. A class that appears solely in a JS template literal underassets/js/is purged from the build. - The main theme’s toolchain (Laravel Elixir, Gulp 3,
node >= 10.9) will not run on a current Node without workarounds.npm-shrinkwrap.jsonis committed and should be respected. - WP Engine page caching is in play (plus Autoptimize and
wpe-advanced-cache-options), which is why logged-in header state is a client-side swap — see [[site-chrome-and-navigation]].
Changing it safely
Section titled “Changing it safely”- Adding a plugin to version control means adding a
!wp-content/plugins/<dir>negation to.gitignoreafter thewp-content/plugins/*line. Order matters in gitignore. - Adding a CI step: both branch pipelines are two-line scripts. A
npm ci && npm run prodstep per theme before the push would remove the forgot-to-build failure mode, but the pipeline image must then have a compatible Node — checkdeployomatic:curlfirst. - Never commit
wp-config.php. Constants belong there (MOODLE_WS_TOKEN,COMPANIES_HOUSE_API_KEY, the Carbon flags); the repo should only ever reference them by name. - Verify a deploy: check the commit hash in
.git-ftp.logon the server matches what you pushed, then view source on a page and confirm the asset query string is that hash. - Deliberately not abstracted: SFTP rather than WP Engine’s git push. Changing
it would change the cache-busting mechanism too, since that reads
.git-ftp.log.
None, in CI or locally.
Related: [[acf-flexible-content-blocks]], [[site-chrome-and-navigation]], [[company-emissions-dashboard]].