phacility/phabricator · error · Exception

Storage patch "%s" executes in phase "%s", but depends on pa

Error message

Storage patch "%s" executes in phase "%s", but depends on patch "%s" which is in a different phase ("%s"). Patches may not have dependencies across phases.

What it means

Storage patches execute in phases ('default' and 'worker') precisely so that a set of patches can complete before daemons restart during an upgrade. A dependency that crosses phases would make the phase split meaningless (the worker-phase patch might run before its default-phase dependency finishes), so the loader forbids it.

Source

Thrown at src/infrastructure/storage/patch/PhabricatorSQLPatchList.php:235

      }
    }

    foreach ($specs as $key => $patch) {
      foreach ($patch['after'] as $after) {
        if (empty($specs[$after])) {
          throw new Exception(
            pht(
              "Patch '%s' references nonexistent dependency, '%s'. ".
              "Patches may only depend on patches which actually exist.",
              $key,
              $after));
        }

        $patch_phase = $patch['phase'];
        $after_phase = $specs[$after]['phase'];

        if ($patch_phase !== $after_phase) {
          throw new Exception(
            pht(
              'Storage patch "%s" executes in phase "%s", but depends on '.
              'patch "%s" which is in a different phase ("%s"). Patches '.
              'may not have dependencies across phases.',
              $key,
              $patch_phase,
              $after,
              $after_phase));
        }
      }
    }

    $patches = array();
    foreach ($specs as $full_key => $spec) {
      $patches[$full_key] = new PhabricatorStoragePatch($spec);
    }

    // TODO: Detect cycles?

View on GitHub (pinned to 5720a38cfe)

Solutions

  1. Put the dependent patch in the same phase as its dependency (change its 'phase' key or '@phase' header).
  2. Or remove the cross-phase dependency if the patches are actually independent.
  3. If a whole chain must run early, move the first patch of the chain and all its dependents to the same phase.
  4. Re-run 'bin/storage status' to confirm the constraint passes.

Example fix

// before
'20180102.jobs.sql' => array(
  'type' => 'sql',
  'name' => $this->getPatchPath('20180102.jobs.sql'),
  'phase' => 'worker',
  'after' => array('20180101.tables.sql'),   // default-phase patch
),

// after
'20180102.jobs.sql' => array(
  'type' => 'sql',
  'name' => $this->getPatchPath('20180102.jobs.sql'),
  'after' => array('20180101.tables.sql'),
);
Defensive patterns

Strategy: validation

Validate before calling

// Dependencies must not cross phases:
$specs = PhabricatorSQLPatchList::buildAllPatches(); // itself enforces the rule
foreach ($specs as $key => $patch) {
  foreach ($patch['after'] as $after) {
    if ($patch['phase'] !== $specs[$after]['phase']) {
      throw new Exception("{$key} depends on {$after} across phases");
    }
  }
}

Prevention

When it happens

Trigger: A patch declares 'phase' => 'worker' (or a PHP patch header '// @phase worker') while its 'after' array names a patch in the 'default' phase — or vice versa. The phase comparison happens on the fully assembled global patch map.

Common situations: Moving a patch into the worker phase without moving the patches it builds on; adding a new dependency to an existing phased patch without checking that dependency's phase; forks cherry-picking phased patches onto a different base.

Related errors


AI-assisted analysis of phacility/phabricator@5720a38cfe (2026-08-21). Data as JSON: /api/errors/f8449edafc34b30d. Report an issue: GitHub.