$line) { if (! str_contains($line, 'docker compose exec')) { continue; } // Only the app container. The gateway and the queues are other // images with other users, and none of them own this checkout. if (! preg_match('/docker compose exec\b[^|]*?\bapp\b/', $line)) { continue; } if (preg_match('/\s-u\s+\S+/', $line)) { continue; } // Help text counts. Telling an operator to run it as root is how // the file ends up owned by root in the first place. $offenders[] = basename($path).':'.($i + 1).' — '.trim($line); } } expect($offenders)->toBe([]); }); it('repairs ownership before it needs it, not after', function () { // A server already carrying the damage has to heal on its next deployment. // Nothing else ever will: a root-owned log file stays root-owned, and the // page that trips over it is nowhere near the deployment that caused it. $update = File::get(base_path('deploy/update.sh')); expect($update)->toContain('normalise_ownership') ->and($update)->toContain('chown -R www-data:www-data storage bootstrap/cache'); // Before the first unprivileged step, or it cannot help: composer and npm // would already have failed on a root-owned vendor directory. $repair = strpos($update, "\nnormalise_ownership\n"); $maintenance = strpos($update, 'php artisan down --retry=60'); expect($repair)->not->toBeFalse() ->and($maintenance)->not->toBeFalse() ->and($repair)->toBeLessThan($maintenance); });