Skip to content
Open
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
153 changes: 153 additions & 0 deletions .github/workflows/release.yaml
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you added too much logic into the actions. The script cannot be tested and debugging is not really possible, Please extract the logic into scripts. We should be able to run, debug and test the scripts from our local computers, if we need to.

Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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' || '' }}
Expand All @@ -38,3 +47,147 @@ 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: |
BEFORE_TAG="${{ steps.before_release.outputs.before_tag }}"
AFTER_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")

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

# Generates the versioned documentation and publishes it to S3.
# This job only runs after a successful release unless the release is on the "next" branch.
# In general, runs on main push to /latest/, while release branches (e.g., release/48.x) push to /v48/
# There are two special cases:
# 1. Release on a release branch while there is no new major release on main.
# This happens, when we already merged breaking changes, but do a release for an older version before releasing from main.
# In this case, we still want to update the /latest/ although we are on a release branch.
# 2. The first release of a new major version.
# In this case, we need to move the existing /latest/ to the previous major version folder.
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
- uses: actions/download-artifact@v7
with:
name: pages
path: pages
- 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
# Prepare gathers the necessary information:
# - the version / major version we are releasing
# - to which directory we need to deploy (latest or v123)
# - latest-version.txt contains the current version of the "latest" object in the S3 bucket
# If we create a new major version, we also move the existing latest/ to the previous major version folder.
# This outputs three values:
# - major_version: the major version we are releasing (e.g., v49)
# - deploy_latest: whether we need to deploy to latest/ (true/false)
# - latest: the current version of latest/ before this release (e.g., v48)
- id: prepare
run: |
DEPLOY_RELEASE="${{ needs.publish.outputs.release_tag }}"
VERSION="${DEPLOY_RELEASE#v}"
MAJOR_VERSION="v${VERSION%%.*}"

echo "major_version=$MAJOR_VERSION" >> "$GITHUB_OUTPUT"

aws s3 cp "s3://${{ env.VERSIONED_BUCKET_NAME }}/latest-version.txt" latest-version.txt || true

LATEST_VERSION=""
if [[ -f latest-version.txt ]]; then
LATEST_VERSION=$(tr -d '\r\n' < latest-version.txt)
fi

DEPLOY_LATEST="false"

# if [[ "${{ github.ref_name }}" == "${{ github.event.repository.default_branch }}" ]]; then
if [[ "a" == "a" ]]; then
DEPLOY_LATEST="true"
if [[ -n "$LATEST_VERSION" && "$LATEST_VERSION" != "$MAJOR_VERSION" ]]; then
aws s3 sync --quiet --no-progress --delete \
"s3://${{ env.VERSIONED_BUCKET_NAME }}/latest/" \
"s3://${{ env.VERSIONED_BUCKET_NAME }}/$LATEST_VERSION/"
fi
elif [[ "$LATEST_VERSION" == "$MAJOR_VERSION" ]]; then
DEPLOY_LATEST="true"
fi

echo "deploy_latest=$DEPLOY_LATEST" >> "$GITHUB_OUTPUT"
echo "latest=$LATEST_VERSION" >> "$GITHUB_OUTPUT"

aws s3 ls s3://${{ env.VERSIONED_BUCKET_NAME }}/ | grep "PRE v" | awk '{print $2}' | sed 's/\/$//' | sed 's/^v//' > s3-versions.txt || true

# Generate versions.json based on the existing versions in S3 and the new release
- uses: actions/github-script@v7
name: Generate versions.json
with:
script: |
const fs = require('fs');

const rawVersions = fs
.readFileSync('s3-versions.txt', 'utf8')
.split(/\r?\n/)
.map(line => line.trim())
.filter(Boolean);

const payload = rawVersions
.sort((a, b) => Number(b) - Number(a))
.map(versionName => ({
version: `v${versionName}`,
title: `${versionName}.x`
}));

if (${{ steps.prepare.outputs.deploy_latest == 'true' }}) {
const majorVersion = "${{ steps.prepare.outputs.major_version }}";
const numericTitle = majorVersion.replace(/^v/i, '');
payload.unshift({ version: '', title: `${numericTitle}.x` });
} else {
const latestFallback = "${{ steps.prepare.outputs.latest }}";
const numericTitle = latestFallback.replace(/^v/i, '');
payload.unshift({ version: '', title: `${numericTitle}.x` });
}

fs.writeFileSync('versions.json', JSON.stringify(payload, null, 2));

# Uploads the generated documentation to the appropriate S3 bucket and path.
- 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.prepare.outputs.major_version }}"

if [[ "${{ steps.prepare.outputs.deploy_latest }}" == "true" ]]; then
aws s3 sync --quiet --no-progress --delete "pages/" "s3://${{ env.VERSIONED_BUCKET_NAME }}/latest/"
echo "$MAJOR_VERSION" > latest-version.txt
aws s3 cp latest-version.txt "s3://${{ env.VERSIONED_BUCKET_NAME }}/latest-version.txt"
else
aws s3 sync --quiet --no-progress --delete "pages/" "s3://${{ env.VERSIONED_BUCKET_NAME }}/$MAJOR_VERSION/"
fi

# Upload versions.json with short cache-control for quick updates
if [[ ! -f "deploy-site/versions.json" ]]; then
echo "Error: deploy-site/versions.json file does not exist"
exit 1
fi
aws s3 cp versions.json s3://${{ env.VERSIONED_BUCKET_NAME }}/versions.json
167 changes: 167 additions & 0 deletions docs/_src/version-selector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/**
* Version Selector for Siemens Element Documentation
*
* This script implements a version switcher that:
* - Fetches versions.json from the root of the domain
* - Supports absolute version URLs
* - Preserves current page path when switching versions
* - Gracefully degrades if versions.json is not found (no errors, just no selector)
*
* If versions.json is not available (404), the page loads normally without the version selector.
* No errors are thrown to ensure documentation remains accessible.
*
* Based on MkDocs Material's version selector implementation
* https://github.com/squidfunk/mkdocs-material
*/

(function () {
'use strict';

/**
* Get the base URL of the site (root domain)
*/
function getBaseURL() {
const location = window.location;
return `${location.protocol}//${location.host}`;
}

/**
* Get current version from URL path
* Only recognizes versions that exist in versions.json
* Returns empty string if at root (no version in path)
*/
function getCurrentVersion(versions) {
const path = window.location.pathname;

// If we don't have versions yet, try to extract from path
if (!versions) {
const match = path.match(/\/([^/]+)\//);
return match ? match[1] : '';
}

// Check if any known version is in the path
for (const version of versions) {
if (version.version && path.includes(`/${version.version}/`)) {
return version.version;
}
}

// No version found in path, assume root-level version
return '';
}

/**
* Get current page path relative to version
*/
function getCurrentPagePath(currentVersion) {
const path = window.location.pathname;

// If no version (root-level), return the full path
if (!currentVersion || currentVersion === '') {
return path.substring(1); // Remove leading slash
}

// Find version in path and get everything after it
const versionIndex = path.indexOf(`/${currentVersion}/`);
if (versionIndex !== -1) {
return path.substring(versionIndex + currentVersion.length + 2);
}

return '';
}

/**
* Build version URL with current page path
* Supports versions at root (empty string), in subdirectories, or absolute URLs
*/
function buildVersionURL(version, currentVersion, preservePath = true) {
// If version is an absolute URL, return as-is
if (
version.startsWith('http://') ||
version.startsWith('https://') ||
version.startsWith('//')
) {
return version;
}

const baseURL = getBaseURL();
const pagePath = preservePath ? getCurrentPagePath(currentVersion) : '';

// If version is empty string or "/", host at root
if (!version || version === '/' || version === '') {
return `${baseURL}/${pagePath}`;
}

return `${baseURL}/${version}/${pagePath}`;
}

/**
* Render version selector HTML
*/
function renderVersionSelector(versions, currentVersion) {
const current = versions.find(v => v.version === currentVersion) || versions[0];
const visibleVersions = versions.filter(v => !v.hidden);

const html = `<div class="md-version"><button class="md-version__current" aria-label="Select version">${current.title}</button><ul class="md-version__list">${visibleVersions.map(version => `<li class="md-version__item"><a href="${buildVersionURL(version.version, currentVersion)}" class="md-version__link">${version.title}</a></li>`).join('')}</ul></div>`;

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();
}
})();
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,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'
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
Expand Down
Loading
Loading