From 38e8b109e2f16136e3c1f23072036fd914e19e71 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Thu, 5 Nov 2026 23:13:07 +0000 Subject: [PATCH] Validate rendered API assets against selected presentation --- .github/workflows/ci.yml | 2 + .github/workflows/docs.yml | 2 + scripts/check-docs-analytics.php | 5 +- scripts/check-docs-analytics.test.py | 92 ++++++++++++++++++++++++++++ 4 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 scripts/check-docs-analytics.test.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0dfbee9d..04796d4e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -580,6 +580,8 @@ jobs: - run: node "$DOCS_TOOLING_DIRECTORY/scripts/check-docs-examples.mjs" - run: npm --prefix "$DOCS_TOOLING_DIRECTORY" run test:docs-portal-links - run: npm --prefix "$DOCS_TOOLING_DIRECTORY" run test:docs-identity-status + - name: Test selected presentation asset validation + run: python "$DOCS_TOOLING_DIRECTORY/scripts/check-docs-analytics.test.py" - name: Verify Composer trust before requests uses: durable-workflow/.github/.github/actions/verify-composer-trust@75587fc33ff40c34f32ef777293dad56bb6f48c7 # clean-trust - run: bash "$DOCS_TOOLING_DIRECTORY/scripts/build-docs.sh" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 0c5af333..464ff418 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -126,6 +126,8 @@ jobs: - run: node "$DOCS_TOOLING_DIRECTORY/scripts/check-docs-examples.mjs" - run: npm --prefix "$DOCS_TOOLING_DIRECTORY" run test:docs-portal-links - run: npm --prefix "$DOCS_TOOLING_DIRECTORY" run test:docs-identity-status + - name: Test selected presentation asset validation + run: python "$DOCS_TOOLING_DIRECTORY/scripts/check-docs-analytics.test.py" - name: Check repository-owned documentation links offline uses: docker://lycheeverse/lychee@sha256:e2d19e57cf6ab037026f20b8e449a1f30d9d7f81eef4194763aab2eab20bd28d # 0.24.2 with: diff --git a/scripts/check-docs-analytics.php b/scripts/check-docs-analytics.php index 53a30278..7b70b274 100644 --- a/scripts/check-docs-analytics.php +++ b/scripts/check-docs-analytics.php @@ -4,13 +4,14 @@ $siteDirectory = rtrim($argv[1] ?? __DIR__.'/../build/site', '/\\'); $sourceDirectory = getenv('DOCS_SOURCE_DIRECTORY') ?: __DIR__.'/..'; +$templateDirectory = getenv('DOCS_TEMPLATE_DIRECTORY') ?: $sourceDirectory.'/.phpdoc/template'; $runtimeSource = file_get_contents($sourceDirectory.'/.phpdoc/template/analytics/analytics.js'); if ($runtimeSource !== false && getenv('CLOUDFLARE_WEB_ANALYTICS_TOKEN')) { $runtimeSource = str_replace('__CLOUDFLARE_WEB_ANALYTICS_TOKEN__', getenv('CLOUDFLARE_WEB_ANALYTICS_TOKEN'), $runtimeSource); } $runtime = file_get_contents($siteDirectory.'/analytics/analytics.js'); -$referenceStyles = file_get_contents($sourceDirectory.'/.phpdoc/template/assets/api-reference.css'); -$referenceRuntime = file_get_contents($sourceDirectory.'/.phpdoc/template/assets/api-reference.js'); +$referenceStyles = file_get_contents($templateDirectory.'/assets/api-reference.css'); +$referenceRuntime = file_get_contents($templateDirectory.'/assets/api-reference.js'); if ($runtimeSource === false || $runtime === false || $runtime !== $runtimeSource) { throw new RuntimeException('Rendered analytics runtime is missing or stale.'); diff --git a/scripts/check-docs-analytics.test.py b/scripts/check-docs-analytics.test.py new file mode 100644 index 00000000..827cbcc4 --- /dev/null +++ b/scripts/check-docs-analytics.test.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +"""Rendered assets follow the selected presentation; analytics stays SDK-owned.""" + +import os +from pathlib import Path +import subprocess +import tempfile +import unittest + + +ROOT = Path(__file__).resolve().parent.parent +CHECKER = ROOT / 'scripts/check-docs-analytics.php' +ANALYTICS = ROOT / '.phpdoc/template/analytics/analytics.js' +STYLES = '.dw-cloud-promotion__eyebrow { letter-spacing: 0; }' +PROMOTION = ('data-promotion-source="sdk-php-reference" ' + 'href="https://cloud.durable-workflow.com/early-access#source=sdk-php-reference"') + + +class RenderedAssetsTest(unittest.TestCase): + def setUp(self): + self.temporary = tempfile.TemporaryDirectory() + self.addCleanup(self.temporary.cleanup) + root = Path(self.temporary.name) + self.source = root / 'sdk' + self.template = self.source / '.phpdoc/template' + self.presentation = root / 'presentation/template' + self.site = root / 'site' + self.write(self.template / 'analytics/analytics.js', ANALYTICS.read_text()) + self.write(self.site / 'analytics/analytics.js', ANALYTICS.read_text()) + self.write(self.template / 'assets/api-reference.css', STYLES + '/* SDK */') + self.write(self.template / 'assets/api-reference.js', '// SDK runtime') + self.write(self.presentation / 'assets/api-reference.css', STYLES + '/* presentation */') + self.write(self.presentation / 'assets/api-reference.js', '// presentation runtime') + for name in ['index.html', 'api/index.html', 'api/classes/Client.html']: + assets = ('href="/assets/api-reference.css" ' + 'type="module" src="/assets/api-reference.js"') if name.startswith('api/') else 'href="/assets/portal.css"' + self.write(self.site / name, f'{assets} {PROMOTION} type="module" src="/analytics/analytics.js"') + self.environment = os.environ | {'DOCS_SOURCE_DIRECTORY': str(self.source)} + for key in ['DOCS_TEMPLATE_DIRECTORY', 'CLOUDFLARE_WEB_ANALYTICS_TOKEN']: + self.environment.pop(key, None) + self.render(self.template) + + @staticmethod + def write(path, text): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text) + + def render(self, template): + for name in ['api-reference.css', 'api-reference.js']: + self.write(self.site / 'assets' / name, (template / 'assets' / name).read_text()) + + def check(self, expected): + result = subprocess.run(['php', str(CHECKER), str(self.site)], env=self.environment, + capture_output=True, text=True, timeout=30) + self.assertEqual(result.returncode == 0, expected, result.stdout + result.stderr) + return result + + def testUnchangedSdkTemplateRemainsTheDefault(self): + self.check(True) + + def testDifferentPresentationAssetsRequireTheirExactSelectedTemplate(self): + self.render(self.presentation) + refused = self.check(False) + self.assertIn('styles are missing or stale', refused.stdout + refused.stderr) + self.environment['DOCS_TEMPLATE_DIRECTORY'] = str(self.presentation) + self.check(True) + + def testChangedRenderedStylesOrRuntimeStillRefuse(self): + self.render(self.presentation) + self.environment['DOCS_TEMPLATE_DIRECTORY'] = str(self.presentation) + for name in ['api-reference.css', 'api-reference.js']: + with self.subTest(name=name): + self.render(self.presentation) + self.write(self.site / 'assets' / name, 'stale') + self.check(False) + + def testMissingSelectedTemplateDoesNotFallBackToSdkAssets(self): + self.environment['DOCS_TEMPLATE_DIRECTORY'] = str(self.presentation / 'absent') + self.check(False) + + def testAnalyticsStillUsesTheSelectedSdkSource(self): + self.render(self.presentation) + self.environment['DOCS_TEMPLATE_DIRECTORY'] = str(self.presentation) + self.write(self.presentation / 'analytics/analytics.js', 'unselected analytics') + self.check(True) + self.write(self.site / 'analytics/analytics.js', 'unselected analytics') + refused = self.check(False) + self.assertIn('analytics runtime is missing or stale', refused.stdout + refused.stderr) + + +if __name__ == '__main__': + unittest.main()