From 33a10c2585231696bd822a5f3b37ce6729689563 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Thu, 5 Nov 2026 23:48:08 +0000 Subject: [PATCH 1/2] Apply staged presentation overrides to published API docs --- .github/workflows/ci.yml | 22 +++++++++++++++++++++- docs/PORTAL.md | 6 ++++++ scripts/build-docs.sh | 13 ++++++++++--- scripts/check-docs-portal.mjs | 7 +++++++ scripts/prepare-docs-presentation.py | 6 +++--- scripts/prepare-docs-presentation.test.py | 4 +++- 6 files changed, 50 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 04796d4e..bb7e7227 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -618,7 +618,27 @@ jobs: --include-fragments --root-dir . README.md docs/*.md - - name: Verify the selected source remains unchanged + - name: Stage accepted presentation over the published documentation source + if: matrix.source == '6a821568db5c72983bd3990fe27c7990f092db12' + env: + PRESENTATION_SOURCE: ${{ github.event.pull_request.base.sha || github.sha }} + run: python "$DOCS_TOOLING_DIRECTORY/scripts/prepare-docs-presentation.py" "$PRESENTATION_SOURCE" "$RUNNER_TEMP/stable-presentation" + - name: Qualify the published SDK with its accepted presentation + if: matrix.source == '6a821568db5c72983bd3990fe27c7990f092db12' + env: + DOCS_SCREENSHOTS_DIRECTORY: build/stable-presentation-screenshots + run: | + bash "$DOCS_TOOLING_DIRECTORY/scripts/build-docs.sh" + node "$DOCS_TOOLING_DIRECTORY/scripts/check-docs-portal.mjs" build/site + php "$DOCS_TOOLING_DIRECTORY/scripts/check-docs-analytics.php" build/site + node "$DOCS_TOOLING_DIRECTORY/scripts/check-docs-browser.mjs" build/site + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + if: always() && matrix.source == '6a821568db5c72983bd3990fe27c7990f092db12' + with: + name: php-docs-stable-presentation + path: build/stable-presentation-screenshots + if-no-files-found: ignore + - name: Verify the selected source and presentation remain bound run: | git diff --exit-code HEAD test "$(git rev-parse HEAD)" = "$(git rev-parse '${{ matrix.source == 'current' && github.sha || matrix.source }}^{commit}')" diff --git a/docs/PORTAL.md b/docs/PORTAL.md index 2b1c33b0..e8b254de 100644 --- a/docs/PORTAL.md +++ b/docs/PORTAL.md @@ -32,6 +32,12 @@ bodies and the API index remain selected-source inputs. The workflow-owned release-data adapter reads the selected SDK's manifest and quickstart contract. It never reads release facts from the presentation commit. +The staged template keeps phpDocumentor's `.phpdoc/template` directory shape. +The renderer runs from that staging root, so phpDocumentor's implicit overrides +cannot select the published checkout's older layout. Normal CI retains the +legacy render and also exercises the accepted presentation over the stable +documentation source, including its full browser checks. + Source code, Composer manifests, quickstart/adoption contracts, examples and runtime configuration cannot be overlaid. Generated API symbols still come from the selected published SDK's `src`. The selected Git checkout remains diff --git a/scripts/build-docs.sh b/scripts/build-docs.sh index af8e2e3a..d41375ac 100644 --- a/scripts/build-docs.sh +++ b/scripts/build-docs.sh @@ -5,9 +5,16 @@ set -euo pipefail tooling_directory=$(cd "$(dirname "$0")/.." && pwd) export DOCS_SOURCE_DIRECTORY=${DOCS_SOURCE_DIRECTORY:-$PWD} cd "$DOCS_SOURCE_DIRECTORY" -"$tooling_directory/vendor/bin/phpdoc" \ - -d "$PWD/src" -t "$PWD/build/api" --cache-folder "$PWD/build/phpdoc-cache" \ - --title='Durable Workflow PHP SDK — API Reference' --template="${DOCS_TEMPLATE_DIRECTORY:-$PWD/.phpdoc/template}" +source_directory=$PWD +template_directory=${DOCS_TEMPLATE_DIRECTORY:-$PWD/.phpdoc/template} +# phpDocumentor implicitly loads .phpdoc/template from its working directory. +# Keep the published SDK's old overrides out of the staged presentation build. +( + cd "${DOCS_PRESENTATION_DIRECTORY:-$source_directory}" + "$tooling_directory/vendor/bin/phpdoc" \ + -d "$source_directory/src" -t "$source_directory/build/api" --cache-folder "$source_directory/build/phpdoc-cache" \ + --title='Durable Workflow PHP SDK — API Reference' --template="$template_directory" +) php "$tooling_directory/scripts/finalize-api-reference.php" build/api "$tooling_directory/node_modules/.bin/eleventy" --config="$tooling_directory/.eleventy.cjs" diff --git a/scripts/check-docs-portal.mjs b/scripts/check-docs-portal.mjs index cf803814..084a4e6d 100644 --- a/scripts/check-docs-portal.mjs +++ b/scripts/check-docs-portal.mjs @@ -67,6 +67,7 @@ assert( ); const htmlFiles = filesUnder(siteDirectory, '.html'); +const oneIdentity = fs.existsSync(path.join(siteDirectory, 'assets/identity.css')); // Ids are indexed from the enumerated pages, so fragment checks look up link // targets without reading a path taken from page content. const pageIds = new Map(htmlFiles.map((file) => [ @@ -77,6 +78,12 @@ const elementIds = (file) => pageIds.get(file) ?? new Set(); for (const file of htmlFiles) { const html = fs.readFileSync(file, 'utf8'); const relative = path.relative(siteDirectory, file).replaceAll(path.sep, '/'); + if (oneIdentity) { + for (const control of ['site-header', 'theme-toggle', 'sdk-switcher', 'site-footer']) { + assert.equal(html.split(`class="${control}"`).length - 1, 1, + `${relative} must render one shared identity control: ${control}`); + } + } if (!relative.startsWith('api/')) { assert( !/\b\d+\.\d+\.\d+-(?:alpha|beta|rc)\.\d+\b/.test(html), diff --git a/scripts/prepare-docs-presentation.py b/scripts/prepare-docs-presentation.py index 11fcf224..ccfcd0bb 100644 --- a/scripts/prepare-docs-presentation.py +++ b/scripts/prepare-docs-presentation.py @@ -78,12 +78,12 @@ def prepare(root: Path, revision: str, destination: Path, release_adapter: Path) overlays = presentation_blobs(root, revision) destination.mkdir(parents=True) copy_regular_tree(root / "docs/portal", destination / "portal") - copy_regular_tree(root / ".phpdoc/template", destination / "template") + copy_regular_tree(root / ".phpdoc/template", destination / ".phpdoc/template") for name, content in overlays: if name.startswith("docs/portal/"): path = destination / "portal" / name.removeprefix("docs/portal/") else: - path = destination / "template" / name.removeprefix(".phpdoc/template/") + path = destination / name path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(content) # This workflow-owned adapter reads release facts from DOCS_SOURCE_DIRECTORY. @@ -114,7 +114,7 @@ def main() -> int: adapter = Path(__file__).resolve().parent.parent / "docs/portal/_data/release.js" packet = prepare(root, sys.argv[1], destination, adapter) values = {"DOCS_PORTAL_DIRECTORY": str(destination / "portal"), - "DOCS_TEMPLATE_DIRECTORY": str(destination / "template"), + "DOCS_TEMPLATE_DIRECTORY": str(destination / ".phpdoc/template"), "DOCS_PRESENTATION_DIRECTORY": str(destination), "DOCS_PRESENTATION_REVISION": packet["presentation_source_revision"]} if any("\n" in value or "\r" in value for value in values.values()): diff --git a/scripts/prepare-docs-presentation.test.py b/scripts/prepare-docs-presentation.test.py index b9608f35..e02643d2 100644 --- a/scripts/prepare-docs-presentation.test.py +++ b/scripts/prepare-docs-presentation.test.py @@ -41,6 +41,7 @@ def setUp(self): self.write('docs/portal/guide.md', 'unpublished future support floor') self.write('.phpdoc/template/index.html.twig', 'future static API index') self.write('docs/portal/assets/identity.css', 'new shared palette') + self.write('.phpdoc/template/layout.html.twig', 'new shared layout') self.write('docs/portal/index.md', 'new landing page with release variables') self.git('add', '.') self.git('commit', '-qm', 'future SDK and shared presentation') @@ -68,7 +69,8 @@ def testOnlyPresentationCanChangeAndBothExactSourcesAreRecorded(self): self.assertEqual(packet['presentation_source_revision'], self.design) self.assertEqual((self.output / 'portal/assets/identity.css').read_text(), 'new shared palette') self.assertEqual((self.output / 'portal/guide.md').read_text(), 'published support and examples') - self.assertEqual((self.output / 'template/index.html.twig').read_text(), 'published API index') + self.assertEqual((self.output / '.phpdoc/template/index.html.twig').read_text(), 'published API index') + self.assertEqual((self.output / '.phpdoc/template/layout.html.twig').read_text(), 'new shared layout') self.assertEqual((self.root / 'src/Client.php').read_text(), 'published implementation') self.assertEqual(json.loads((self.root / 'composer.json').read_text())['published_version'], '3.9.4') self.assertEqual(self.git('status', '--porcelain'), '') From 1fd38c711ef7b4b153904cfcf74bd311d9c4aade Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Thu, 5 Nov 2026 23:51:56 +0000 Subject: [PATCH 2/2] Name the staged phpDocumentor override directory once --- scripts/prepare-docs-presentation.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/prepare-docs-presentation.py b/scripts/prepare-docs-presentation.py index ccfcd0bb..5bd25fa0 100644 --- a/scripts/prepare-docs-presentation.py +++ b/scripts/prepare-docs-presentation.py @@ -20,6 +20,7 @@ ".phpdoc/template/components/identity-footer.html.twig", }) PACKET_FILE = "presentation.json" +TEMPLATE_DIRECTORY = ".phpdoc/template" PRESENTATION_TREES = ("docs/portal/assets/", ".phpdoc/template/assets/") @@ -78,7 +79,7 @@ def prepare(root: Path, revision: str, destination: Path, release_adapter: Path) overlays = presentation_blobs(root, revision) destination.mkdir(parents=True) copy_regular_tree(root / "docs/portal", destination / "portal") - copy_regular_tree(root / ".phpdoc/template", destination / ".phpdoc/template") + copy_regular_tree(root / TEMPLATE_DIRECTORY, destination / TEMPLATE_DIRECTORY) for name, content in overlays: if name.startswith("docs/portal/"): path = destination / "portal" / name.removeprefix("docs/portal/") @@ -114,7 +115,7 @@ def main() -> int: adapter = Path(__file__).resolve().parent.parent / "docs/portal/_data/release.js" packet = prepare(root, sys.argv[1], destination, adapter) values = {"DOCS_PORTAL_DIRECTORY": str(destination / "portal"), - "DOCS_TEMPLATE_DIRECTORY": str(destination / ".phpdoc/template"), + "DOCS_TEMPLATE_DIRECTORY": str(destination / TEMPLATE_DIRECTORY), "DOCS_PRESENTATION_DIRECTORY": str(destination), "DOCS_PRESENTATION_REVISION": packet["presentation_source_revision"]} if any("\n" in value or "\r" in value for value in values.values()):