oliverthiele/deployer-git-drift

Deployer recipe: detect and warn about direct server-side file changes before deployment

Maintainers

Package info

github.com/oliverthiele/deployer-git-drift

pkg:composer/oliverthiele/deployer-git-drift

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.2.1 2026-07-02 11:09 UTC

This package is auto-updated.

Last update: 2026-07-02 11:27:51 UTC


README

Deployer recipe that detects and warns about files modified directly on the server (via FTP, SFTP, or SSH) before they are silently overwritten by the next deployment.

Packagist Version PHP License Changelog

Pre-1.0 — This package is under active development. Task names and configuration keys may still change before v1.0. Pin to an exact version and review the CHANGELOG before upgrading.

The Problem

Deployer uses an atomic symlink swap for zero-downtime deployments. This means there is no Git repository on the production server — only the deployed files exist. When someone modifies files directly on the server (editing a config file, quick-fixing a bug via FTP, uploading an asset), those changes are invisible. The next deployment silently overwrites them.

This recipe solves the problem by:

  1. Initializing a shallow Git repository in each release directory after deployment
  2. Checking for file changes in the current release before the next deployment starts
  3. Warning the developer and requiring explicit confirmation before overwriting

Requirements

  • PHP >= 8.4
  • Deployer >= 7.0
  • Git installed on the remote server

Installation

composer require --dev oliverthiele/deployer-git-drift

Usage

In your deploy.php:

require 'recipe/common.php';
require __DIR__ . '/vendor/oliverthiele/deployer-git-drift/src/GitDrift.php';

// Hook into the deployment flow
after('deploy:symlink', 'git-drift:init');
before('deploy:vendors', 'git-drift:check');

Hooks are opt-in by design — the recipe registers tasks only, not automatic hooks.

Configuration

// Always abort when drift is detected (default: false — ask interactively)
set('git_drift_abort_on_drift', true);

// Ignore paths that are expected to differ from the Git state
// Typical candidates: generated files, caches, installed dependencies —
// NOT shared_dirs/shared_files, those are handled automatically (see below)
set('git_drift_ignore_paths', [
    'vendor/',
    'node_modules/',
    'var/',
]);

// Additional tracked files that are expected to differ or be rewritten on the
// server (e.g. rewrite rules regenerated by an install routine)
set('git_drift_skip_worktree_paths', [
    'public/.htaccess',
]);

Available Tasks

Task Description
git-drift:init Initialize Git tracking in the release directory after deployment
git-drift:check Check for drift before deployment — warns or aborts
git-drift:status Show drift status without deploying

Run the status check manually at any time:

dep git-drift:status production

Example Output

When drift is detected:

⚠ Server drift detected:

 public/index.php | 5 +++--
 config/system/settings.php | 12 ++++++++----

Untracked files added on server:
public/fileadmin/direct-upload.zip

These changes were made directly on the server.
They will be LOST after this deployment.

Continue deployment and discard changes? [y/N]

How it works

After each deployment, git-drift:init runs git init in the release directory, fetches the deployed branch with --depth=1, and sets FETCH_HEAD as the baseline via git reset. Any subsequent server-side file modifications will appear as changes relative to this baseline.

On the very first deployment after adding this recipe, there is no previous release to compare against, so git-drift:check skips with a notice instead of checking anything. Drift detection becomes active starting with the deployment after that.

Paths listed in git_drift_ignore_paths are written to .git/info/exclude (local gitignore, does not modify project files) so generated, untracked directories are excluded from drift detection.

Shared directories and export-ignored files

Deployer replaces shared_dirs/shared_files paths with symlinks into shared/, so a tracked file underneath one (e.g. a .gitkeep placeholder in a shared uploads directory) will always differ from its Git blob. Files marked export-ignore in .gitattributes are absent from the deployed release entirely and would otherwise look permanently "deleted". Both cases are detected automatically — from Deployer's own shared_dirs/shared_files config and from a git archive comparison — and marked with Git's --skip-worktree bit, so they never show up as drift. The shared symlinks themselves are also appended to .git/info/exclude automatically, the same file git_drift_ignore_paths writes to. No project-specific configuration is needed for either case.

Use git_drift_skip_worktree_paths only for tracked files outside of shared dirs that are still expected to be rewritten on the server, such as .htaccess rules regenerated by an install routine.

Development

The skip-worktree decision logic lives in GitDriftIndexPlanner, a pure class with no Git or Deployer dependency, so it is covered by unit tests without a real repository.

ddev composer install
ddev exec vendor/bin/phpunit
ddev exec vendor/bin/phpstan analyse
ddev exec vendor/bin/php-cs-fixer fix --dry-run --diff

License

MIT — Oliver Thiele