soviann / deploy-tasks-bundle
Run-once deploy tasks and service-injected data migrations for Symfony applications (pre-deploy, post-deploy, seeding, one-time commands)
Package info
github.com/Soviann/deploy-tasks-bundle
Type:symfony-bundle
pkg:composer/soviann/deploy-tasks-bundle
Requires
- php: >=8.2
- psr/clock: ^1.0
- psr/log: ^3.0
- symfony/config: ^6.4 || ^7.0 || ^8.0
- symfony/console: ^6.4 || ^7.0 || ^8.0
- symfony/dependency-injection: ^6.4 || ^7.0 || ^8.0
- symfony/event-dispatcher-contracts: ^2.5 || ^3.0
- symfony/filesystem: ^6.4 || ^7.0 || ^8.0
- symfony/finder: ^6.4 || ^7.0 || ^8.0
- symfony/http-kernel: ^6.4 || ^7.0 || ^8.0
- symfony/string: ^6.4 || ^7.0 || ^8.0
Requires (Dev)
- doctrine/dbal: ^4.3
- friendsofphp/php-cs-fixer: ^3.0
- infection/infection: ^0.32
- phpstan/phpstan: ^2.0
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^10.5 || ^11.0
- symfony/clock: ^6.4 || ^7.0 || ^8.0
- symfony/dotenv: ^6.4 || ^7.0 || ^8.0
- symfony/event-dispatcher: ^6.4 || ^7.0 || ^8.0
- symfony/framework-bundle: ^6.4 || ^7.0 || ^8.0
- symfony/lock: ^6.4 || ^7.0 || ^8.0
- symfony/phpunit-bridge: ^8.0.8
- symfony/process: ^6.4 || ^7.0 || ^8.0
Suggests
- doctrine/dbal: Required (^4.3) for the database storage backend
- symfony/event-dispatcher: Required for task lifecycle events
- symfony/lock: Required for concurrent execution protection
- symfony/process: Required by ProcessRunnerTrait for tasks that shell out to external commands
Provides
None
Conflicts
- doctrine/dbal: <4.3
- symfony/event-dispatcher: <6.4
- symfony/lock: <6.4
- symfony/process: <6.4
Replaces
None
README
A Symfony bundle for running one-time (run-once) deploy tasks — service-injected data migrations, backfills, cache warmups, seed scripts, and pre/post-deploy operations — via CLI. Each task is tracked so it executes exactly once across deployments.
Status: pre-1.0. Public API and configuration may change without a major-version bump until
v1.0.0. Breaking changes bump the minor version and are documented inUPGRADE.md.
Why DeployTasksBundle?
- Doctrine Migrations vs. Deploy Tasks —
doctrine/doctrine-migrations-bundleis built for SQL schema changes (DDL) and intentionally discourages injecting Symfony services into migrations. UseDeployTasksBundlefor one-off data migrations, data backfills, seeders, and external API syncs that need full Symfony Dependency Injection (autowired repositories, domain services, message buses, HTTP clients). - No custom
ExecutedTaskboilerplate — replaces hand-rolled "run-once console command" tables or flag files with ready-to-use state tracking (Filesystem JSON or Doctrine DBAL table), concurrent-run protection (symfony/lock), configurable database transactions (per_task,all_or_nothing,none), and staged task groups (predeploy,postdeploy). - Coming from Laravel or Rails? — fills the same role in Symfony as
timokoerber/laravel-one-time-operationsin Laravel, orafter_party/ Shopify'smaintenance_tasksin Ruby on Rails.
Requirements
- PHP >= 8.2 (>= 8.4 for Symfony 8)
- Symfony 6.4 LTS, 7.x or 8.x
Installation
composer require soviann/deploy-tasks-bundle
With Symfony Flex, the bundle is registered automatically. Without Flex, register it manually in config/bundles.php:
return [ // ... Soviann\DeployTasksBundle\SoviannDeployTasksBundle::class => ['all' => true], ];
Flex recipe
The bundle's Flex recipe lives in symfony/recipes-contrib. When Contrib recipes are enabled, composer require soviann/deploy-tasks-bundle registers the bundle, publishes config/packages/soviann_deploy_tasks.yaml, installs the host runner (bin/deploy-tasks-host.sh), and adds the host-task .gitignore entries automatically. Flex asks once per project before running a Contrib recipe — answer yes, or enable them permanently:
composer config extra.symfony.allow-contrib true
The recipe is optional — without it, the bundle works with its default configuration, and deploytasks:host:install scaffolds the host runner on demand.
Alternative: dedicated recipe endpoint
Prefer not to enable Contrib recipes globally? The same recipe is also served from a dedicated endpoint:
composer config extra.symfony.endpoint --json '["https://api.github.com/repos/Soviann/flex-recipes/contents/index.json", "flex://defaults"]'
Quick Start
Creating a task
Tasks are regular Symfony services — inject any autowired dependency in __construct() and implement DeployTaskInterface:
use App\Repository\CategoryRepository; use Soviann\DeployTasksBundle\Attribute\AsDeployTask; use Soviann\DeployTasksBundle\DeployTaskInterface; use Soviann\DeployTasksBundle\TaskResult; use Symfony\Component\Console\Output\OutputInterface; #[AsDeployTask(id: 'task_20260412143000_seed_categories', priority: 10)] final class SeedCategoriesTask implements DeployTaskInterface { public function __construct( private readonly CategoryRepository $categories, ) { } public function getDescription(): string { return 'Seeds the categories table with initial data.'; } public function run(OutputInterface $output): TaskResult { $this->categories->seedDefaults(); $output->writeln('Categories seeded.'); return TaskResult::SUCCESS; } }
Running tasks
Execute all pending tasks:
bin/console deploytasks:run
Check the status of all tasks:
bin/console deploytasks:status
Configuration
# config/packages/soviann_deploy_tasks.yaml soviann_deploy_tasks: slow_task_threshold: 300 # seconds; warn when a task runs longer (nothing is killed) storage: type: filesystem # filesystem | database | custom filesystem: path: '%kernel.project_dir%/var/deploy-tasks' lock: enabled: true
Full reference: docs/storage.md and docs/installation.md.
Storage Backends
Filesystem (default): stores execution records as files in var/deploy-tasks/. No additional dependencies required.
Database: stores execution records in a database table. Requires doctrine/dbal.
Custom: plug in any TaskStorageInterface implementation via storage.type: custom. See docs/storage.md.
Task Groups
Tasks can be assigned to one or more groups (e.g. predeploy, postdeploy) to split a deploy into named stages. Without --group, every command operates on every slot — the default (ungrouped) slot and every declared group; --group=<name> (repeatable) narrows to the tasks declaring the listed group(s), and a multi-group task records one row per matching slot.
#[AsDeployTask(id: 'task_...', groups: 'predeploy')] #[AsDeployTask(id: 'task_...', groups: ['predeploy', 'postdeploy'])]
See docs/creating-tasks.md and docs/commands.md for details.
Host-scope tasks
Host tasks run outside the Symfony container — useful for operations that must execute on the host (Docker restarts, SSH-driven commands, infrastructure prep). The host runner (bin/deploy-tasks-host.sh) is installed automatically by the Flex recipe; without it, one command scaffolds everything:
bin/console deploytasks:host:install
This installs the runner script (executable), creates the configured host-task directory (default deploy/host-tasks/, with a .gitkeep), and adds a Flex-style .gitignore block for the runner's log, lock, and local-override files — each step idempotent. Re-run with --force to refresh the runner after a bundle update. See docs/host-tasks.md for generation, execution, .env cascade, and concurrency details.
Commands
| Command | Description | Options |
|---|---|---|
deploytasks:run |
Execute pending tasks | --dry-run, --rerun-all, --id=<id>, --group=<name> (repeatable), --require-some |
deploytasks:status |
List tasks with their execution state | --no-state, --group=<name> (repeatable), --filter-status=<comma-list>, --show-orphaned |
deploytasks:show <id> |
Show full metadata and every stored execution record for a single task | — |
deploytasks:skip <id> |
Mark a task as skipped (interactive confirm) | --group=<name> (repeatable) |
deploytasks:reset <id> |
Clear the execution record for a task (interactive confirm) | --no-interaction, --group=<name> (repeatable), --force |
deploytasks:prune |
Prune orphaned task execution records from storage (interactive confirm) | --dry-run, --group=<name> (repeatable), --no-interaction, --force |
deploytasks:rollup |
Clear history and mark all tasks as executed | --no-interaction, --group=<name> (repeatable), --force |
deploytasks:generate |
Generate a blank deploy task (PHP class, runs inside the Symfony container) | --dir, --namespace |
deploytasks:create-schema |
Create the storage schema (storages implementing SchemaManageableInterface) |
--dump-sql |
deploytasks:host:install |
Install the host runner, the configured host-task directory, and the .gitignore block (idempotent) |
--force |
deploytasks:host:generate |
Generate a blank deploy task (bash script, runs on the host outside the container) | --dir |
deploytasks:host:skip <id> |
Mark a host-scope task as done in the completion log (interactive confirm) | — |
deploytasks:host:reset <id> |
Remove a host-scope task's completion-log entry | --no-interaction, --force |
deploytasks:host:rollup |
Mark every pending host-scope task as done | --no-interaction, --force |
deploytasks:host:config |
Render (or write) the host runner env config matching soviann_deploy_tasks.host.* |
--write |
Running shell commands
Tasks that shell out to external binaries can opt into the ProcessRunnerTrait, which wraps symfony/process to stream output and enforce a per-call timeout. See docs/creating-tasks.md for setup and behavior notes.
Documentation
Full index: docs/index.md.
| Topic | File |
|---|---|
| Installation, requirements, optional packages | docs/installation.md |
| Creating tasks (attributes, env/group filtering, IDs) | docs/creating-tasks.md |
| Console commands reference | docs/commands.md |
| Storage backends (filesystem, database, custom) | docs/storage.md |
Host-scope tasks (host runner, .env cascade, concurrency) |
docs/host-tasks.md |
| Lifecycle events | docs/events.md |
| Logging (PSR-3, Monolog channel) | docs/logging.md |
| Testing (unit, functional, command tester) | docs/testing.md |
| Security model, host runner hardening | docs/security.md |
| Advanced (custom sorter, locking, slow-task threshold, transactions) | docs/advanced.md |
| Troubleshooting / FAQ | docs/troubleshooting.md |
Project meta: CHANGELOG.md (release notes, Keep-a-Changelog format), UPGRADE.md (breaking-change migration notes), SECURITY.md (vulnerability disclosure), CONTRIBUTING.md (local dev setup and PR conventions).
Security
Failure logs from DBAL-backed storage are scrubbed of full exception objects to avoid leaking connection credentials into shared log sinks. See docs/logging.md and docs/security.md for the full trust model and hardening notes.
Contributing
See CONTRIBUTING.md for guidelines.
License
MIT. See LICENSE for details.