From 7d8f63032335d6d3088cb8460cf67cae48041609 Mon Sep 17 00:00:00 2001 From: Linus Schlumberger Date: Thu, 29 Jan 2026 17:29:57 +0100 Subject: [PATCH] chore(docs): add versioned documentation with auto-detection Close #675 --- .github/workflows/release.yaml | 197 +++++++++++++++++++++++++++++++++ docs/_src/version-selector.js | 167 ++++++++++++++++++++++++++++ mkdocs.yml | 1 + pyproject.toml | 2 +- uv.lock | 14 +-- 5 files changed, 373 insertions(+), 8 deletions(-) create mode 100644 docs/_src/version-selector.js diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 8a431c3e9..d57594393 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -16,6 +16,9 @@ jobs: - build-and-test permissions: id-token: write + outputs: + new_release: ${{ steps.check_release.outputs.new_release }} + release_tag: ${{ steps.check_release.outputs.release_tag }} steps: - uses: actions/checkout@v6 with: @@ -30,6 +33,12 @@ jobs: name: dist path: dist - run: npm ci --prefer-offline --no-audit + + - id: before_release + run: | + BEFORE_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + echo "before_tag=$BEFORE_TAG" >> $GITHUB_OUTPUT + - run: npx semantic-release env: SKIP_COMMIT: ${{ github.ref_name == 'next' && 'true' || '' }} @@ -38,3 +47,191 @@ jobs: GIT_COMMITTER_NAME: 'Siemens Element Bot' GIT_COMMITTER_EMAIL: 'simpl.si@siemens.com' GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_GITHUB_TOKEN }} + + - id: check_release + run: | + AFTER_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + BEFORE_TAG="${{ steps.before_release.outputs.before_tag }}" + + if [[ -n "$AFTER_TAG" && "$AFTER_TAG" != "$BEFORE_TAG" ]]; then + echo "release_tag=$AFTER_TAG" >> $GITHUB_OUTPUT + echo "new_release=true" >> $GITHUB_OUTPUT + else + echo "new_release=false" >> $GITHUB_OUTPUT + echo "release_tag=" >> $GITHUB_OUTPUT + fi + + publish-documentation-release: + runs-on: ubuntu-24.04 + needs: + - publish + - build-and-test + if: success() && needs.publish.outputs.new_release == 'true' && github.ref_name != 'next' + permissions: + id-token: write + env: + VERSIONED_BUCKET_NAME: simpl-element-release + CLOUDFRONT_DOMAIN: d2uqfzn4lxgtwv.cloudfront.net + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - id: deploy + uses: actions/github-script@v7 + with: + script: | + const deployAsRelease = "${{ needs.publish.outputs.release_tag }}"; + + if (!deployAsRelease) { + core.setFailed("deploy_as_release input is required"); + return; + } + + const versionRegex = /^v[0-9]+\.[0-9]+\.[0-9]+.*$/; + if (!versionRegex.test(deployAsRelease)) { + core.setFailed(`Error: Deploy as release version must be in format 'v1.2.3' or 'v1.2.3-suffix'\nProvided: '${deployAsRelease}'`); + return; + } + + const version = deployAsRelease.substring(1); + const majorVersion = `v${version.split('.')[0]}`; + const isPrerelease = version.includes('-'); + + const deployMajor = !isPrerelease; + const shouldDeploy = deployMajor; + + core.setOutput('deploy_major', deployMajor); + core.setOutput('major_version', majorVersion); + core.setOutput('version', version); + core.setOutput('should_deploy', shouldDeploy); + + console.log('DEPLOYMENT PLAN'); + console.log('==============='); + console.log(`Branch: ${{ github.ref }}`); + console.log(`Trigger: ${{ github.event_name }}`); + console.log(`Major (${majorVersion}): ${deployMajor}`); + + if (!shouldDeploy) { + core.setFailed("Skipping deployment (pre-release version)"); + } + + - uses: actions/download-artifact@v7 + with: + name: pages + path: new-docs + + - uses: aws-actions/configure-aws-credentials@v5.1.1 + with: + role-to-assume: arn:aws:iam::974483672234:role/simpl-element-release + role-session-name: element-release-docs + aws-region: eu-west-1 + + - run: | + MAJOR_VERSION="${{ steps.deploy.outputs.major_version }}" + + echo "Updating /$MAJOR_VERSION/..." + mkdir "deploy-site" + mv new-docs "deploy-site/$MAJOR_VERSION" + + - run: | + # Generate versions.json from S3 directory listing (no download needed) + echo "Generating versions.json from S3 directory listing..." + + # Collect all versions from S3 + ALL_VERSIONS=() + + # List all existing version-specific folders from S3 (v1, v2, v48, etc.) + # aws s3 ls lists directories with trailing slashes, e.g., "PRE v1/" + aws s3 ls s3://${{ env.VERSIONED_BUCKET_NAME }}/ | grep "PRE v" | awk '{print $2}' | sed 's/\/$//' > s3-versions.txt || true + + # Add the currently deploying version to ensure it's included + MAJOR_VERSION="${{ steps.deploy.outputs.major_version }}" + echo "$MAJOR_VERSION" >> s3-versions.txt + + # Read all versions and remove duplicates + while IFS= read -r version_name; do + if [[ -n "$version_name" ]]; then + ALL_VERSIONS+=("$version_name") + fi + done < s3-versions.txt + + # Remove duplicates and sort versions in descending order + SORTED_VERSIONS=($(printf '%s\n' "${ALL_VERSIONS[@]}" | sort -u -t 'v' -k 2 -n -r)) + + # Find the highest version for "Latest" + LATEST_VERSION="${SORTED_VERSIONS[0]}" + echo "Latest version: $LATEST_VERSION" + + # Build versions.json with correct order: + # 1. Latest (empty version string) + # 2. All versions in descending order (v48, v18, v17, etc.) + # 3. Preview (redirect to element.siemens.io) + + VERSIONS='[]' + + # Add Latest first (empty version string points to root) + latest_num=$(echo "$LATEST_VERSION" | sed 's/^v//') + echo "Adding: Latest (${latest_num}.x)" + VERSIONS=$(echo "$VERSIONS" | jq '. += [{"version": "", "title": "Latest"}]') + + # Add all versions in descending order + for version_name in "${SORTED_VERSIONS[@]}"; do + version_num=$(echo "$version_name" | sed 's/^v//') + echo "Adding: $version_name (${version_num}.x)" + VERSIONS=$(echo "$VERSIONS" | jq --arg version "$version_name" --arg title "${version_num}.x" '. += [{"version": $version, "title": $title}]') + done + + # Add Preview last + echo "Adding: Preview (redirect to https://element.siemens.io)" + VERSIONS=$(echo "$VERSIONS" | jq '. += [{"version": "https://element.siemens.io", "title": "Preview"}]') + + # Write to deploy-site/versions.json + echo "$VERSIONS" | jq '.' > deploy-site/versions.json + + echo "Generated versions.json:" + cat deploy-site/versions.json + + - run: | + SITE_URL="https://element.siemens.io/" + + # Update canonical URLs to point to versioned URLs instead of root + # This ensures search engines index the correct versioned documentation (only one version) + MAJOR_VERSION="${{ steps.deploy.outputs.major_version }}" + VERSION_URL="${SITE_URL}${MAJOR_VERSION}/" + + find "deploy-site/$MAJOR_VERSION" -name "*.html" -type f -exec sed -i \ + -e "s| v.version === currentVersion) || versions[0]; + const visibleVersions = versions.filter(v => !v.hidden); + + const html = `
`; + + return html; + } + + /** + * Initialize version selector + */ + function initVersionSelector() { + const baseURL = getBaseURL(); + const versionsURL = `${baseURL}/versions.json`; + + fetch(versionsURL) + .then(response => { + if (!response.ok) { + if (response.status === 404) { + return null; + } + throw new Error(`Failed to fetch versions.json: ${response.status}`); + } + return response.json(); + }) + .then(versions => { + if (!versions) { + return; + } + + if (!Array.isArray(versions) || versions.length === 0) { + console.warn('[Version Selector] versions.json is empty or invalid'); + return; + } + + // Get current version after we have the versions list + const currentVersion = getCurrentVersion(versions); + + // Find the .md-header element + const header = document.querySelector('.md-header'); + if (!header) { + console.warn('[Version Selector] .md-header element not found'); + return; + } + + // Create .md-header__topic wrapper with version selector inside + const html = renderVersionSelector(versions, currentVersion); + const topicWrapper = document.createElement('div'); + topicWrapper.className = 'md-header__topic'; + topicWrapper.innerHTML = html; + + // Append to .md-header + header.appendChild(topicWrapper); + }) + .catch(error => { + console.error('[Version Selector] Failed to load:', error.message); + }); + } + + // Initialize when DOM is ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initVersionSelector); + } else { + initVersionSelector(); + } +})(); diff --git a/mkdocs.yml b/mkdocs.yml index dacb61308..1c54350a9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -225,6 +225,7 @@ extra_javascript: - '//w3.siemens.com/ote/ote_config.js' - '//w3.siemens.com/ote/sinet/ote.js' - 'https://assets.adobedtm.com/5dfc7d97c6fb/f16b45bec907/launch-af252bb19983.min.js' + - '_src/version-selector.js' extra: links: - name: 'GitHub' diff --git a/pyproject.toml b/pyproject.toml index 6af26dd81..95395dca0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ license = "MIT" requires-python = ">=3.11, <4" readme = "README.md" dependencies = [ - "mkdocs-code-siemens-code-docs-theme>=7.7.0,<8", + "mkdocs-code-siemens-code-docs-theme>=7.8.0,<8", "mkdocs-minify-html-plugin==0.3.9", "mkdocs-element-docs-builder", ] diff --git a/uv.lock b/uv.lock index 7241d79fc..7a81a8e5b 100644 --- a/uv.lock +++ b/uv.lock @@ -120,7 +120,7 @@ dependencies = [ [package.metadata] requires-dist = [ - { name = "mkdocs-code-siemens-code-docs-theme", specifier = ">=7.7.0,<8", index = "https://code.siemens.com/api/v4/groups/3259/-/packages/pypi/simple" }, + { name = "mkdocs-code-siemens-code-docs-theme", specifier = ">=7.8.0,<8", index = "https://code.siemens.com/api/v4/groups/3259/-/packages/pypi/simple" }, { name = "mkdocs-element-docs-builder", editable = "projects/element-docs-builder" }, { name = "mkdocs-minify-html-plugin", specifier = "==0.3.9" }, ] @@ -259,15 +259,15 @@ wheels = [ [[package]] name = "mkdocs-code-siemens-code-docs-theme" -version = "7.7.0" +version = "7.8.0" source = { registry = "https://code.siemens.com/api/v4/groups/3259/-/packages/pypi/simple" } dependencies = [ { name = "mkdocs" }, { name = "mkdocs-material" }, ] -sdist = { url = "https://code.siemens.com/api/v4/groups/3259/-/packages/pypi/files/5466e241cd053e818300f964f74e70764f988156ba6d0eb042364be23828728b/mkdocs_code_siemens_code_docs_theme-7.7.0.tar.gz", hash = "sha256:5466e241cd053e818300f964f74e70764f988156ba6d0eb042364be23828728b" } +sdist = { url = "https://code.siemens.com/api/v4/groups/3259/-/packages/pypi/files/e3b7946b001b72cb17cc129c7af2ad748812d34a200ef51265a6253fcbc30648/mkdocs_code_siemens_code_docs_theme-7.8.0.tar.gz", hash = "sha256:e3b7946b001b72cb17cc129c7af2ad748812d34a200ef51265a6253fcbc30648" } wheels = [ - { url = "https://code.siemens.com/api/v4/groups/3259/-/packages/pypi/files/68573928ab9d3c5bf14ca954481629c9512c9c837d4f3cdc15fa72eb54f47bb7/mkdocs_code_siemens_code_docs_theme-7.7.0-py3-none-any.whl", hash = "sha256:68573928ab9d3c5bf14ca954481629c9512c9c837d4f3cdc15fa72eb54f47bb7" }, + { url = "https://code.siemens.com/api/v4/groups/3259/-/packages/pypi/files/035a290de19b47fb31e9a91cc9c88083ce7baadde41f1a57b2237b8f59f080e3/mkdocs_code_siemens_code_docs_theme-7.8.0-py3-none-any.whl", hash = "sha256:035a290de19b47fb31e9a91cc9c88083ce7baadde41f1a57b2237b8f59f080e3" }, ] [[package]] @@ -297,7 +297,7 @@ wheels = [ [[package]] name = "mkdocs-material" -version = "9.6.16" +version = "9.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, @@ -312,9 +312,9 @@ dependencies = [ { name = "pymdown-extensions" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dd/84/aec27a468c5e8c27689c71b516fb5a0d10b8fca45b9ad2dd9d6e43bc4296/mkdocs_material-9.6.16.tar.gz", hash = "sha256:d07011df4a5c02ee0877496d9f1bfc986cfb93d964799b032dd99fe34c0e9d19", size = 4028828, upload-time = "2025-07-26T15:53:47.542Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/3b/111b84cd6ff28d9e955b5f799ef217a17bc1684ac346af333e6100e413cb/mkdocs_material-9.7.0.tar.gz", hash = "sha256:602b359844e906ee402b7ed9640340cf8a474420d02d8891451733b6b02314ec", size = 4094546, upload-time = "2025-11-11T08:49:09.73Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/65/f4/90ad67125b4dd66e7884e4dbdfab82e3679eb92b751116f8bb25ccfe2f0c/mkdocs_material-9.6.16-py3-none-any.whl", hash = "sha256:8d1a1282b892fe1fdf77bfeb08c485ba3909dd743c9ba69a19a40f637c6ec18c", size = 9223743, upload-time = "2025-07-26T15:53:44.236Z" }, + { url = "https://files.pythonhosted.org/packages/04/87/eefe8d5e764f4cf50ed91b943f8e8f96b5efd65489d8303b7a36e2e79834/mkdocs_material-9.7.0-py3-none-any.whl", hash = "sha256:da2866ea53601125ff5baa8aa06404c6e07af3c5ce3d5de95e3b52b80b442887", size = 9283770, upload-time = "2025-11-11T08:49:06.26Z" }, ] [[package]]