Code Quality and Deployment Checks in Acquia Pipelines

Acquia

Code Quality and Deployment Checks in Acquia Pipelines

A practical guide to validating Composer metadata, React build output, Drupal coding standards, PHP syntax, YAML files, artifacts, and notifications in acquia-pipelines.yaml.

An acquia-pipelines.yaml file can do much more than assemble a Drupal artifact. It can reject invalid dependency metadata, detect stale frontend builds, enforce Drupal coding standards, lint PHP and YAML files, notify the team when a build fails, and explain exactly which artifact is ready for deployment.

This guide examines a production-oriented pipeline using PHP 8.3, Composer 2, MySQL, Memcached, Node.js 24, a custom React-powered Drupal theme, GrumPHP, PHPCS, and Slack notifications. The objective is not merely to make a pipeline green. It is to make failures early, specific, and actionable.

Pipeline services and global variables

The pipeline begins by declaring the runtime services needed during the build:

version: 1.3.0

services:
  - php:
      version: 8.3
  - composer:
      version: 2
  - mysql
  - memcached

variables:
  global:
    COMPOSER_BIN: $SOURCE_DIR/vendor/bin
    PIPELINE_ENV: true
    CI: true
    BRANCH: ${PIPELINE_VCS_PATH:-unknown}
    PIPELINE_JOB_URL: https://cloud.acquia.com/a/applications/$PIPELINE_APPLICATION_ID/pipelines/jobs/$PIPELINE_JOB_ID

The PHP version should match the supported application runtime closely. Composer 2 provides deterministic dependency installation from composer.lock. MySQL is available for installation or update tests, while Memcached allows the build environment to resemble the application's cache stack when tests require it.

CI=true also tells many frontend tools to use non-interactive CI behavior. Pipeline-provided identifiers are used to build a link back to the Acquia job in notifications.

Never commit a plain Slack webhook, API token, password, or SSH private key. Store sensitive values using Acquia's supported encrypted-variable mechanism and redact them from documentation and logs.

Check 1: install the expected Node.js version

The first build step establishes the frontend runtime:

- install-node:
    script:
      - nvm install 24
      - nvm use 24

Pinning Node.js prevents the build from silently changing when a runner's default version changes. For even tighter reproducibility, keep the same version in local development configuration and the theme's package metadata.

This step verifies that the required Node runtime can be installed and selected. It does not install theme packages yet.

Check 2: reproduce the React theme build

The frontend check installs exactly what is recorded in package-lock.json, builds the custom theme, and verifies that committed build output is current:

- build-react-theme:
    type: script
    script:
      - npm --prefix docroot/themes/custom/swift_devportal_9 ci
      - npm --prefix docroot/themes/custom/swift_devportal_9 run build
      - git diff --exit-code -- docroot/themes/custom/swift_devportal_9/js/build/

Each command answers a separate question:

  1. npm ci asks whether package.json and package-lock.json describe a reproducible installation.
  2. npm run build asks whether the source code can produce deployable frontend assets.
  3. git diff --exit-code asks whether the generated js/build directory matches what was committed.

The final command is valuable when compiled assets are stored in Git and deployed as part of the Drupal artifact. A nonzero exit means a developer changed source files without committing the corresponding build, or generated output differs between environments.

If compiled assets are intentionally excluded from Git, replace this drift check with artifact-existence, size, or smoke checks appropriate to that delivery model.

Check 3: validate Composer metadata

Before installing PHP dependencies, validate the project definition:

- setup-env:
    type: script
    script:
      - composer validate --no-check-all --ansi
      - composer install --ansi --no-interaction --prefer-dist --no-progress
      - mysql -u root -proot -e "CREATE DATABASE IF NOT EXISTS drupal"

composer validate detects malformed JSON, invalid package definitions, and inconsistencies between composer.json and composer.lock. --no-check-all avoids some stricter publishing-oriented checks that are less relevant to an application repository.

composer install then proves the locked dependency graph can be installed without prompting. --prefer-dist normally reduces checkout overhead, and --no-progress keeps CI logs compact.

Creating the empty database prepares the build for later Drupal installation or database-update tests. It is environment setup, not a database-schema verification by itself.

Check 4: count and report the validation scope

The validation step prints its intended scope before running tools:

PHPCS_FILE_COUNT=$(find docroot/modules/custom docroot/themes/custom tests \
  -type f \( -name '*.php' -o -name '*.module' -o -name '*.inc' \
  -o -name '*.install' -o -name '*.test' -o -name '*.profile' \
  -o -name '*.theme' -o -name '*.css' -o -name '*.info' \
  -o -name '*.txt' -o -name '*.md' -o -name '*.yml' \) \
  2>/dev/null | wc -l | xargs)

Similar commands count PHP-lint and YAML-lint candidates. These counts do not validate files; they make the job observable. A surprising drop to zero can reveal a renamed directory, incorrect checkout, or broken search scope that might otherwise produce a misleading green build.

The pipeline also prints the PHPCS standard and scanned paths. When a check becomes slow or unexpectedly misses files, this information makes diagnosis much easier.

2>/dev/null keeps optional missing directories from cluttering logs, but it can also conceal genuine filesystem problems. Use it only when missing paths are expected, and consider failing explicitly when a required custom-code directory is absent.

Check 5: run the aggregate Drupal validation suite

The main validation command is deliberately short:

- validate:
    type: script
    script: |
      set -euo pipefail
      START_TIME=$(date +%s)

      # Print branch, check names, paths, and file counts here.

      composer drupal:validate

      END_TIME=$(date +%s)
      echo "validate step took $((END_TIME - START_TIME)) seconds"

In this project, the Composer script delegates to GrumPHP:

{
  "scripts": {
    "drupal:phpcs": "php -d error_reporting='E_ALL & ~E_DEPRECATED' vendor/bin/phpcs --standard=phpcs.xml.dist --runtime-set ignore_warnings_on_exit 1",
    "drupal:validate": "grumphp run"
  }
}

The GrumPHP suite runs four checks: Composer validation, the drupal:phpcs Composer script, PHP syntax linting, and YAML linting. Keeping tool configuration in repository files allows developers to run the same suite locally:

composer drupal:validate

set -euo pipefail strengthens the surrounding shell step. It exits on a failed command, rejects unset variables, and preserves failures from commands inside pipelines. Without it, a later successful command can sometimes hide an earlier failure.

Check 6: enforce Drupal coding standards with PHPCS

The composer_script GrumPHP task invokes drupal:phpcs, which uses phpcs.xml.dist. The ruleset scans:

  • docroot/modules/custom
  • docroot/themes/custom
  • tests

It includes Drupal file extensions such as .module, .install, .profile, and .theme, along with selected CSS, YAML, Markdown, and metadata files. It excludes generated or third-party directories including vendor, node_modules, and Behat paths.

PHPCS can identify coding-standard violations, debug statements, deprecated PHP functions, invalid line endings, unsafe short tags, inconsistent naming, and many maintainability problems configured by the ruleset.

The command uses ignore_warnings_on_exit 1, so warnings do not fail the build; errors still do. That is a policy choice. Teams should periodically review warnings rather than allowing them to become permanent background noise.

Check 7: lint PHP syntax

GrumPHP's phplint task parses PHP-family files and reports syntax errors without bootstrapping Drupal. It catches problems such as:

  • Missing semicolons
  • Unbalanced braces or parentheses
  • Invalid PHP syntax
  • Parse errors in .module, .install, .theme, or related files

Syntax linting is fast and broad, but it cannot find incorrect types, nonexistent methods, Drupal API misuse, or runtime logic defects. Add PHPStan and automated tests when the project needs deeper analysis.

The pipeline's printed PHP file count helps confirm that linting still covers the custom modules, custom themes, and tests intended by the team.

Check 8: lint YAML files

Drupal depends heavily on YAML for services, routes, permissions, configuration, libraries, and deployment definitions. One indentation error can prevent container compilation or configuration import.

The pipeline counts .yml and .yaml files while excluding:

  • vendor
  • Drupal core
  • Contributed modules and themes
  • Generated public files

GrumPHP's yamllint task then validates the configured YAML scope, with the project's blt directory excluded. The check should include acquia-pipelines.yaml itself, custom module YAML, custom theme libraries, and project-owned configuration.

YAML linting checks structure and syntax. It does not prove that Drupal service IDs, route callbacks, configuration dependencies, or Acquia-specific keys are semantically correct. Drupal configuration import and platform execution provide those later checks.

Check 9: measure validation duration

Recording the validation start and end time produces a simple performance signal:

START_TIME=$(date +%s)
composer drupal:validate
END_TIME=$(date +%s)
echo "validate step took $((END_TIME - START_TIME)) seconds"

This is not a pass/fail quality check, but it helps identify sudden CI slowdowns. If validation time grows significantly, inspect file counts, dependency downloads, cache behavior, and tool configuration before developers begin bypassing the pipeline because it feels too slow.

Check 10: notify the team after a failed build

The fail-on-build event sends a Slack message containing the branch and a direct link to the Acquia Pipelines job:

fail-on-build:
  steps:
    - fail:
        type: script
        script: |
          message=":rotating_light: *Build Artifact Failed*
          • *Branch name:* $BRANCH
          • *Pipeline job:* <$PIPELINE_JOB_URL|View Job>"

          curl -X POST -H 'Content-type: application/json' \
            --data "{\"text\": \"$message\"}" \
            "$SLACK_WEBHOOK_URL" \
            && echo "Slack failure notification sent." \
            || echo "Failed to send Slack failure notification."

          pipelines-artifact fail || true

Notification failure is intentionally non-blocking because the build has already failed for another reason. However, the webhook should be checked for a nonempty value before calling curl, just as the success handler does.

Constructing JSON through shell interpolation can break when branch names or messages contain quotes, backslashes, or newlines. A safer production implementation builds the payload with jq --arg and uses curl --fail-with-body so HTTP errors are visible.

Check 11: report a successful artifact without automatic CDE deployment

The post-deploy event records artifact details and sends a success notification:

ARTIFACT_BRANCH="${PIPELINE_DEPLOY_VCS_PATH:-pipelines-build-${BRANCH}}"
BRANCH_COMMIT="${PIPELINE_GIT_HEAD_REF:-$(git rev-parse HEAD 2>/dev/null || echo unknown)}"

echo "Artifact branch: ${ARTIFACT_BRANCH}"
echo "Commit: ${BRANCH_COMMIT}"
echo "Please verify/select this artifact branch from the Acquia Cloud UI."

Because cde-databases is commented out, the pipeline creates the artifact but deliberately skips automatic Cloud Development Environment deployment. This separates artifact production from environment promotion and allows a human to select the artifact branch in Acquia Cloud.

The success Slack message includes branch, artifact branch, commit, job URL, and the next operational step. This is an important delivery check: a green build is useful only if the team knows exactly which artifact was created and how to promote it.

Check 12: deploy pull-request lifecycle events

The final hooks run the Acquia deployment command for merged or closed pull requests:

pr-merged:
  steps:
    - deploy:
        script:
          - pipelines-deploy

pr-closed:
  steps:
    - deploy:
        script:
          - pipelines-deploy

These events should be tested carefully against the team's intended environment lifecycle. In particular, confirm what a closed-but-unmerged pull request should do. If closing a pull request should clean up a temporary environment rather than deploy an artifact, encode and document that distinction explicitly.

Checks this pipeline does not yet run

The pipeline provides a strong syntax and coding-standard baseline, but a mature Drupal delivery process can add:

  • composer audit --locked for published PHP dependency advisories
  • npm audit --omit=dev for production frontend dependencies
  • PHPStan for static analysis of custom PHP code
  • Gitleaks or another secret scanner
  • ESLint and Stylelint for frontend source
  • Twig syntax linting
  • Drupal installation and configuration-import tests
  • Database-update tests against a sanitized production copy
  • PHPUnit, Kernel, Functional, Behat, or Playwright tests
  • Accessibility, Lighthouse, and broken-link checks against a deployed environment
  • Artifact inspection to ensure secrets, development files, and unexpected packages are excluded

The commented test-updates section is a natural place for database-update validation, but it requires carefully managed SSH access and sanitized live content. Never put an unencrypted private key in the repository or pipeline logs.

Recommended execution order

A reliable Acquia pipeline should fail from cheapest checks to most expensive operations:

  1. Validate YAML and Composer metadata.
  2. Install locked PHP and npm dependencies.
  3. Run PHP syntax and coding-standard checks.
  4. Run static analysis and dependency-security audits.
  5. Build frontend assets and verify generated-output drift.
  6. Run automated application tests.
  7. Test database updates or configuration import when applicable.
  8. Create the artifact.
  9. Notify the team and promote the artifact according to environment policy.

This ordering shortens feedback time and avoids spending resources on deployments that basic validation would reject.

Final recommendations

Keep the pipeline understandable enough that developers can reproduce its checks locally. Pin major runtimes, install dependencies from lockfiles, print scan scope, fail on actionable quality errors, and make notifications secondary to the actual build result.

Most importantly, treat acquia-pipelines.yaml, composer.json, grumphp.yml, phpcs.xml.dist, and the frontend lockfile as one coordinated quality system. The pipeline orchestrates the work; those repository-owned files define what “valid” means.