Skip to content

Build and deployment

Deployment is Bitbucket Pipelines running git ftp push over SFTP to WP Engine. Three environments map to three branches; production is a manual pipeline off master. There is no build step in CI — compiled assets are committed to the repository.

The repository contains only the theme (wp-content/themes/bpcollins) plus site root files, not WordPress core or the database, so deployment is a file sync rather than a release process. git ftp was chosen because WP Engine exposes SFTP and Bitbucket Pipelines has no persistent shell on the target.

The consequence to internalise: CI does not compile anything. Whatever is in assets/ at commit time is what production serves.

Path What it does
bitbucket-pipelines.yml The whole deployment definition — three pipelines
wp-content/themes/bpcollins/gulpfile.js Local build: SCSS → assets/css, JS → assets/js
wp-content/themes/bpcollins/package.json Toolchain and the watch / compile scripts
wp-content/themes/bpcollins/inc/enqueue.php:50 get_cache_key() — reads .git-ftp.log on WP Engine for cache busting
wp-content/themes/bpcollins/inc/required-plugins/required-plugins.php TGMPA list of required plugins
wp-config-sample-base-theme.php Sample wp-config for a new local environment

Pipelines (bitbucket-pipelines.yml) all run the image strategiq/deployomatic:curl and share one shape: check out the exact commit, confirm with git name-rev that it belongs to the expected branch, then git ftp push --insecure --auto-init with that environment’s credentials.

Trigger Branch Target
Manual (custom pipeline deploy-production) must be on master production
Automatic on push staging staging
Automatic on push development development

git ftp keeps a .git-ftp.log file on the remote containing the deployed commit SHA and transfers only files that changed since it. --auto-init creates that state on a first deploy.

Cache busting. get_cache_key() returns the trimmed contents of .git-ftp.log when $_SERVER['IS_WPE'] is set — so on WP Engine every enqueued asset is versioned by the deployed commit, and a deploy invalidates asset caches exactly once. Locally (no IS_WPE) it returns time(), busting on every request. If the log file cannot be read it returns false, which makes WordPress omit the version entirely.

Local build. From wp-content/themes/bpcollins:

  • npm install
  • npx gulp watch — compiles resources/scss/** and blocks/**/*.scss to assets/css/, rolls up blocks/**/*.js and resources/js/app.js to assets/js/**.min.js, and starts BrowserSync proxying gulpfile.js’s urlToPreview.
  • One-off tasks: gulp sass, gulp js, gulp compile (via the npm scripts gulpstyles, gulpscripts, gulpcompile).

abovethefold.scss is compiled without a sourcemap because its output is inlined into <head> by header.php.

Plugins are not deployed by this repository. TGMPA declares them as required and force-activates them: Yoast SEO, ACF PRO (bundled as a zip in inc/required-plugins/), Contact Form 7, Google Apps Login Premium, Velvet Blues Update URLs, Query Monitor, Redirection, Fly Dynamic Image Resizer, CookieHub and WP Rocket.

flowchart TD
A[developer: npx gulp watch] --> B[assets/css + assets/js updated]
B --> C[commit source AND built assets]
C --> D{branch}
D -->|development| E[auto pipeline → dev SFTP]
D -->|staging| F[auto pipeline → staging SFTP]
D -->|master| G[manual deploy-production pipeline → prod SFTP]
G --> H[git ftp writes .git-ftp.log on the remote]
H --> I[get_cache_key reads it → asset version]

Bitbucket repository variables, one set per environment (documented at the top of bitbucket-pipelines.yml) — names only:

  • DEVELOPMENT_SFTP_USER, DEVELOPMENT_SFTP_PASS, DEVELOPMENT_SFTP_HOST, DEVELOPMENT_SFTP_ROOT
  • STAGING_SFTP_USER, STAGING_SFTP_PASS, STAGING_SFTP_HOST, STAGING_SFTP_ROOT
  • PRODUCTION_SFTP_USER, PRODUCTION_SFTP_PASS, PRODUCTION_SFTP_HOST, PRODUCTION_SFTP_ROOT

Host values include the port. Set these in Bitbucket → Repository settings → Repository variables, secured.

Environment detection in the theme uses $_SERVER['IS_WPE'] (set by WP Engine) and $_SERVER['HTTPS']. gulpfile.js’s urlToPreview is per-developer and currently committed as https://bpcollins.build/.

  • CI runs no build. If you commit SCSS or JS without running gulp, the deploy succeeds and the change does nothing. This is the single most common deployment surprise in this repo — always commit assets/ alongside sources.
  • assets/ is committed (225 tracked files). Two people building the same branch will produce conflicting minified diffs; resolve by rebuilding, not by merging minified output.
  • --insecure disables SFTP host-key verification in all three pipelines.
  • The branch guard is git name-rev | grep. It checks whether the commit is reachable from a branch of that name, not that you deployed the branch tip. A commit reachable from master deploys to production even when triggered from elsewhere, and the pipeline silently succeeds while doing nothing if the guard fails — no error, no output.
  • git ftp deletes remote files that were deleted in git, within the deployed root. Anything a client uploaded into a theme directory is at risk.
  • Deploys are incremental from .git-ftp.log. If that file is lost or the remote is restored from a backup, git ftp can refuse to push or push the wrong delta; --auto-init re-establishes state but re-uploads everything.
  • Losing .git-ftp.log also breaks cache busting, because get_cache_key() falls back to false and assets are then served with no version string — stale CSS/JS for returning visitors.
  • get_cache_key() reads .git-ftp.log by relative path (inc/enqueue.php:55), so it depends on the PHP working directory being the site root.
  • The toolchain is old: gulp 4, gulp-sass 4 with node-sass 4. It will not install on a current Node — use nvm and an older Node (Node 14/16 era) for npm install.
  • package.json still describes the base theme ("name": "strategiq", "description": "Theme for strategiq.co", a GitLab repository URL). It is not this site’s metadata.
  • There is no rollback step. Rolling back means deploying an earlier commit through the same pipeline.
  • No database or uploads are deployed — content moves between environments by other means (WP Engine copy tools).
  • Before any push: run gulp, confirm assets/ changed as expected, and commit both source and built files.
  • New environment → copy one of the three pipeline steps, add its four repository variables, and keep the git name-rev guard pointed at the right branch name.
  • Changing the deployed root → *_SFTP_ROOT decides what git ftp treats as the remote root. Getting it wrong can delete files outside the theme. Verify against development first.
  • Adding a plugin dependency → add it to the TGMPA array in inc/required-plugins/required-plugins.php. Plugins are installed on the site, not deployed from here.
  • Deliberately not abstracted: committed build output. It exists because CI has no build step; if you add one, remove assets/ from version control in the same change or you will ship stale files.
  • Verify a deploy: check the pipeline actually transferred files (an empty transfer list usually means the branch guard failed), then load a page and confirm the asset version query string matches the new commit SHA.

No tests run in CI — bitbucket-pipelines.yml contains only deploy steps, and the repository has no test suite. Deploy to development or staging and verify in a browser before promoting to production.