23: Deployment & CI/CD

A Phlo app is static once it is built. build::release transpiles your .phlo sources into plain PHP, CSS and JS under a release/ directory (see the Tooling chapter); production serves that output with no transpiler and no build step in the request path. Deployment is the act of getting a tested release/ onto the server and switching to it safely.

This chapter describes a pipeline you can build on any CI provider. The shape is always the same: a push builds and tests a release; a person approves; the tested artifact ships; the server swaps to it and rolls back on a failed healthcheck. The mechanics are automated, the decision to release stays with a human.

23.1: The release build

The unit you deploy is the release/ tree, produced by:

php www/app.php build::release

It runs the release hooks, transpiles every .phlo to release/php/*.php, and writes the asset bundles to release/www/. Production's webroot points at release/www/ and the entrypoint runs with build: false and debug: false, so no build:: tooling and no control UI ship (see the Runtime config chapter).

The output is deterministic given the same sources and the same engine, so the release you test is the release you ship. Pin the engine to an exact commit in CI (via composer.lock or an explicit ref) so a build is never a moving target.

23.2: Continuous integration

Run CI on every push and pull request. A Phlo CI job does four things:

  1. Check out your app with the Phlo engine pinned to a known commit.
  2. Install dependencies.
  3. build::release, then lint the generated output: php -l over release/php/*.php and node --check over the JS bundles. A release that does not lint never leaves CI.
  4. Run the test suite (phpunit), starting any service the tests need, such as a database.

On success, upload the release/ tree as a build artifact keyed by the commit. That artifact, not a fresh build on the server, is what deployment ships. Building once and shipping the exact bytes you tested takes "works in CI, breaks on the box" off the table.

# Sketch of the CI job. Adapt the syntax to your provider.
on: [push, pull_request]
jobs:
  build:
    steps:
      - checkout app (with the engine pinned to a known commit)
      - install dependencies
      - run: php www/app.php build::release
      - run: for f in release/php/*.php; do php -l "$f"; done
      - run: for f in release/www/*.js;  do node --check "$f"; done
      - run: vendor/bin/phpunit
      - upload artifact: release/  (named after the commit)

23.3: The deploy gate

Deployment is a separate, manually triggered job (a workflow_dispatch on GitHub Actions, or your provider's equivalent). The manual trigger is the approval step: CI proves a commit is releasable, a person decides when it goes live.

The deploy job does not rebuild. It resolves a successful CI run on your release branch (the latest, or a run id you pass in), verifies it really is a green run on that branch, downloads its release artifact, and ships that. Keeping build and deploy separate lets you re-deploy a known-good artifact or roll forward to a specific tested commit without a rebuild introducing surprises.

23.4: A safe deploy on the server

Ship the artifact over SSH to a single restricted command on the server; never hand CI a shell. A minimal safe-deploy receiver:

  1. Unpacks the incoming release/ into a staging directory.
  2. Snapshots the currently live release (a tarball) for rollback.
  3. Swaps the new release in atomically: write beside the live tree, then move, so a request never sees a half-updated app.
  4. Runs a healthcheck against the new release, a real request that must return 200.
  5. On a failed healthcheck, restores the snapshot automatically and exits non-zero.

Because the swap is atomic and the rollback is automatic, a bad deploy self-heals to the last good release instead of leaving the site down. Run database migrations as an explicit, ordered step before the swap, and write them to be safe while the old code serves for the last moment before the switch.

23.5: Recording what is live

The receiver writes a small data/build.json at deploy time, recording the commit and timestamp it just made live:

{ "commit": "<sha>", "built": "<timestamp>" }

This is the source of truth for what is actually running, distinct from the app's own version prop. A dashboard or status page reads it to show a "deployed build" badge, so you can see which commit a server is on at a glance, without logging in.

23.6: Automate the mechanics, not the decision

Everything above automates the safe parts: building, testing, shipping identical bytes, the atomic swap, the healthcheck, the rollback. What it deliberately leaves to a person is the choice to release. A push does not go live; someone triggers the deploy when the change is ready, having read the diff and knowing what else is in flight.

That split is the point. A release is a deliberate, content-aware act: a schema change, a first-run migration, a robots rule, an asset cache-buster are judgment calls, not steps a git push should take on its own. Automate the repetitive, error-prone mechanics so they are boring and reversible, and keep the timing and the judgment with a human.

我们使用必要的cookie来使该网站正常工作。在您的许可下,我们还使用分析工具来改善网站。