This project is not covered by Drupal’s security advisory policy.
"Profile Updates" lets site administrators review the optional configuration updates shipped by an install profile (or its modules) and decide — one by one, or in bulk — which ones to apply. Unlike a forced hook_post_update_N(), applying a profile update is always a deliberate action taken in the admin UI (or via Drush); nothing runs automatically on drush updb / drush deploy:hook.
The problem it solves
Distributions hit this the moment they have real clients on real sites: not every site wants every profile change. Sites carry local customizations, and forced update hooks can overwrite site-specific work — yet the site owner has no say in the matter. Profile Updates gives them one, while still letting the distribution ship a steady stream of improvements.
It is a lightweight alternative to Config Distro (config_distro): instead of a full config-import workflow, Profile Updates surfaces each shipped change as a discrete, per-site opt-in — reviewable and applied one at a time, straight from the admin UI, with no central sync directory to manage.
How it works
- A developer changes configuration and ships an update task: a small YAML file naming the changed config item(s), with a human-readable label and description.
- Tasks are auto-discovered as plugins from
EXTENSION/update_tasks/*.yml, re-scanned on every cache clear — no import step, no stale registry. - Administrators with the Administer profile updates permission open Configuration » Development » Profile updates and see what actually needs attention on this site.
- They Apply (through the Batch API, so it scales) or Skip updates. Every choice is recorded as an audit-trail entry that stays readable even if the originating task later changes or disappears.
Per-site, computed state
Each update's state is worked out live by diffing the shipped config against the site's active config and consulting the log:
- Pending — shipped config differs from active; actionable now.
- Blocked — actionable, but a required module or another update must come first; the row names each one, including when what holds it back is an update you skipped yourself.
- Up to date — already in sync; nothing to apply.
- Applied / Skipped — recorded decisions, browsable on their own tabs (un-skipping is simply removing a record from the Skipped tab).
Key features
- Nothing forced on deploy — applying is always an explicit, per-update decision.
- Version-aware — state is keyed to a content hash of the shipped config, so a re-released update re-appears as pending automatically instead of being silently missed.
- Diff on every row — see exactly what an update would change; the Applied tab even shows the historical before/after captured at apply time.
- Dependencies — a task can require modules to be enabled or other tasks to be applied first, and stays blocked until they are.
- Multi-config tasks applied in order inside a DB transaction (config writes roll back on failure; structural DDL implicit-commits and does not), an ordering
weight, and a full audit log. - Drush support —
pu:list,pu:apply,pu:skip— for CI and automation.
For distribution maintainers
The optional Profile Updates: Export submodule (dev-only) generates update tasks straight from a diff of your active configuration, grouped per module or profile. It can alternatively generate forced hook_post_update_N() hooks for changes that genuinely must not be optional. The runtime module has no dependency on it — any module can ship tasks by hand.
A task is a tracker, not a migration: it names the config it watches, and each config item is claimed by exactly one task the first time it is exported — hand-written tasks included. Later releases of that config leave the task file alone and only change what the profile ships, because every site's diff is computed live against its own active config.
Everything runs through a single Drush command once the submodule is enabled:
drush profile-updates-export— re-export active config back into the profile/moduleconfig/install/config/optionalfiles.--diff— dry run;--module=a,b— scope to extensions;--no-new— existing files only;--new-config-target=profile|owner— where brand-new config lands (default: profile).--with-update-tasks— also generate optional update tasks (enableprofile_updates_export_update_tasks); add--group=slug --label="…"to bundle everything changed this run into one task, spanning modules if it spans.--label="…"on its own — rename the task tracking the config you changed this run, leaving its config set alone. Since the log snapshots a task's label at apply time, refreshing it each release is what makes the update history read like a changelog rather than a list of module names.--with-update-hooks— also generate forcedpost_updatehooks (enableprofile_updates_export_update_hooks).
Setup: the export writes back into the profile as the running site sees it, so the profile must be your editable source under the docroot — either living in the docroot directly (developing in the profile's own repo) or symlinked into it (e.g. a Composer path repository, or a git submodule). A profile copied in by Composer as a normal package works too, but those writes are untracked and get overwritten on the next composer install.
Requirements
- Drupal 10.3+ or 11.
- Configuration Update Manager (
drupal/config_update), used to revert/import and diff each config item.
Project information
- Project categories: Administration tools
- Created by r_cheh on , updated
This project is not covered by the security advisory policy.
Use at your own risk! It may have publicly disclosed vulnerabilities.
