Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .eleventy.cjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
const fs = require('node:fs');
const path = require('node:path');

const QUICKSTART_CONTRACT = require('./docs/quickstart-contract.json');
// Deployment tooling can be newer than the selected published SDK source.
const sourceDirectory = path.resolve(process.env.DOCS_SOURCE_DIRECTORY || __dirname);
const QUICKSTART_CONTRACT = require(path.join(sourceDirectory, 'docs/quickstart-contract.json'));
const QUICKSTART_BASES = QUICKSTART_CONTRACT.reference_resolution?.bases;
const QUICKSTART_SOURCES = Object.freeze(Object.fromEntries(
Object.entries(QUICKSTART_CONTRACT.sources).map(([name, reference]) => {
Expand Down Expand Up @@ -83,8 +85,8 @@ module.exports = function configure(eleventyConfig) {
if (typeof sourcePath !== 'string') {
throw new Error(`Unknown package-owned quickstart source: ${sourceName}`);
}
const resolved = path.resolve(__dirname, sourcePath);
const examplesDirectory = path.resolve(__dirname, 'examples');
const resolved = path.resolve(sourceDirectory, sourcePath);
const examplesDirectory = path.resolve(sourceDirectory, 'examples');
if (!resolved.startsWith(`${examplesDirectory}${path.sep}`)) {
throw new Error(`Quickstart source must stay under ${examplesDirectory}: ${sourcePath}`);
}
Expand Down
52 changes: 41 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -528,13 +528,36 @@ jobs:
- run: composer analyse

docs:
name: ${{ matrix.source == 'current' && 'docs' || format('docs compatibility ({0})', matrix.source) }}
runs-on: ubuntu-24.04
timeout-minutes: 15
strategy:
fail-fast: false
max-parallel: 2
matrix:
source: [current, '3.9.4', '6a821568db5c72983bd3990fe27c7990f092db12']
env:
DOCS_SOURCE_DIRECTORY: ${{ github.workspace }}
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
ref: ${{ matrix.source == 'current' && github.sha || matrix.source }}
fetch-depth: 0
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: '3.12'
- name: Select the temporary tooling directory
run: echo "DOCS_TOOLING_DIRECTORY=$RUNNER_TEMP/docs-tooling" >> "$GITHUB_ENV"
- name: Prepare exact current tooling against the selected source
env:
DOCS_WORKFLOW_REVISION: ${{ github.sha }}
run: |
git fetch origin "$DOCS_WORKFLOW_REVISION"
git show "$DOCS_WORKFLOW_REVISION:scripts/prepare-docs-tooling.py" > "$RUNNER_TEMP/prepare-docs-tooling.py"
python "$RUNNER_TEMP/prepare-docs-tooling.py" "$DOCS_WORKFLOW_REVISION" "$DOCS_TOOLING_DIRECTORY"
- name: Test documentation release-channel guards
if: matrix.source == 'current'
run: python "$DOCS_TOOLING_DIRECTORY/scripts/test-docs-release-channel.py"
- name: Select authenticated dependency trust
uses: durable-workflow/.github/.github/actions/bootstrap-patched-pip@75587fc33ff40c34f32ef777293dad56bb6f48c7 # clean-trust
with:
Expand All @@ -547,19 +570,18 @@ jobs:
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: '24'
cache: npm
- name: Verify Composer trust before requests
uses: durable-workflow/.github/.github/actions/verify-composer-trust@75587fc33ff40c34f32ef777293dad56bb6f48c7 # clean-trust
- run: composer install --no-interaction --prefer-dist
- run: npm ci --ignore-scripts
- run: npm audit --audit-level=moderate
- run: npm run check:docs-examples
- run: npm run test:docs-portal-links
- run: composer --working-dir="$DOCS_TOOLING_DIRECTORY" install --no-interaction --prefer-dist
- run: npm --prefix "$DOCS_TOOLING_DIRECTORY" ci --ignore-scripts
- run: npm --prefix "$DOCS_TOOLING_DIRECTORY" audit --audit-level=moderate
- run: node "$DOCS_TOOLING_DIRECTORY/scripts/check-docs-examples.mjs"
- run: npm --prefix "$DOCS_TOOLING_DIRECTORY" run test:docs-portal-links
- name: Verify Composer trust before requests
uses: durable-workflow/.github/.github/actions/verify-composer-trust@75587fc33ff40c34f32ef777293dad56bb6f48c7 # clean-trust
- run: composer docs
- run: npm run check:docs
- run: php scripts/check-docs-analytics.php build/site
- run: bash "$DOCS_TOOLING_DIRECTORY/scripts/build-docs.sh"
- run: node "$DOCS_TOOLING_DIRECTORY/scripts/check-docs-portal.mjs" build/site
- run: php "$DOCS_TOOLING_DIRECTORY/scripts/check-docs-analytics.php" build/site
- name: Check rendered portal links before merge
uses: docker://lycheeverse/lychee@sha256:e2d19e57cf6ab037026f20b8e449a1f30d9d7f81eef4194763aab2eab20bd28d # 0.24.2
with:
Expand All @@ -569,9 +591,11 @@ jobs:
--root-dir build/site
build/site
- name: Install the locked Chromium browser
run: ./node_modules/.bin/playwright install --with-deps chromium
run: '"$DOCS_TOOLING_DIRECTORY/node_modules/.bin/playwright" install --with-deps chromium'
- name: Verify missing-resource browser evidence
run: npm run test:docs-browser-failures
run: npm --prefix "$DOCS_TOOLING_DIRECTORY" run test:docs-browser-failures
- name: Check portal and API layouts in Chromium
run: node "$DOCS_TOOLING_DIRECTORY/scripts/check-docs-browser.mjs" build/site
- name: Check repository-owned documentation links offline
uses: docker://lycheeverse/lychee@sha256:e2d19e57cf6ab037026f20b8e449a1f30d9d7f81eef4194763aab2eab20bd28d # 0.24.2
with:
Expand All @@ -581,6 +605,12 @@ jobs:
--include-fragments
--root-dir .
README.md docs/*.md
- name: Verify the selected source remains unchanged
run: |
git diff --exit-code HEAD
test "$(git rev-parse HEAD)" = "$(git rev-parse '${{ matrix.source == 'current' && github.sha || matrix.source }}^{commit}')"
test "$(git -C "$DOCS_TOOLING_DIRECTORY" rev-parse HEAD)" = "$GITHUB_SHA"
python "$DOCS_TOOLING_DIRECTORY/scripts/bind-docs-artifact.py" "$(git rev-parse '${{ matrix.source == 'current' && github.sha || matrix.source }}^{commit}')"
package-smoke:
runs-on: ubuntu-24.04
timeout-minutes: 10
Expand Down
89 changes: 34 additions & 55 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ on:
- '.eleventy.cjs'
- 'scripts/check-docs-*'
- 'scripts/finalize-api-reference.php'
- 'scripts/build-docs.sh'
- 'scripts/prepare-docs-tooling.py'
- 'scripts/bind-docs-artifact.py'
- 'README.md'
- 'composer.json'
- 'package.json'
Expand All @@ -36,6 +39,8 @@ concurrency:
jobs:
build:
runs-on: ubuntu-24.04
env:
DOCS_SOURCE_DIRECTORY: ${{ github.workspace }}
outputs:
stable: ${{ steps.release-channel.outputs.stable }}
steps:
Expand Down Expand Up @@ -72,51 +77,35 @@ jobs:
php-version: '8.3'
tools: composer:2.10.3
coverage: none
- name: Select the temporary tooling directory
run: echo "DOCS_TOOLING_DIRECTORY=$RUNNER_TEMP/docs-tooling" >> "$GITHUB_ENV"
- name: Prepare the exact workflow tooling separately
env:
DOCS_WORKFLOW_REVISION: ${{ github.workflow_sha }}
run: |
if [[ ! "$DOCS_WORKFLOW_REVISION" =~ ^[0-9a-f]{40}$ ]]; then
echo 'The documentation tooling requires the exact workflow revision' >&2
exit 1
fi
git show "$DOCS_WORKFLOW_REVISION:scripts/prepare-docs-tooling.py" > "$RUNNER_TEMP/prepare-docs-tooling.py"
python "$RUNNER_TEMP/prepare-docs-tooling.py" "$DOCS_WORKFLOW_REVISION" "$DOCS_TOOLING_DIRECTORY"
- name: Resolve the public documentation release channel
id: release-channel
env:
STABLE_TAG: ${{ inputs.stable_tag }}
STABLE_SOURCE: ${{ inputs.stable_source }}
GH_TOKEN: ${{ github.token }}
run: |
version=$(php -r '$manifest = json_decode(file_get_contents("composer.json"), true, flags: JSON_THROW_ON_ERROR); echo $manifest["extra"]["durable-workflow"]["product-train"];')
if [ -n "$STABLE_TAG" ]; then
test "$version" = "$STABLE_TAG"
test "$(gh release view "$STABLE_TAG" --repo "$GITHUB_REPOSITORY" --json isDraft,isPrerelease --jq '(.isDraft == false and .isPrerelease == false)')" = true
if [ -z "$STABLE_SOURCE" ]; then
test "$(git rev-parse HEAD)" = "$(git rev-parse "refs/tags/$STABLE_TAG^{commit}")"
else
test "$(git rev-parse HEAD)" = "$STABLE_SOURCE"
git merge-base --is-ancestor HEAD origin/main
git diff --exit-code "refs/tags/$STABLE_TAG" HEAD -- src examples resources config
git show "refs/tags/$STABLE_TAG:composer.json" > "$RUNNER_TEMP/published-sdk-composer.json"
PUBLISHED_MANIFEST="$RUNNER_TEMP/published-sdk-composer.json" php <<'PHP'
<?php
$published = json_decode(file_get_contents(getenv('PUBLISHED_MANIFEST')), true, flags: JSON_THROW_ON_ERROR);
$selected = json_decode(file_get_contents('composer.json'), true, flags: JSON_THROW_ON_ERROR);
unset($published['extra']['durable-workflow']['product-train'], $selected['extra']['durable-workflow']['product-train']);
if ($published !== $selected) {
throw new RuntimeException('Selected documentation source changes the published package manifest.');
}
PHP
fi
fi
if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo stable=true >> "$GITHUB_OUTPUT"
else
echo stable=false >> "$GITHUB_OUTPUT"
fi
run: python "$DOCS_TOOLING_DIRECTORY/scripts/check-docs-release-channel.py"
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: '24'
cache: npm
- name: Verify Composer trust before requests
uses: durable-workflow/.github/.github/actions/verify-composer-trust@75587fc33ff40c34f32ef777293dad56bb6f48c7 # clean-trust
- run: composer install --no-interaction --prefer-dist
- run: npm ci --ignore-scripts
- run: npm audit --audit-level=moderate
- run: npm run check:docs-examples
- run: npm run test:docs-portal-links
- run: composer --working-dir="$DOCS_TOOLING_DIRECTORY" install --no-interaction --prefer-dist
- run: npm --prefix "$DOCS_TOOLING_DIRECTORY" ci --ignore-scripts
- run: npm --prefix "$DOCS_TOOLING_DIRECTORY" audit --audit-level=moderate
- run: node "$DOCS_TOOLING_DIRECTORY/scripts/check-docs-examples.mjs"
- run: npm --prefix "$DOCS_TOOLING_DIRECTORY" run test:docs-portal-links
- name: Check repository-owned documentation links offline
uses: docker://lycheeverse/lychee@sha256:e2d19e57cf6ab037026f20b8e449a1f30d9d7f81eef4194763aab2eab20bd28d # 0.24.2
with:
Expand All @@ -130,19 +119,12 @@ jobs:
uses: durable-workflow/.github/.github/actions/verify-composer-trust@75587fc33ff40c34f32ef777293dad56bb6f48c7 # clean-trust
- name: Build authored portal and generated API reference
env:
DOCS_REQUIRE_ANALYTICS_TOKEN: 'true'
CLOUDFLARE_WEB_ANALYTICS_TOKEN: ${{ vars.CLOUDFLARE_WEB_ANALYTICS_TOKEN }}
run: |
if [ "$GITHUB_SERVER_URL" = https://github.com ]; then
if ! printf '%s' "$CLOUDFLARE_WEB_ANALYTICS_TOKEN" | grep -Eq '^[a-f0-9]{32}$'; then
echo 'CLOUDFLARE_WEB_ANALYTICS_TOKEN must be the canonical 32-character site token' >&2
exit 1
fi
sed -i "s/__CLOUDFLARE_WEB_ANALYTICS_TOKEN__/$CLOUDFLARE_WEB_ANALYTICS_TOKEN/" \
.phpdoc/template/analytics/analytics.js
fi
composer docs
npm run check:docs
php scripts/check-docs-analytics.php build/site
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
test -s build/site/index.html
test -s build/site/api/index.html
test "$(cat build/site/CNAME)" = "php.durable-workflow.com"
Expand All @@ -154,20 +136,17 @@ jobs:
--offline
--root-dir build/site
build/site
- run: ./node_modules/.bin/playwright install --with-deps chromium
- run: '"$DOCS_TOOLING_DIRECTORY/node_modules/.bin/playwright" install --with-deps chromium'
- name: Test missing-resource browser evidence
run: npm run test:docs-browser-failures
run: npm --prefix "$DOCS_TOOLING_DIRECTORY" run test:docs-browser-failures
- name: Check portal and API layouts in Chromium
run: npm run check:docs-browser
run: node "$DOCS_TOOLING_DIRECTORY/scripts/check-docs-browser.mjs" build/site
- name: Bind Pages artifact to the exact source revision
env:
EXPECTED_SOURCE_REVISION: ${{ steps.release-channel.outputs.source_revision }}
run: |
SOURCE_REVISION=$(git rev-parse HEAD)
if ! printf '%s' "$SOURCE_REVISION" | grep -Eq '^[a-f0-9]{40}$'; then
echo 'SOURCE_REVISION must be an exact lowercase commit SHA' >&2
exit 1
fi
printf '{"schema":"durable-workflow.sdk-php.docs-deployment/v1","source_revision":"%s"}\n' \
"$SOURCE_REVISION" > build/site/deployment-audit.json
test "$(git -C "$DOCS_TOOLING_DIRECTORY" rev-parse HEAD)" = "$GITHUB_WORKFLOW_SHA"
python "$DOCS_TOOLING_DIRECTORY/scripts/bind-docs-artifact.py" "$EXPECTED_SOURCE_REVISION"
- uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5
if: github.server_url == 'https://github.com'
with:
Expand Down
26 changes: 26 additions & 0 deletions scripts/bind-docs-artifact.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env python3
"""Bind a rendered site to its verified, unchanged selected Git source."""

import json
import re
import subprocess
import sys
from pathlib import Path


def main() -> int:
if len(sys.argv) != 2 or not re.fullmatch(r"[0-9a-f]{40}", sys.argv[1]):
print("Expected the exact selected source commit.", file=sys.stderr)
return 1
selected = subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip()
if selected != sys.argv[1] or subprocess.run(["git", "diff", "--exit-code", "HEAD"], check=False).returncode:
print("The selected documentation source changed before artifact binding.", file=sys.stderr)
return 1
Path("build/site/deployment-audit.json").write_text(json.dumps({
"schema": "durable-workflow.sdk-php.docs-deployment/v1", "source_revision": selected,
}) + "\n")
return 0


if __name__ == "__main__":
sys.exit(main())
26 changes: 26 additions & 0 deletions scripts/build-docs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail

# Run workflow-owned tools and dependencies against the selected source inputs.
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="$PWD/.phpdoc/template"
php "$tooling_directory/scripts/finalize-api-reference.php" build/api
"$tooling_directory/node_modules/.bin/eleventy" --config="$tooling_directory/.eleventy.cjs"

# Substitute only the generated runtime; keep the selected checkout unchanged.
if [[ "${DOCS_REQUIRE_ANALYTICS_TOKEN:-}" = true ]]; then
if [[ ! "${CLOUDFLARE_WEB_ANALYTICS_TOKEN:-}" =~ ^[a-f0-9]{32}$ ]]; then
echo 'CLOUDFLARE_WEB_ANALYTICS_TOKEN must be the canonical 32-character site token' >&2
exit 1
fi
python - <<'PY'
import os
from pathlib import Path
runtime = Path("build/site/analytics/analytics.js")
runtime.write_text(runtime.read_text().replace("__CLOUDFLARE_WEB_ANALYTICS_TOKEN__", os.environ["CLOUDFLARE_WEB_ANALYTICS_TOKEN"]))
PY
fi
10 changes: 7 additions & 3 deletions scripts/check-docs-analytics.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@
declare(strict_types=1);

$siteDirectory = rtrim($argv[1] ?? __DIR__.'/../build/site', '/\\');
$runtimeSource = file_get_contents(__DIR__.'/../.phpdoc/template/analytics/analytics.js');
$sourceDirectory = getenv('DOCS_SOURCE_DIRECTORY') ?: __DIR__.'/..';
$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(__DIR__.'/../.phpdoc/template/assets/api-reference.css');
$referenceRuntime = file_get_contents(__DIR__.'/../.phpdoc/template/assets/api-reference.js');
$referenceStyles = file_get_contents($sourceDirectory.'/.phpdoc/template/assets/api-reference.css');
$referenceRuntime = file_get_contents($sourceDirectory.'/.phpdoc/template/assets/api-reference.js');

if ($runtimeSource === false || $runtime === false || $runtime !== $runtimeSource) {
throw new RuntimeException('Rendered analytics runtime is missing or stale.');
Expand Down
8 changes: 6 additions & 2 deletions scripts/check-docs-examples.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import {readFile} from 'node:fs/promises';
import Ajv2020 from 'ajv/dist/2020.js';
import addFormats from 'ajv-formats';
import path from 'node:path';
import {pathToFileURL} from 'node:url';

const repoRoot = new URL('../', import.meta.url);
const repoRoot = process.env.DOCS_SOURCE_DIRECTORY
? pathToFileURL(path.resolve(process.env.DOCS_SOURCE_DIRECTORY) + path.sep)
: new URL('../', import.meta.url);
const contract = JSON.parse(
await readFile(new URL('scripts/check-docs-examples-contract.json', repoRoot), 'utf8'),
await readFile(new URL('./check-docs-examples-contract.json', import.meta.url), 'utf8'),
);
const quickstart = JSON.parse(
await readFile(new URL(contract.quickstartContract, repoRoot), 'utf8'),
Expand Down
4 changes: 2 additions & 2 deletions scripts/check-docs-portal.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import Ajv2020 from 'ajv/dist/2020.js';
import {assertPrimaryGuideNavigation} from './check-docs-portal-navigation.mjs';
import {fragmentLinks} from './check-docs-portal-links.mjs';

const siteDirectory = path.resolve(process.argv[2] ?? 'build/site');
const projectDirectory = path.resolve(import.meta.dirname, '..');
const projectDirectory = path.resolve(process.env.DOCS_SOURCE_DIRECTORY || path.join(import.meta.dirname, '..'));
const siteDirectory = path.resolve(process.argv[2] ?? path.join(projectDirectory, 'build/site'));
const composer = JSON.parse(fs.readFileSync(path.join(projectDirectory, 'composer.json'), 'utf8'));
const navigation = JSON.parse(fs.readFileSync(path.join(projectDirectory, 'docs/portal/_data/navigation.json'), 'utf8'));
const quickstartContract = JSON.parse(fs.readFileSync(path.join(projectDirectory, 'docs/quickstart-contract.json'), 'utf8'));
Expand Down
Loading
Loading