Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@

- **Statamic's own fast YAML parser is used where it has one.** Flatpack's copy is only used on Statamic versions without it.

### Fixed

- **Building after a deploy into a new release directory.** With zero-downtime deploys the app moves to a new path while `storage` is shared. `flatpack:build` then failed, and left the site with no content until it was fixed. It now builds from the new path, and a build that fails for any reason leaves the last good one in place.
- `flatpack:refresh` (and Refresh in the Control Panel) builds everything when the content was compiled for another path, where before it failed. `flatpack:status` says so too.

## 1.1.0 (2026-11-10)

### Changed
Expand Down
8 changes: 6 additions & 2 deletions bin/parity
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ if [[ -z "${KEEP:-}" || ! -d "$APP/vendor" ]]; then
# Older Statamic releases have published security advisories, which Composer refuses to
# install by default. This throwaway app only exists to test against them.
"${COMPOSER[@]}" config policy.advisories.block false 2>/dev/null || "${COMPOSER[@]}" config audit.block-insecure false
"${COMPOSER[@]}" require "statamic/cms:$VERSION" "1994/flatpack-statamic:@dev" "laravel/framework:^10.0|^11.0|^12.0|^13.0" -W --no-interaction -q
# Everything is updated, not only these three: the skeleton pins tools (laravel/pail, for one)
# to versions that need a newer Laravel than an older Statamic allows.
"${COMPOSER[@]}" require "statamic/cms:$VERSION" "1994/flatpack-statamic:@dev" "laravel/framework:^10.0|^11.0|^12.0|^13.0" --no-update --no-interaction -q
"${COMPOSER[@]}" update -W --no-interaction -q
"$PHP" artisan key:generate -q
for kv in STATAMIC_PRO_ENABLED=true STATAMIC_STACHE_WATCHER=false CACHE_STORE=file CACHE_DRIVER=file QUEUE_CONNECTION=sync APP_ENV=local; do
k="${kv%%=*}"; grep -q "^$k=" .env && sed -i.bak "s|^$k=.*|$kv|" .env || echo "$kv" >> .env
Expand All @@ -36,7 +39,8 @@ sed -i.bak "s/'multisite' => false/'multisite' => true/" "$APP/config/statamic/s
# KEEP=1 with a different version: switch the existing app to it instead of starting again.
cd "$APP"
if [[ "$("$PHP" -r 'require "vendor/autoload.php"; echo ltrim(Composer\InstalledVersions::getPrettyVersion("statamic/cms"), "v");')" != "${VERSION#v}" ]] && [[ "$VERSION" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
"${COMPOSER[@]}" require "statamic/cms:$VERSION" "laravel/framework:^10.0|^11.0|^12.0|^13.0" -W --no-interaction -q
"${COMPOSER[@]}" require "statamic/cms:$VERSION" "laravel/framework:^10.0|^11.0|^12.0|^13.0" --no-update --no-interaction -q
"${COMPOSER[@]}" update -W --no-interaction -q
fi

# The fast YAML parser against this version's Symfony YAML.
Expand Down
56 changes: 52 additions & 4 deletions src/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,53 @@ public static function store(): FlatpackStore
public static function compile(int $bodyWorkers = 0): array
{
$t = hrtime(true);
Stache::clear();
Listeners\RecordManifest::$paused = true;
try {
Stache::clear();
// Clearing forgets each store's saved keys but leaves the ones it has in memory. They
// hold absolute paths, so after a deploy into a new release directory every file
// would look like a duplicate of itself at its old path, and nothing would be built.
self::forgetKeysInMemory();
Stache::warm();
self::recordTimestamps();
} catch (\Throwable $e) {
// A build that fails must leave the last good one in place, not a cleared store.
self::store()->discard();

throw $e;
} finally {
Listeners\RecordManifest::$paused = false;
}
self::recordTimestamps();
self::store()->persist();
$gc = self::store()->gc();
$out = ['compile_s' => round((hrtime(true) - $t) / 1e9, 2), 'gc_files' => $gc];
if ($bodyWorkers > 0) $out += self::precompileBodies($bodyWorkers);
self::stamp(['built_at' => time(), 'build_s' => round((hrtime(true) - $t) / 1e9, 2), 'refreshed_at' => null, 'via' => 'flatpack:build']);
self::stamp(['built_at' => time(), 'build_s' => round((hrtime(true) - $t) / 1e9, 2), 'refreshed_at' => null, 'via' => 'flatpack:build', 'base_path' => base_path()]);

return $out;
}

/**
* Plain stores first: finding an aggregate store's children (a collection's entries, a
* taxonomy's terms) reads the collections and taxonomies stores, which must be clean by then.
*/
private static function forgetKeysInMemory(): void
{
$aggregates = [];
foreach (Stache::stores() as $store) {
if ($store instanceof \Statamic\Stache\Stores\AggregateStore) {
$aggregates[] = $store;
} else {
$store->keys()->setKeys([]);
}
}
foreach ($aggregates as $aggregate) {
foreach ($aggregate->discoverStores() as $store) {
$store->keys()->setKeys([]);
}
}
}

/** @return array<string, \Statamic\Stache\Stores\Store> every concrete store, keyed by its key */
public static function allStores(): array
{
Expand Down Expand Up @@ -92,7 +122,25 @@ public static function stamps(): array
/** A warm outside flatpack:build (stache:warm, stache:refresh, the Cache Manager) counts as a build. */
public static function stampWarm(): void
{
self::stamp(['built_at' => time(), 'build_s' => null, 'refreshed_at' => null, 'via' => 'stache:warm']);
self::stamp(['built_at' => time(), 'build_s' => null, 'refreshed_at' => null, 'via' => 'stache:warm', 'base_path' => base_path()]);
}

/**
* Whether the content was compiled for the app at another path: a deploy into a new release
* directory with shared storage. Everything compiled holds absolute paths, so it all has to
* be built again from here.
*/
public static function builtElsewhere(): bool
{
if (($path = self::stamps()['base_path'] ?? null) !== null) {
return $path !== base_path();
}

// A build from before the path was recorded: see where its collection files were.
$saved = Stache::cacheStore()->get('stache::indexes::collections::path') ?: [];
$directory = Stache::store('collections')?->directory();

return is_string($first = reset($saved)) && $directory && ! str_starts_with($first, rtrim($directory, '/').'/');
}

private static function stamp(array $values): void
Expand Down
6 changes: 6 additions & 0 deletions src/Commands/Refresh.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ public function handle(): int
return $this->call('flatpack:build');
}

if (Builder::builtElsewhere()) {
$this->components->info('The content was built for the app at another path (a new release directory), so building everything.');

return $this->call('flatpack:build');
}

$out = Builder::refresh();

if (! $out['changed_stores']) {
Expand Down
6 changes: 6 additions & 0 deletions src/Commands/Status.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ public function handle(): int
return self::SUCCESS;
}

if (\NineteenNinetyFour\Flatpack\Builder::builtElsewhere()) {
$row('Changed on disk since', '<fg=yellow>built for another path</> (a new release directory: run `php please flatpack:build`)');

return self::SUCCESS;
}

$pending = State::pending();
if (! $pending['manifest']) {
$row('Changed on disk since', 'unknown: run `php please flatpack:build` to start tracking');
Expand Down
7 changes: 7 additions & 0 deletions src/FlatpackStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,13 @@ public function getPrefix()

// ---------------------------------------------------------------- persist

/** Drop everything buffered in this process, so what's on disk stays as it was. */
public function discard(): void
{
$this->kvDirty = $this->shardOps = $this->fbOps = $this->fbRead = $this->loaded = [];
$this->pointer = null;
}

/** Write buffered changes: new content-addressed files, then the pointer, under a lock. */
public function persist(): void
{
Expand Down
3 changes: 2 additions & 1 deletion src/Jobs/RunFlatpack.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ public function handle(): void
Cache::put(Status::RUNNING, ['action' => $this->action, 'at' => time()], 900);

try {
$result = $this->action === 'build'
// A refresh can't follow the app to a new path: everything compiled holds absolute paths.
$result = $this->action === 'build' || Builder::builtElsewhere()
? Builder::compile((int) config('flatpack.build_workers', 4))
: Builder::refresh();

Expand Down
13 changes: 13 additions & 0 deletions tests/parity/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,19 @@ oracle "$HERE/clear.php" "$O" >/dev/null
(cd "$C" && flatpack please flatpack:refresh -q) || { echo " refresh failed:"; (cd "$C" && flatpack please flatpack:refresh 2>&1 | head -15); fails=$((fails+1)); }
dump edited; same "files changed on disk, then flatpack:refresh" edited

# A deploy into a new release directory with shared storage: the app is at a new path and the
# compiled content, which holds absolute paths, comes with it. A build there, or a refresh, has
# to give the same answers from the new path.
M="$C-moved"; rm -rf "$M" "$M-again"; cp -R "$C" "$M"
(cd "$M" && flatpack please flatpack:build -q) || { echo " build after moving failed:"; (cd "$M" && flatpack please flatpack:build 2>&1 | head -15); fails=$((fails+1)); }
flatpack "$HERE/dump.php" "$M" "$OUT/c-moved.json" >/dev/null
check "moved to a new directory, then flatpack:build" python3 "$HERE/compare.py" "$OUT/o-edited.json" "$OUT/c-moved.json" --show 5
cp -R "$M" "$M-again"
(cd "$M-again" && flatpack please flatpack:refresh -q) || { echo " refresh after moving failed:"; (cd "$M-again" && flatpack please flatpack:refresh 2>&1 | head -15); fails=$((fails+1)); }
flatpack "$HERE/dump.php" "$M-again" "$OUT/c-moved-again.json" >/dev/null
check "moved again, then flatpack:refresh" python3 "$HERE/compare.py" "$OUT/o-edited.json" "$OUT/c-moved-again.json" --show 5
rm -rf "$M" "$M-again"

# Switched off, Flatpack must stay out of the way: Statamic's own commands work, and
# flatpack:build / flatpack:refresh fall back to Statamic's own clear + warm.
check "switched off: stache:warm, flatpack:build and flatpack:refresh still work" bash -c "cd '$O' && FLATPACK_ENABLED=false '${PHP[0]}' please stache:warm -q && FLATPACK_ENABLED=false '${PHP[0]}' please flatpack:build -q && FLATPACK_ENABLED=false '${PHP[0]}' please flatpack:refresh -q"
Expand Down
Loading