π 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:
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.lockprior togit fetchprevents deployment failures caused by stale locks left behind by interrupted SSH sessions. - Twig Cache Directory Invalidation: Deleting
sites/default/files/php/twig/*before invokingdrush crprevents PHPDirectoryIteratorfatal errors when Drush scans compiled Twig templates during cache rebuilding.
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:
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 inscripts/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 version11.4.5, maintaining compliance with PHP 8.4 strict typing and upstream dependency patches. - Cache Tag Invalidation: Enforced automatic clearing of
node_listandrenderedcache 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/.jpgpost assets into WebP format before Drush import. - JSON-LD Schema Verification: Expand automated checks for
BlogPostingandTechArticlestructured 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