Skip to content

Deploy main automatically, and stop shipping stale PHP while doing it - #10

Merged
datlechin merged 1 commit into
mainfrom
chore/cd-and-upgrades
Aug 15, 2026
Merged

datlechin merged 1 commit into
mainfrom
chore/cd-and-upgrades

Conversation

@datlechin

Copy link
Copy Markdown
Member

main had no path to the server. Nine merged pull requests sat undeployed until
someone ran a script by hand — the server was still on b4a340f, the commit
before all of them.

The bug this turned up

Deploying by hand surfaced something worse than the missing automation. This
host runs FPM with opcache.validate_timestamps=0, and the previous
scripts/deploy.sh did not count resources/views/ as a PHP change. A Blade
template compiles to a PHP file named after its path, so the compiled name does
not change when the contents do — opcache kept serving the previous
compilation.

The result: the redesign's root template never reached a visitor. Live HTML was
still <meta name="theme-color" content="#FFAA46"> with zero font preloads,
hours after a deploy that reported success. Fixed on the server, and
tests/Feature/DeployScriptTest now reads the patterns out of the script and
checks them — both regressions were re-injected and confirmed to fail the suite.

Two things make this easy to get wrong, so they are written down in
docs/deployment.md:

  • php -i reads /etc/php/8.4/cli/php.ini, which says On. FPM reads its own
    ini, which says 0.
  • A probe file written and fetched immediately is never cached at all, because
    of opcache.file_update_protection. A naive probe reports that everything
    reloads fine.

What ships

.github/workflows/deploy.yml runs after tests go green on main
scripts/deploy.sh rewritten around what the old one got right
docs/deployment.md the host, the two apps sharing the domain, the key, rollback

The workflow is the only one here that holds a secret, which tests.yml
explicitly warns against because this repository accepts pull requests from
forks. Three things keep it out of their reach: it never runs on
pull_request, it never checks the repository out, and its key is pinned by
command= in authorized_keys to the deploy script alone.

The script keeps the previous version's good ideas — build both bundles sideways
and rename them in, do only the work the diff calls for — and adds:

  • Verification before the swap. A manifest without an entry for app.tsx
    stops the deploy with the old bundle still serving.
  • Restore after it. Any failure past the rename puts both old bundles back
    and restarts SSR.
  • A smoke test that can fail. 200 and a server-rendered <h1>, because
    SSR falling over still returns 200 with an empty shell.

Also

  • Hashed assets under /build/ now carry immutable for a year. The origin
    sent no Cache-Control at all, so Cloudflare filled the gap with its
    four-hour default and every returning visitor revalidated files whose names
    change when their contents do. Not extended to /images/, which carry no
    hash and are due to be replaced.
  • Pest 4 → 5 (PHPUnit 13), TypeScript 6 → 7, mockery 1.6.13.

TypeScript 7 moved the SSR overload error off the call and onto the two
offending property lines, so the old @ts-expect-error stopped covering it.
resources/js/ssr.tsx now suppresses only render — the one line that
genuinely cannot type, since every overload that accepts it also requires
resolve, and resolve must stay absent because @inertiajs/vite only injects
it into a call that lacks one.

Verification

  • 139 passed, 1950 assertions, REQUIRE_SSR=1 — 107 before, plus 32 new
  • typecheck, build, pint, shellcheck clean
  • sideways SSR build proven by running the SSR suite against a swapped-in bundle
  • rollback trap proven by forcing a post-swap failure
  • cache header confirmed at the origin, through Cloudflare, and absent from HTML

…g it

Adds the missing half of the release process. `main` had no path to the
server: nine merged pull requests sat undeployed until someone ran the
script by hand, and the server was still on the commit before all of them.

  .github/workflows/deploy.yml  runs after tests go green on main
  scripts/deploy.sh             does the work, on the server

The workflow is the only one here that holds a secret, which tests.yml
warns against because the repository takes pull requests from forks. It
never runs on `pull_request`, never checks the repository out, and the
key it uses is pinned by `command=` in authorized_keys to the deploy
script alone, so a leaked copy can deploy main and nothing else.

scripts/deploy.sh keeps what the previous version got right — build the
bundles sideways and rename them in, do only the work the diff calls for
— and fixes what it got wrong:

  * `resources/views/` was missing from the PHP change test. A Blade
    template compiles to a PHP file named after its path, so its name
    does not change when its contents do, and this host runs FPM with
    opcache.validate_timestamps=0. The first release deployed from this
    repository shipped a rewritten root template — the two scheme-aware
    theme colours, the two font preloads — that reached no visitor at
    all until FPM was reloaded by hand. tests/Feature/DeployScriptTest
    now reads the patterns out of the script and checks them.

    Note for anyone verifying this: `php -i` reads the CLI ini, which
    says On while FPM says 0, and a probe file written and fetched
    immediately is never cached at all thanks to file_update_protection.
    Both roads lead to the wrong answer.

  * Nothing verified a build before it went live. A manifest without an
    entry for app.tsx now stops the deploy with the old bundle still in
    place, and any failure after the rename puts both old bundles back.

  * Nothing checked the result. It now asserts a 200 *and* a
    server-rendered <h1>, because SSR falling over still returns 200
    with an empty shell.

Also here:

  * Hashed assets under /build/ get `immutable` for a year. The origin
    sent no Cache-Control at all, so Cloudflare filled the gap with its
    four-hour default and every returning visitor revalidated files
    whose names change when their contents do. Deliberately not extended
    to /images/, which carry no hash.

  * Pest 4 -> 5 (PHPUnit 13), TypeScript 6 -> 7, mockery 1.6.13.

TypeScript 7 reports the SSR overload mismatch on the two offending
property lines rather than on the call, so the old @ts-expect-error no
longer covered it. resources/js/ssr.tsx now suppresses the one line that
genuinely cannot type — `render`, which every overload accepting it also
requires `resolve` for, and `resolve` must stay absent because
@inertiajs/vite only injects it into a call that lacks it.

139 passed (1950 assertions) with REQUIRE_SSR=1.
@datlechin
datlechin merged commit c964396 into main Aug 15, 2026
5 checks passed
@datlechin
datlechin deleted the chore/cd-and-upgrades branch August 15, 2026 10:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant