Engineering Log: Decoupled CI/CD Automation, Drupal 11.4.5 & Markdown Engine Enhancements

β€’ zo, 13 sep 2026
Engineering Log: Decoupled CI/CD Automation, Drupal 11.4.5 & Markdown Engine Enhancements
πŸ“Έ Photo / Image Credit: Illustration: Decoupled Publishing Pipeline Architecture Concept (AI-impression)

πŸš€ Engineering Log: Decoupled CI/CD Automation, Drupal 11.4.5 & Markdown Engine Enhancements

A technical breakdown of infrastructure, deployment pipeline, and CMS engine updates made to endegraaf.nl since the August 29th architecture writeup.



πŸ’‘ Executive Summary & Core Takeaways

Over the past two weeks (August 29 – September 13, 2026), site maintenance for endegraaf.nl focused exclusively on infrastructure hardening, automated deployment triggers, and fixing Markdown ingestion edge cases.

1. Unattended Cross-Repo CI/CD: Pushing changes to endegraaf-content now automatically fires a repository_dispatch event to endegraaf-drupal-01, executing a zero-touch SSH deployment on Strato hosting without manual intervention.
2. Drupal 11.4.5 Upgrade: Upgraded Drupal core dependencies to 11.4.5, maintaining compatibility under PHP 8.4 runtime environments.
3. Twig Template Cache Lock Remediation: Added pre-requisite file purges of sites/default/files/php/twig/* prior to running drush cr to prevent fatal DirectoryIterator errors during cache rebuilds.
4. Slug-Based Node Resolution: Refactored import-markdown.php to resolve existing nodes using path alias slugs (/articles/{slug}) rather than node title matches, eliminating duplicate node creation.
5. Image Skip Validation (currentImageFile): Extended import-markdown.php skip conditions to compare the currently attached Drupal image entity (field_image) against the target image filename, ensuring updates to frontmatter image: references trigger full node re-saves even if post text is unchanged.



πŸ›οΈ 1. Decoupled Cross-Repository CI/CD Pipeline

In our initial architecture, canonical Markdown content (endegraaf-content) and Drupal codebase/configuration (endegraaf-drupal-01) were separate, but triggering production updates still required either a secondary git push to the Drupal repo or running manual SSH commands.

We completed the unidirectional 2-repo event pipeline:

Decoupled Cross-Repository CI/CD Pipeline Architecture
Figure 1: Decoupled 4-Stage CI/CD Pipeline Architectureβ€”Content Push, Event Dispatcher, Remote SSH Execution & Production Node Sync.

graph TD
    A["endegraaf-content
(Markdown & Asset Source)"] -->|1. git push main| B["GitHub Action: trigger-deploy.yml"] B -->|2. repository_dispatch event| C["endegraaf-drupal-01
GitHub Actions Runner"] C -->|3. SSH via appleboy/ssh-action| D["Production Cloud Host
(${{ secrets.STRATO_SSH_HOST }})"] D -->|4. git fetch & reset --hard| E["Local Repository Clones"] E -->|5. php8.4 drush php:script| F["import-markdown.php Engine"] F -->|6. Cache Invalidation & Twig Purge| G["Live Site Engine
(endegraaf.nl)"]

Dispatch Trigger Workflow (endegraaf-content)

In endegraaf-content/.github/workflows/trigger-deploy.yml, we introduced an event dispatch step using peter-evans/repository-dispatch@v3:

name: Trigger Drupal Site Deployment

on:
  push:
    branches:
      - main

jobs:
  dispatch:
    runs-on: ubuntu-latest
    steps:
      - name: Trigger endegraaf-drupal-01 Deployment
        uses: peter-evans/repository-dispatch@v3
        with:
          token: ${{ secrets.DISPATCH_TOKEN || secrets.GITHUB_TOKEN }}
          repository: endegraaf/endegraaf-drupal-01
          event-type: content_updated

Production Deployment & SSH Execution (endegraaf-drupal-01)

The receiver workflow in endegraaf-drupal-01/.github/workflows/deploy.yml listens for repository_dispatch (event-type: content_updated) and executes remote provisioning over SSH:

name: Deploy to Production Cloud (endegraaf.nl)

on:
  push:
    branches:
      - main
  workflow_dispatch:
  repository_dispatch:
    types: [content_updated]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Trigger Remote Deployment & Import on Production Cloud
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.STRATO_SSH_HOST }}
          username: ${{ secrets.STRATO_SSH_USER }}
          password: ${{ secrets.STRATO_SSH_PASSWORD }}
          key: ${{ secrets.STRATO_SSH_KEY }}
          script: |
            rm -f /var/www/drupal_01/.git/index.lock
            cd /var/www/drupal_01
            git fetch origin
            git reset --hard origin/main
            if [ ! -d "/var/www/endegraaf-content" ]; then
              git clone git@github.com:endegraaf/endegraaf-content.git /var/www/endegraaf-content
            else
              cd /var/www/endegraaf-content && rm -f .git/index.lock && git fetch origin && git reset --hard origin/main
            fi
            POSTS_DIR="/var/www/endegraaf-content/posts"
            cd /var/www/drupal_01
            php8.4 ./vendor/drush/drush/drush.php php:script scripts/import-markdown.php -- "$POSTS_DIR"
            rm -rf sites/default/files/php/twig/*
            php8.4 ./vendor/drush/drush/drush.php cr

Key Pipeline Hardening Steps:


  • Git Lock File Purge: Executing rm -f .git/index.lock prior to git fetch prevents deployment failures caused by stale locks left behind by interrupted SSH sessions.

  • Twig Cache Directory Invalidation: Deleting sites/default/files/php/twig/* before invoking drush cr prevents PHP DirectoryIterator fatal errors when Drush scans compiled Twig templates during cache rebuilding.


Twig Cache Invalidation Process Sequence
Figure 2: Deployment Sequence & Pre-requisite Twig Cache Directory Invalidation.


πŸ”§ 2. Ingestion Engine Enhancements (scripts/import-markdown.php)

Slug-Based Path Alias Resolution

To prevent duplicate Drupal node generation when editing existing Markdown files, import-markdown.php was refactored to look up existing entities via Drupal's path_alias entity storage:

// Resolve node by canonical URL alias slug (/articles/{slug})
$aliasPath = '/articles/' . $frontmatter['slug'];
$pathAlias = \Drupal::entityTypeManager()->getStorage('path_alias')->loadByProperties([
  'alias' => $aliasPath,
  'langcode' => 'nl',
]);

if (!empty($pathAlias)) {
  $aliasObj = reset($pathAlias);
  $path = $aliasObj->getPath(); // Resolves internal path: /node/52
  if (preg_match('#/node/(\d+)#', $path, $matches)) {
    $existingNode = Node::load($matches[1]);
  }
}

Image Reference Skip Validation (currentImageFile)

The importer optimizes performance by skipping posts that haven't changed. Previously, the skip check evaluated body, summary, status, and taxonomy tags.

However, if an article's image: frontmatter reference was updated (e.g. replacing a diagram or cover photo with a new filename) without altering the Markdown text, the importer evaluated all existing checks as TRUE and skipped the node update ([SKIP] Node ID ... is up to date). As a result, the Drupal node kept pointing to its old image entity.

We updated the skip condition to inspect the existing attached image file on the Drupal node (field_image) and compare it against the target image filename:

import-markdown.php Skip Decision Engine Flowchart
Figure 3: Decision Engine Flowchart inside import-markdown.php for evaluating Node updates vs Skip branch.

$currentImageFile = '';
if ($node->hasField('field_image') && !$node->get('field_image')->isEmpty()) {
  $imgEntity = $node->get('field_image')->entity;
  if ($imgEntity) {
    $currentImageFile = basename($imgEntity->getFileUri());
  }
}
$targetImageFile = !empty($imagePath) ? basename($imagePath) : '';

// Ensure node is NOT skipped if the featured image filename has changed
if ($currentBody === $htmlBody && $currentSummary === $summary && 
    $currentStatus === $status && $currentTagIds === $termIds && 
    $currentImageFile === $targetImageFile) {
  echo "[SKIP] Node ID {$nid} is up to date (no changes): '{$title}'\n";
  return;
}

Tag Array Sanitization

Cleaned up YAML frontmatter tag parsing in scripts/lib/taxonomy_resolver.php to sanitize bracketed or quoted strings (e.g. ["Drupal", "CI/CD"]), ensuring clean taxonomy term mapping without trailing punctuation.

πŸ“¦ 3. Drupal Core Maintenance

  • Drupal 11.4.5 Core Bump: Upgraded Drupal core packages (e7f5e1be) to version 11.4.5, maintaining compliance with PHP 8.4 strict typing and upstream dependency patches.
  • Cache Tag Invalidation: Enforced automatic clearing of node_list and rendered cache tags upon completion of Markdown processing:
if (\Drupal::hasService('cache_tags.invalidator')) {
  \Drupal::service('cache_tags.invalidator')->invalidateTags(['node_list', 'rendered']);
}

πŸ—ΊοΈ 4. Upcoming Technical Tasks

  • WebP Build Hook: Add automated image optimization during deployment to compress .png/.jpg post assets into WebP format before Drush import.
  • JSON-LD Schema Verification: Expand automated checks for BlogPosting and TechArticle structured metadata tags.
  • CI Frontmatter Linter: Add pre-commit schema validation in GitHub Actions to verify required frontmatter keys (title, slug, date, summary, status) prior to dispatching remote deployments.

Posted by endegraaf | Engineering & Systems Architecture | September 13, 2026