Skip to content

feat: add multi-version build script for OCM website #553

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Aug 7, 2025
Merged
Show file tree
Hide file tree
Changes from 8 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
215 changes: 215 additions & 0 deletions .github/scripts/build-multi-version.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
#!/usr/bin/env bash
set -euo pipefail

# -----------------------------------------------------------------------------
# Multi-Version Build Script for OCM Website
# -----------------------------------------------------------------------------
# Builds all website versions in parallel using git worktrees.
# For performance, it reuses the central node_modules folder via symlink if the
# package-lock.json is identical. If dependencies differ, node_modules is installed
# separately in the worktree using npm ci.
#
# USAGE:
# bash .github/scripts/build-multi-version.sh [baseURL]
#
# baseURL: Optional. The base URL for the built site versions. Default is
# "https://ocm.software". For local testing, you can use e.g.
# "http://localhost:1313".
#
# EXAMPLES:
# bash .github/scripts/build-multi-version.sh
# bash .github/scripts/build-multi-version.sh http://localhost:1313
# -----------------------------------------------------------------------------

# Read baseURL from first argument, default to https://ocm.software
BASE_URL="${1:-https://ocm.software}"

# Helper for error output
err() { echo "[ERROR] $*" >&2; }
# Helper for info output
info() { echo "[INFO] $*"; }

# Check required commands
for cmd in git npm jq cmp; do
if ! command -v "$cmd" &>/dev/null; then
err "$cmd is required but not installed."
exit 1
fi
done

# --- Determine the correct source for data/versions.json ---
# By default, use the local file in the current branch
VERSIONS_JSON_PATH="data/versions.json"

# Get docsVersion and current branch only once for later use
DOCS_VERSION=$(grep -E '^[[:space:]]*docsVersion[[:space:]]*=' config/_default/params.toml | cut -d'=' -f2 | tr -d ' "')
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)

# --- Check for version mismatch between branch and docsVersion ---
# Determine expected docsVersion based on current branch
if [ "$CURRENT_BRANCH" = "main" ]; then
EXPECTED_DOCSVERSION="dev"
elif [[ "$CURRENT_BRANCH" =~ ^website/v[0-9]+\.[0-9]+$ ]]; then
EXPECTED_DOCSVERSION="${CURRENT_BRANCH#website/}"
else
# For other branches (e.g., feature branches), skip validation
EXPECTED_DOCSVERSION=""
fi

# Validate docsVersion matches expected value (if we have an expectation)
if [ -n "$EXPECTED_DOCSVERSION" ] && [ "$DOCS_VERSION" != "$EXPECTED_DOCSVERSION" ]; then
err "docsVersion ('$DOCS_VERSION') does not match expected value ('$EXPECTED_DOCSVERSION') for branch '$CURRENT_BRANCH'"
err "Please update docsVersion in config/_default/params.toml to '$EXPECTED_DOCSVERSION'"
exit 1
fi

# Determine the upstream branch for version resolution
# If docsVersion is "dev", upstream is "main"; otherwise, it's "website/<docsVersion>"
if [ "$DOCS_VERSION" = "dev" ]; then
UPSTREAM_BRANCH="main"
else
UPSTREAM_BRANCH="website/$DOCS_VERSION"
fi

# Decide which data/versions.json to use:
# - If on main, or a PR branch for main (upstream is origin/main), use the local file
# - Otherwise, fetch the file from origin/main and use it temporarily
ACTUAL_UPSTREAM=$(git rev-parse --symbolic-full-name --abbrev-ref "$CURRENT_BRANCH@{upstream}" 2>/dev/null || echo "")
if [ "$CURRENT_BRANCH" = "main" ] || [ "$ACTUAL_UPSTREAM" = "origin/main" ]; then
info "Using local data/versions.json from current branch ($CURRENT_BRANCH)"
else
# Create a temporary directory and fetch the latest data/versions.json from origin/main
TMP_MAIN_VERSIONS=".tmp-main-versions"
rm -rf "$TMP_MAIN_VERSIONS"
mkdir -p "$TMP_MAIN_VERSIONS"
git show origin/main:data/versions.json > "$TMP_MAIN_VERSIONS/versions.json" || { err "Could not fetch data/versions.json from main"; exit 1; }
VERSIONS_JSON_PATH="$TMP_MAIN_VERSIONS/versions.json"
info "Using data/versions.json from main (temporary) for version resolution."
fi

# Read all available versions from the determined data/versions.json file
VERSIONS=$(jq -r '.versions[]' "$VERSIONS_JSON_PATH")
if [ -z "$VERSIONS" ]; then
err "No versions found in $VERSIONS_JSON_PATH."
exit 1
fi

# Read the default version from config/_default/params.toml
DEFAULT_VERSION=$(grep -E '^[[:space:]]*defaultVersion' config/_default/params.toml | cut -d'=' -f2 | tr -d ' "')
if [ -z "$DEFAULT_VERSION" ]; then
err "defaultVersion not found in config/_default/params.toml."
exit 1
fi

# Check if the default version exists in the versions.json
if ! echo "$VERSIONS" | grep -q "^$DEFAULT_VERSION$"; then
err "defaultVersion '$DEFAULT_VERSION' not found in versions.json"
err "Available versions: $(echo "$VERSIONS" | tr '\n' ' ')"
exit 1
fi

git worktree prune # Clean up any stale worktrees

# Prepare output and worktree directories
# Remove and recreate the public directory for fresh build output
PUBLIC_DIR="public"
rm -rf "$PUBLIC_DIR"
mkdir -p "$PUBLIC_DIR"

# Remove and recreate the worktree base directory for temporary git worktrees
WORKTREE_BASE=".worktrees"
rm -rf "$WORKTREE_BASE"
mkdir -p "$WORKTREE_BASE"

# Prune any stale git worktrees before starting the build
git worktree prune


# Build each version listed in versions.json
# BUILT_VERSIONS will hold the mapping: version -> output directory
declare -A BUILT_VERSIONS
for VERSION in $VERSIONS; do
# Determine output directory, branch and final base URL for each version
if [ "$VERSION" = "dev" ]; then
OUTDIR="$PUBLIC_DIR/dev"
BRANCH="main"
FINAL_BASE_URL="$BASE_URL/dev"
elif [ "$VERSION" = "$DEFAULT_VERSION" ]; then
OUTDIR="$PUBLIC_DIR"
BRANCH="website/$VERSION"
FINAL_BASE_URL="$BASE_URL" # no /version suffix for default version!
else
OUTDIR="$PUBLIC_DIR/$VERSION"
BRANCH="website/$VERSION"
FINAL_BASE_URL="$BASE_URL/$VERSION"
fi

# If the current branch matches docsVersion, build directly from the current branch (no worktree needed)
if [ "$VERSION" = "$DOCS_VERSION" ]; then
info "Building $VERSION version directly from current branch ($CURRENT_BRANCH) into $OUTDIR"
# Make npm clean install (using the dependency lock file)
npm ci || { err "npm ci failed for $CURRENT_BRANCH"; exit 1; }
# Always update Hugo modules and install dependencies before building
npm run hugo -- mod get -u || { err "hugo mod get -u failed for $CURRENT_BRANCH"; exit 1; }
npm run hugo -- mod tidy || { err "hugo mod tidy failed for $CURRENT_BRANCH"; exit 1; }
# Execute Hugo build with the final base URL
npm run build -- --destination "$OUTDIR" --baseURL "$FINAL_BASE_URL" || { err "npm run build failed for $CURRENT_BRANCH"; exit 1; }
BUILT_VERSIONS["$VERSION"]="$OUTDIR"
continue
fi

# Otherwise, build from the corresponding branch using a git worktree
# Ensure the branch exists locally; if not, try to fetch it from origin
if ! git rev-parse --verify "$BRANCH" >/dev/null 2>&1; then
info "Branch $BRANCH not found locally, attempting to fetch from origin."
git fetch origin $BRANCH:$BRANCH || {
err "Branch '$BRANCH' for version '$VERSION' does not exist (neither local nor remote)";
err "Please create the branch or remove '$VERSION' from versions.json";
exit 1;
}
fi

info "Building version $VERSION from branch $BRANCH into $OUTDIR"
git worktree add "$WORKTREE_BASE/$VERSION" "$BRANCH" || { err "Failed to add worktree for $BRANCH"; exit 1; }
pushd "$WORKTREE_BASE/$VERSION" >/dev/null

# Copy the latest data/versions.json into the worktree for the version switcher (except for current branch)
if [ "$VERSION" != "$DOCS_VERSION" ]; then
cp "../../$VERSIONS_JSON_PATH" data/versions.json
info "Copied latest data/versions.json into worktree for $VERSION."
fi

# Optimization: reuse central node_modules if package-lock.json is identical
# This saves time and disk space for identical dependencies across versions
if cmp -s package-lock.json ../../package-lock.json; then
info "package-lock.json is identical, creating symlink to central node_modules."
ln -s ../../node_modules ./node_modules
else
# Make npm clean install (using the dependency lock file)
info "package-lock.json differs from central version. Installing separate dependencies for this version."
npm ci || { err "npm ci failed for $BRANCH"; popd >/dev/null; exit 1; }
fi

# Always update Hugo modules before building
npm run hugo -- mod get -u || { err "hugo mod get -u failed for $BRANCH"; popd >/dev/null; exit 1; }
npm run hugo -- mod tidy || { err "hugo mod tidy failed for $BRANCH"; popd >/dev/null; exit 1; }

# Build the site for this version using the final base URL
npm run build -- --destination "../../$OUTDIR" --baseURL "$FINAL_BASE_URL" || { err "npm run build failed for $BRANCH"; popd >/dev/null; exit 1; }
BUILT_VERSIONS["$VERSION"]="$OUTDIR"

# Clean up and remove the worktree after building
popd >/dev/null
git worktree remove "$WORKTREE_BASE/$VERSION" --force
done

# Print a summary of all built versions and their output directories
info "Multi-version build completed. Output in folder $PUBLIC_DIR/."
echo "--- Build Summary ---"
for VERSION in "${!BUILT_VERSIONS[@]}"; do
printf "Version: %-10s → %s\n" "$VERSION" "${BUILT_VERSIONS[$VERSION]}"
done

# Cleanup the worktrees and .tmpdirectory after all builds are complete
rm -rf "$WORKTREE_BASE"
rm -rf "$TMP_MAIN_VERSIONS"
18 changes: 8 additions & 10 deletions .github/workflows/publish-site.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,25 @@ jobs:
# Instead of creating the PR below with GITHUB_TOKEN use a generated
# short-lived App installation token with full REST rights
id: generate_token
uses: tibdex/github-app-token@v2
uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a #v2
with:
app_id: ${{ secrets.OCMBOT_APP_ID }}
private_key: ${{ secrets.OCMBOT_PRIV_KEY }}

- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #v4
with:
node-version: 22.12.0 # keep in sync with 'engines'@package.json

- name: Install Dependencies
run: npm install

- name: Build Site
run: npm run build
- name: Build Multi-Version Site
# Using script .github/workflows/scripts/build-multi-version.sh
run: npm run build-multi-version

- name: Setup Python
uses: actions/setup-python@v5
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 #v5
with:
python-version: '3.11'

Expand All @@ -57,7 +55,7 @@ jobs:
cp schema-v2.html schema_doc.css schema_doc.min.js public/docs/overview/specification

- name: Publish as GitHub Pages
uses: peaceiris/actions-gh-pages@v4
uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e #v4
with:
github_token: ${{ steps.generate_token.outputs.token }}
publish_dir: ./public
Expand Down
2 changes: 2 additions & 0 deletions netlify-multi-version-build-wrapper.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/bash
exec .github/scripts/build-multi-version.sh "$@"
48 changes: 24 additions & 24 deletions netlify.toml
Original file line number Diff line number Diff line change
@@ -1,25 +1,12 @@
[build]
publish = "public"
functions = "functions"

[build.environment]
NODE_VERSION = "23.11.0"
NODE_VERSION = "22.12.0"
NPM_VERSION = "10.9.2"

[context.production]
command = "npm run build"
[build]
publish = "public"

[context.deploy-preview]
command = "npm run build -- -b $DEPLOY_PRIME_URL"

[context.branch-deploy]
command = "npm run build -- -b $DEPLOY_PRIME_URL"

[context.next]
command = "npm run build"

[context.next.environment]
HUGO_ENV = "next"
command = "bash .github/scripts/build-multi-version.sh $DEPLOY_PRIME_URL"

[[plugins]]
package = "netlify-plugin-submit-sitemap"
Expand All @@ -33,10 +20,23 @@
"yandex"
]

[dev]
framework = "#custom"
command = "npm run start"
targetPort = 1313
port = 8888
publish = "public"
autoLaunch = false
# Redirects for versioned paths
[[redirects]]
from = "/dev/*"
to = "/dev/:splat"
status = 200

[[redirects]]
from = "/current/*"
to = "/current/:splat"
status = 200

[[redirects]]
from = "/v*/*"
to = "/v:version/:splat"
status = 200

[[redirects]]
from = "/*"
to = "/:splat"
status = 200
30 changes: 18 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,31 @@
"description": "OCM website",
"author": "open-component-model",
"license": "Apache-2.0",

"scripts": {
"dev": "exec-bin node_modules/.bin/hugo/hugo server --bind=0.0.0.0 --disableFastRender --baseURL=http://localhost --noHTTPCache",
"dev:drafts": "exec-bin node_modules/.bin/hugo/hugo server --bind=0.0.0.0 --disableFastRender --baseURL=http://localhost --noHTTPCache --buildDrafts",
"create": "exec-bin node_modules/.bin/hugo/hugo new",
"lint": "npm run lint:scripts && npm run lint:styles && npm run lint:markdown",
"lint:scripts": "eslint --cache assets/js",
"lint:styles": "stylelint --cache \"assets/scss/**/*.{css,sass,scss}\"",
"lint:markdown": "markdownlint-cli2 \"*.md\" \"content/**/*.md\"",
"test": "echo \"Error: no test specified\" && exit 1",
"build-multi-version": "bash .github/scripts/build-multi-version.sh",
"build": "exec-bin node_modules/.bin/hugo/hugo --minify --gc",
"preview": "http-server --gzip --brotli --ext=html --cors",
"clean": "npm run clean:build && npm run clean:lint && npm run clean:install",
"clean:build": "shx rm -rf public resources .hugo_build.lock",
"clean:install": "shx rm -rf node_modules package-lock.json yarn.lock pnpm-lock.yaml",
"clean:lint": "shx rm -rf .eslintcache .stylelintcache",
"preinfo": "npm version",
"clean": "npm run clean:build && npm run clean:lint && npm run clean:install",
"create": "exec-bin node_modules/.bin/hugo/hugo new",
"dev:drafts": "exec-bin node_modules/.bin/hugo/hugo server --bind=0.0.0.0 --disableFastRender --baseURL=http://localhost --noHTTPCache --buildDrafts",
"dev": "exec-bin node_modules/.bin/hugo/hugo server --bind=0.0.0.0 --disableFastRender --baseURL=http://localhost --noHTTPCache",
"hugo": "exec-bin node_modules/.bin/hugo/hugo",
"info": "npm list",
"lint:markdown": "markdownlint-cli2 \"*.md\" \"content/**/*.md\"",
"lint:scripts": "eslint --cache assets/js",
"lint:styles": "stylelint --cache \"assets/scss/**/*.{css,sass,scss}\"",
"lint": "npm run lint:scripts && npm run lint:styles && npm run lint:markdown",
"mod:clean": "npm run hugo -- mod clean",
"mod:tidy": "npm run hugo -- mod tidy",
"mod:get": "npm run hugo -- mod get",
"postinfo": "exec-bin node_modules/.bin/hugo/hugo version",
"postinstall": "hugo-installer --version otherDependencies.hugo --extended --destination node_modules/.bin/hugo"
"postinstall": "hugo-installer --version otherDependencies.hugo --extended --destination node_modules/.bin/hugo",
"preinfo": "npm version",
"preview": "http-server --gzip --brotli --ext=html --cors",
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"@tabler/icons": "^3.34.0",
Expand Down