Reusable Drupal module for pre-release QA gate checks and Drush reporting.
Requires
- php: ^8.3
- drupal/core: ^10 || ^11
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Reusable Drupal module for pre-release QA checks, release gating, and Drush reporting.
Purpose
The module is intended to be reused across Drupal 10 and Drupal 11 projects by installing it as a Composer package and enabling it in the target site.
Requirements
- PHP 8.3+
- Drupal 10 or 11
- No extra package is required for shared scanners; this package bundles the
qa_audit_coresubmodule
Features
- Runs a consistent pre-release QA gate through
drush qa:run - Supports full runs or targeted check selection with
--checks=<ids> - Produces table output for operators and JSON output for automation
- Auto-saves timestamped reports to
docs/report/by default - Reads shipped defaults from
qa.config.ymland project overrides fromqa.settingswithout a cache rebuild
Available Checks
config_drift: verifies active configuration matchesconfig/syncprod_split: validates expected production and development config split stateforbidden_modules: blocks dev-only modules from production releasesperformance_hardening: validates cache, asset, Twig, and error-level production settingsstorage_config: verifies public/temporary (and, if configured, private) stream wrappers are registered and writable, and that$settings['config_sync_directory']is set and points at an existing directoryrecent_errors: reads the dblogwatchdogtable and fails on error-level entries in a lookback window (default 24h), grouped by type and message with counts and last location — surfaces the real cause behind Drupal's generic "The website encountered an unexpected error" pagemigration_integrity: detects invalid translation source rows and broken entity references with field-level breakdownstranslation_completeness: reports translation coverage, missing languages, and sample untranslated nodesorphaned_content: finds stale unpublished content and configurable content lacking menu or taxonomy relationshipsbroken_links: validates entity links, internal paths, and sampled external URLsmissing_required_fields: reports published content missing required or configured critical fieldsorphaned_paragraphs: surfaces orphan paragraph findings through the sharedqa_audit_coreservice layerrender_smoke: runs sampled render validation across selected bundlesnavigation: verifies menus are driven by Drupal: required menus have an enabled block in the default theme, links are not broken, unpublished or untranslated, content types do not offer restricted menus (admin, account) to editors, and the theme's layout templates contain no hardcodedhrefsmodule_configuration: verifies that a registered list of contrib modules (e.g.pathauto,honeypot,antibot,simple_sitemap) are installed and properly configured, not just enabledinterface_translations: on multilingual sites withlocaleenabled, reads the translation status cache (populated bydrush locale:checkor the translations report cron job) and warns when a language has updates available, a project has no translation file at all, or the status is stale pastmax_status_age_hours(default 168). "en" is never reported as missing a translation file — interface strings are authored in English, so no project ships one upstream, andtranslate_englishexists precisely to let a site override its own English strings locally via config. Useignored_projectsto skip specific projects entirely, e.g. a small contrib module with no upstream translation for a language the site otherwise supports — a permanent upstream gap a re-check will never resolve
Extending module_configuration
module_configuration delegates to one validator service per module, tagged
qa.module_config_validator (see
src/ReleaseQa/ModuleConfig/ModuleConfigValidatorInterface.php). Shipped
validators live in src/ReleaseQa/ModuleConfig/Validator/:
PathautoValidator: at least one enabled pattern exists; optionally checks specific node bundles are covered; warns when published nodes (per language) in pattern-covered bundles have no URL alias (max_missing_aliases,sample_limit), listing the offenders and the command to generate themBackupMigrateValidator: fails when no schedule is enabled, a schedule has no period or points at a missing source/destination, a destination usespublic://, an unregistered stream wrapper (typicallyprivate://with nofile_private_path) or an unwritable/in-webroot directory, or when no backup file exists; warns when the newest backup is older thanmax_backup_age_days, a schedule keeps every backup, or a source listed inrequired_sourcesis not scheduledHoneypotValidator: fails if honeypot provides no protection at all (time limit is 0, "protect all forms" is off, and no forms are individually protected); warns on a low time limit or missing required formsAntibotValidator: warns if Antibot isn't applied to any forms, or to specific required formsSimpleSitemapValidator: fails if no content is indexed for the sitemap; warns if cron generation is disabledMetatagValidator: fails if theglobaldefault is missing or lackstitle/canonical_url; warns when the front page or content ends up without a description, title or canonical URL (inheritance fromglobalis respected), or the Open Graph / Twitter cards submodules are offSchemaMetatagValidator: warns when noschema_*tag is configured on any default (the module is enabled but outputs no JSON-LD), or when site-level (Organization/WebSite) or content (Article/WebPage) structured data is missing. Site-level tags belong on the front page default — search engines read them from the homepage only; setting them onglobalrepeats them on every page and is reported as a note, not a warning. Options:organization_types(require e.g.LocalBusiness),require_content_schema(disable for sites with nothing to mark up),note_global_site_tags. Configuration only; it does not render a page
To add another module to the checked list:
- Create a class implementing
ModuleConfigValidatorInterfaceinsrc/ReleaseQa/ModuleConfig/Validator/.getModuleName()returns the module's machine name;validate()returns aReleaseQaCheckResult(pass/warn/fail) and is only called once that module is installed. - Register it in
qa.services.ymltaggedqa.module_config_validator(follow the existingqa.module_config.*entries). - Optionally add
options.<module_name>defaults undermodule_configurationinqa.config.yml.
No changes to ModuleConfigurationCheck itself are needed — it discovers
validators through the tagged iterator.
A module with a validator that simply isn't installed is skipped (not a
failure) unless it's listed in required_modules.
Installation
composer require protonsystems/qa
drush en qa
This package bundles the reusable qa_audit_core submodule so release QA and
other ProtonSystems tools can share audit services without a third Composer
project.
Usage
# Run all enabled checks.
drush qa:run
# Run a selected subset of checks.
drush qa:run --checks=config_drift,prod_split,performance_hardening
# Emit JSON to stdout and write a specific artifact file.
drush qa:run --format=json --output=tmp/qa/latest.json
# Fail on warnings as well as failures.
drush qa:run --fail-on-warn
The command alias drush release-qa:run is also available.
Configuration
The module ships package defaults in qa.config.yml at the module root.
Projects should override them through the qa.settings Drupal config, either
through config sync or the admin form at
/admin/config/development/qa. The admin form exposes each check separately,
with an enable/disable override and a YAML field for that check's default
options.
checks:
broken_links:
enabled: false
defaults:
max_links: 200
Rules:
- Shipped
qa.config.ymlvalues act as defaults for every project qa.settingsoverrides are merged on top of those defaults per checkenabled: falseskips the check during an unfiltereddrush qa:run--checks=<id>always runs the named check even if it is disabled in YAMLdefaults:values act as baselines and CLI options always override them- Config changes are read at runtime, so no cache rebuild is required
enabledaccepts boolean values from config sync and Drush, includingfalseand'false'- Some checks expose complex list/mapping defaults, so the per-check override field remains YAML rather than one bespoke widget per option
Useful options:
--fail-on-warn: return a non-zero exit code for warnings--checks=<ids>: comma-separated subset of checks--format=table|json: choose human or machine output--output=<path>: write the JSON report to a specific location--save-report=0|1: control automatic report export todocs/report/--sample-size=<n>,--max-bundles=<n>,--bundles=<ids>: render smoke scope overrides--max-broken-refs=<n>,--max-invalid-translations=<n>: migration integrity thresholds--max-links=<n>: broken link scan limit
Reports
When --save-report is enabled, qa:run writes a timestamped JSON report to
docs/report/qa-YYYY-MM-DD_HHMMSS.json. Passing --output=<path> overrides
the default report location.
For the report format and examples, see docs/report/README.md.
Releasing A New Version On Packagist
Packagist publishes versions of protonsystems/qa from git tags. Do
not add a version field to composer.json; create and push a new release tag
instead.
For the first public release, submit the public Git repository URL to Packagist.
If you want automatic updates from GitLab, configure the Packagist integration
in GitLab under Settings > Integrations using your Packagist username and API
token.
Prepare the release commit on main:
git checkout main
git pull origin main
composer validate --no-check-publish --strict
git status
git add <files>
git commit -m "Release v1.0.0"
git push origin main
Create and push an annotated tag for the release:
git tag -a v1.0.0 -m "Release v1.0.0"
git push origin v1.0.0
Use semantic version tags such as v1.0.0. After the tag is pushed, Packagist
should detect the new version automatically. If it does not appear, open the
package page in Packagist and trigger a manual update.
Notes
- The bundled
qa_audit_coresubmodule ships with this package and does not require a separate Packagist release