From 620c49745b6290620640987edb2a8f1473c2d825 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Thu, 20 Aug 2026 17:36:54 +1000 Subject: [PATCH 01/12] Fixed the documentation build breaking on merge to main. Absolute '/docs/...' links in the other major's content resolved against the current major once served at '/docs/v{N}'. They are now re-pointed during assembly, and every current-major build assembles so branches are checked against the aggregate site their merge publishes. --- .github/workflows/vortex-release.yml | 17 ++--- .github/workflows/vortex-test-docs.yml | 25 ++++--- .vortex/.ahoy.yml | 4 ++ .../docs/.utils/assemble-versioned-docs.sh | 65 +++++++++++++++++++ .../maintenance/documentation.mdx | 30 +++++++++ .vortex/docs/package.json | 1 + 6 files changed, 117 insertions(+), 25 deletions(-) create mode 100755 .vortex/docs/.utils/assemble-versioned-docs.sh diff --git a/.github/workflows/vortex-release.yml b/.github/workflows/vortex-release.yml index 44438e445b..307979bdbc 100644 --- a/.github/workflows/vortex-release.yml +++ b/.github/workflows/vortex-release.yml @@ -145,7 +145,6 @@ jobs: env: CURRENT_MAJOR: ${{ vars.VORTEX_CURRENT_MAJOR || '1' }} - OTHER_MAJOR: ${{ (vars.VORTEX_CURRENT_MAJOR || '1') == '1' && '2' || '1' }} steps: - name: Checkout code @@ -213,19 +212,13 @@ jobs: run: yarn install --frozen-lockfile working-directory: .vortex/docs - # Assemble the multi-version site for production: snapshot this ref's docs - # as the current major (served at the bare '/docs') and pull the other - # major's '{N}.x' branch docs in at '/docs/v{other}'. All driven by - # 'VORTEX_CURRENT_MAJOR'. Mirrors the same step in 'vortex-test-docs.yml' - # (development / Netlify). + # Production (GitHub Pages) serves the same aggregate site that + # 'vortex-test-docs.yml' builds for development deploys and previews. - name: Assemble versioned docs - run: | - yarn docusaurus docs:version "${CURRENT_MAJOR}.x" - git -C "${{ github.workspace }}" fetch origin "${OTHER_MAJOR}.x" --depth=1 || { echo "Failed to fetch the ${OTHER_MAJOR}.x branch."; exit 1; } - git -C "${{ github.workspace }}" rev-parse --verify "origin/${OTHER_MAJOR}.x" || { echo "The ${OTHER_MAJOR}.x branch does not exist."; exit 1; } - rm -rf content - git -C "${{ github.workspace }}" checkout "origin/${OTHER_MAJOR}.x" -- .vortex/docs/content || { echo "Failed to check out content from ${OTHER_MAJOR}.x."; exit 1; } + run: ./.utils/assemble-versioned-docs.sh working-directory: .vortex/docs + env: + VORTEX_CURRENT_MAJOR: ${{ env.CURRENT_MAJOR }} - name: Build documentation site run: yarn run build diff --git a/.github/workflows/vortex-test-docs.yml b/.github/workflows/vortex-test-docs.yml index 16af91e870..3ab74e4d7c 100644 --- a/.github/workflows/vortex-test-docs.yml +++ b/.github/workflows/vortex-test-docs.yml @@ -25,6 +25,10 @@ jobs: env: CURRENT_MAJOR: ${{ vars.VORTEX_CURRENT_MAJOR || '1' }} OTHER_MAJOR: ${{ (vars.VORTEX_CURRENT_MAJOR || '1') == '1' && '2' || '1' }} + # The major the triggering branch belongs to. Each major ships its own + # binary from its own workflow, so the workflow name identifies the major + # for pushes, branches and fork pull requests alike. + BUILD_MAJOR: ${{ github.event.workflow_run.name == 'Vortex - Test CLI' && '2' || '1' }} BINARY_ARTIFACT: ${{ github.event.workflow_run.name == 'Vortex - Test CLI' && 'vortex-cli' || 'vortex-installer' }} BINARY_FILE: ${{ github.event.workflow_run.name == 'Vortex - Test CLI' && 'vortex.phar' || 'installer.phar' }} @@ -99,21 +103,16 @@ jobs: run: yarn run spellcheck working-directory: '${{ github.workspace }}/.vortex/docs' - # On the main (development) deploy, assemble the multi-version site: - # snapshot main's docs as the current major (served at the bare '/docs') - # and pull the other major's '{N}.x' branch docs in at '/docs/v{other}'. - # Driven by 'VORTEX_CURRENT_MAJOR'. Skipped on PR/branch builds, which stay - # single-version previews. Mirrors the same step in 'vortex-release.yml' - # (production / GitHub Pages). + # Every current-major build assembles, so a branch is checked against the + # same aggregate site its merge publishes. Other-major branches stay + # single-version previews: their docs reach production through the + # current-major branch, which builds them as '/docs/v{other}'. - name: Assemble versioned docs - if: github.event.workflow_run.head_branch == 'main' - run: | - yarn docusaurus docs:version "${CURRENT_MAJOR}.x" - git -C "${{ github.workspace }}" fetch origin "${OTHER_MAJOR}.x" --depth=1 || { echo "Failed to fetch the ${OTHER_MAJOR}.x branch."; exit 1; } - git -C "${{ github.workspace }}" rev-parse --verify "origin/${OTHER_MAJOR}.x" || { echo "The ${OTHER_MAJOR}.x branch does not exist."; exit 1; } - rm -rf content - git -C "${{ github.workspace }}" checkout "origin/${OTHER_MAJOR}.x" -- .vortex/docs/content || { echo "Failed to check out content from ${OTHER_MAJOR}.x."; exit 1; } + if: env.BUILD_MAJOR == env.CURRENT_MAJOR + run: ./.utils/assemble-versioned-docs.sh working-directory: '${{ github.workspace }}/.vortex/docs' + env: + VORTEX_CURRENT_MAJOR: ${{ env.CURRENT_MAJOR }} # On the main deploy, publish both majors to match the multi-version # docs: '/v1/install' and '/v2/install' are the stable per-major pins and diff --git a/.vortex/.ahoy.yml b/.vortex/.ahoy.yml index 366989a047..1560697da0 100644 --- a/.vortex/.ahoy.yml +++ b/.vortex/.ahoy.yml @@ -137,6 +137,10 @@ commands: usage: Update the documentation. cmd: yarn --cwd=docs run update-variables + assemble-docs: + usage: Assemble the multi-version documentation site. Replaces 'docs/content' with the other major's documentation. + cmd: yarn --cwd=docs run assemble-versions + update-videos: usage: Update documentation videos. Pass names to record a subset (installer, build, provision, lint, test, test-bdd); default is all six. cmd: php docs/.utils/update-videos.php "$@" diff --git a/.vortex/docs/.utils/assemble-versioned-docs.sh b/.vortex/docs/.utils/assemble-versioned-docs.sh new file mode 100755 index 0000000000..61bfa18659 --- /dev/null +++ b/.vortex/docs/.utils/assemble-versioned-docs.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +## +# Assemble the multi-version documentation site. +# +# Snapshots this branch's 'content/' as the current major's version and replaces +# 'content/' with the other major's docs from its '{N}.x' branch. Docusaurus +# serves the snapshot at the bare '/docs' and 'content/' at '/docs/v{other}'. +# +# @usage +# cd .vortex/docs && ./.utils/assemble-versioned-docs.sh + +set -eu +set -o pipefail +[ "${VORTEX_DEBUG-}" = "1" ] && set -x + +# The major this branch ships. Its docs become the site's default version. +VORTEX_CURRENT_MAJOR="${VORTEX_CURRENT_MAJOR:-1}" + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../" && pwd)" + +sed_opts=(-i) && [ "$(uname)" = "Darwin" ] && sed_opts=(-i '') + +case "${VORTEX_CURRENT_MAJOR}" in + 1) other_major=2 ;; + 2) other_major=1 ;; + *) + echo "ERROR: Invalid VORTEX_CURRENT_MAJOR='${VORTEX_CURRENT_MAJOR}'. Expected 1 or 2." >&2 + exit 1 + ;; +esac + +yarn docusaurus docs:version "${VORTEX_CURRENT_MAJOR}.x" + +git -C "${ROOT_DIR}" fetch origin "${other_major}.x" --depth=1 || { + echo "ERROR: Failed to fetch the ${other_major}.x branch." >&2 + exit 1 +} + +git -C "${ROOT_DIR}" rev-parse --verify "origin/${other_major}.x" >/dev/null || { + echo "ERROR: The ${other_major}.x branch does not exist." >&2 + exit 1 +} + +rm -rf content +mkdir -p content + +# Extracted through an archive rather than checked out, so a local run leaves +# the index untouched and 'git restore content' returns to the branch. +git -C "${ROOT_DIR}" archive "origin/${other_major}.x:.vortex/docs/content" | tar -x -C content || { + echo "ERROR: Failed to extract content from ${other_major}.x." >&2 + exit 1 +} + +[ -n "$(ls -A content)" ] || { + echo "ERROR: The ${other_major}.x branch carries no documentation content." >&2 + exit 1 +} + +# Every branch authors its docs against the bare '/docs' mount, so an absolute +# link lands on the current major once the content is served at '/docs/v{other}'. +# Re-point those links at the major they were written for. A link that already +# names a version is left as authored, so the alternation skips '/v' followed by +# a digit. +find content -type f \( -name '*.md' -o -name '*.mdx' \) -exec \ + sed -E "${sed_opts[@]}" "s%\]\(/docs([)#?]|/[^v]|/v[^0-9])%](/docs/v${other_major}\1%g" {} + diff --git a/.vortex/docs/content/contributing/maintenance/documentation.mdx b/.vortex/docs/content/contributing/maintenance/documentation.mdx index b746e5ebbb..184ccb2f76 100644 --- a/.vortex/docs/content/contributing/maintenance/documentation.mdx +++ b/.vortex/docs/content/contributing/maintenance/documentation.mdx @@ -75,6 +75,36 @@ The 6 terminal demo videos embedded in the docs are regenerated with `ahoy update-videos [names]` from `.vortex/` - see [Installer > Installer video](installer.mdx#installer-video) for the pipeline. +### Multi-version site + +The published site serves two majors at once. The branch that ships the current +major - selected by the `VORTEX_CURRENT_MAJOR` repository variable, default `1` - +is snapshotted as the default version at `/docs`, and the other major's `{N}.x` +branch content is served at `/docs/v{N}`. + +Each branch writes its content against the bare `/docs` mount, so an absolute +link such as `/docs/tools/behat` points at the current major. When that content +is assembled under `/docs/v{N}`, those links are re-pointed at the major they +were written for, and a link that already names a version is left alone. Write +links the way they read on their own branch. + +To assemble and preview the aggregate site locally: + +```shell +cd .vortex + +# Fetch the other major's documentation into 'docs/content'. +ahoy assemble-docs + +# Build both versions. The build fails on broken internal links. +ahoy build-docs +``` + +Assembling replaces `docs/content` with the other major's documentation. +Restore the branch with `git restore .vortex/docs/content` and delete +`versioned_docs/`, `versioned_sidebars/` and `versions.json` to return to a +single-version build. + ### Publishing Automated continuous integration builds publish this documentation: diff --git a/.vortex/docs/package.json b/.vortex/docs/package.json index e914afdb49..9470424f9c 100644 --- a/.vortex/docs/package.json +++ b/.vortex/docs/package.json @@ -13,6 +13,7 @@ "write-heading-ids": "docusaurus write-heading-ids", "spellcheck": "cspell \"content/**/*.mdx\"", "update-variables": "./.utils/update-docs.sh", + "assemble-versions": "./.utils/assemble-versioned-docs.sh", "test": "jest", "test:watch": "jest --watch", "test:coverage": "jest --coverage", From 53e740ecb86d90125d860b750db2d8474589bb74 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Fri, 21 Aug 2026 07:46:13 +1000 Subject: [PATCH 02/12] Assembled the versioned docs into a disposable workspace. Assembly staged both majors by replacing the tracked 'content/', leaving a branch dirty after a local run. It now only reads 'content/' and stages into git-ignored directories that the build detects. 'editUrl' is composed from the branch that carries the page, so 'Edit this page' resolves to a committed file on both versions. --- .vortex/docs/.gitignore | 7 +++- .../docs/.utils/assemble-versioned-docs.sh | 38 +++++++++++------ .../maintenance/documentation.mdx | 11 ++--- .vortex/docs/docusaurus.config.js | 41 +++++++++++++------ .vortex/docs/package.json | 2 +- 5 files changed, 66 insertions(+), 33 deletions(-) diff --git a/.vortex/docs/.gitignore b/.vortex/docs/.gitignore index b773ebf90c..5396bc3267 100644 --- a/.vortex/docs/.gitignore +++ b/.vortex/docs/.gitignore @@ -11,8 +11,11 @@ .cache-loader # Assembled docs versions - created by the CI publish step (or a local -# `docusaurus docs:version` to preview the multi-version site) and never -# committed; the build auto-detects `versioned_docs/` to switch to multi-version. +# `ahoy assemble-docs` to preview the multi-version site) and never committed. +# The build auto-detects `versioned_docs/` to switch to multi-version and +# `.docusaurus-versioned/content` to read the other major's documentation, so +# the tracked `content/` is never used as a staging area. +/.docusaurus-versioned /versioned_docs /versioned_sidebars /versions.json diff --git a/.vortex/docs/.utils/assemble-versioned-docs.sh b/.vortex/docs/.utils/assemble-versioned-docs.sh index 61bfa18659..3824fa035a 100755 --- a/.vortex/docs/.utils/assemble-versioned-docs.sh +++ b/.vortex/docs/.utils/assemble-versioned-docs.sh @@ -2,9 +2,13 @@ ## # Assemble the multi-version documentation site. # -# Snapshots this branch's 'content/' as the current major's version and replaces -# 'content/' with the other major's docs from its '{N}.x' branch. Docusaurus -# serves the snapshot at the bare '/docs' and 'content/' at '/docs/v{other}'. +# Snapshots this branch's docs as the current major's version and stages the +# other major's docs from its '{N}.x' branch. Docusaurus serves the snapshot at +# the bare '/docs' and the staged docs at '/docs/v{other}'. +# +# Everything is written to disposable, git-ignored locations: the tracked +# 'content/' is only ever read. Delete 'WORKSPACE_DIR', 'versioned_docs', +# 'versioned_sidebars' and 'versions.json' to return to a single-version build. # # @usage # cd .vortex/docs && ./.utils/assemble-versioned-docs.sh @@ -16,6 +20,10 @@ set -o pipefail # The major this branch ships. Its docs become the site's default version. VORTEX_CURRENT_MAJOR="${VORTEX_CURRENT_MAJOR:-1}" +# Staging area for the docs the build reads. 'docusaurus.config.js' switches to +# it when it exists. +WORKSPACE_DIR=".docusaurus-versioned" + ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../" && pwd)" sed_opts=(-i) && [ "$(uname)" = "Darwin" ] && sed_opts=(-i '') @@ -29,8 +37,6 @@ case "${VORTEX_CURRENT_MAJOR}" in ;; esac -yarn docusaurus docs:version "${VORTEX_CURRENT_MAJOR}.x" - git -C "${ROOT_DIR}" fetch origin "${other_major}.x" --depth=1 || { echo "ERROR: Failed to fetch the ${other_major}.x branch." >&2 exit 1 @@ -41,17 +47,25 @@ git -C "${ROOT_DIR}" rev-parse --verify "origin/${other_major}.x" >/dev/null || exit 1 } -rm -rf content -mkdir -p content +# Start from a known state so a re-run cannot snapshot a previous assembly. +rm -rf "${WORKSPACE_DIR}" versioned_docs versioned_sidebars versions.json +mkdir -p "${WORKSPACE_DIR}" + +# Snapshot the current major from a copy, so 'docs:version' reads the branch's +# documentation without the tracked 'content/' being the staging area. +cp -R content "${WORKSPACE_DIR}/content" + +yarn docusaurus docs:version "${VORTEX_CURRENT_MAJOR}.x" + +rm -rf "${WORKSPACE_DIR}/content" +mkdir -p "${WORKSPACE_DIR}/content" -# Extracted through an archive rather than checked out, so a local run leaves -# the index untouched and 'git restore content' returns to the branch. -git -C "${ROOT_DIR}" archive "origin/${other_major}.x:.vortex/docs/content" | tar -x -C content || { +git -C "${ROOT_DIR}" archive "origin/${other_major}.x:.vortex/docs/content" | tar -x -C "${WORKSPACE_DIR}/content" || { echo "ERROR: Failed to extract content from ${other_major}.x." >&2 exit 1 } -[ -n "$(ls -A content)" ] || { +[ -n "$(ls -A "${WORKSPACE_DIR}/content")" ] || { echo "ERROR: The ${other_major}.x branch carries no documentation content." >&2 exit 1 } @@ -61,5 +75,5 @@ git -C "${ROOT_DIR}" archive "origin/${other_major}.x:.vortex/docs/content" | ta # Re-point those links at the major they were written for. A link that already # names a version is left as authored, so the alternation skips '/v' followed by # a digit. -find content -type f \( -name '*.md' -o -name '*.mdx' \) -exec \ +find "${WORKSPACE_DIR}/content" -type f \( -name '*.md' -o -name '*.mdx' \) -exec \ sed -E "${sed_opts[@]}" "s%\]\(/docs([)#?]|/[^v]|/v[^0-9])%](/docs/v${other_major}\1%g" {} + diff --git a/.vortex/docs/content/contributing/maintenance/documentation.mdx b/.vortex/docs/content/contributing/maintenance/documentation.mdx index 184ccb2f76..4bcbbf4ac4 100644 --- a/.vortex/docs/content/contributing/maintenance/documentation.mdx +++ b/.vortex/docs/content/contributing/maintenance/documentation.mdx @@ -93,17 +93,18 @@ To assemble and preview the aggregate site locally: ```shell cd .vortex -# Fetch the other major's documentation into 'docs/content'. +# Stage both majors into a disposable workspace. ahoy assemble-docs # Build both versions. The build fails on broken internal links. ahoy build-docs ``` -Assembling replaces `docs/content` with the other major's documentation. -Restore the branch with `git restore .vortex/docs/content` and delete -`versioned_docs/`, `versioned_sidebars/` and `versions.json` to return to a -single-version build. +Assembling only ever reads the tracked `docs/content`. The snapshot and the +other major's documentation are staged in git-ignored directories, and the +build switches to them when they are present, so a branch's own files are never +modified. Run `yarn clear` in `.vortex/docs` to discard the assembly and return +to a single-version build. ### Publishing diff --git a/.vortex/docs/docusaurus.config.js b/.vortex/docs/docusaurus.config.js index e79364be46..78bf0ee027 100644 --- a/.vortex/docs/docusaurus.config.js +++ b/.vortex/docs/docusaurus.config.js @@ -10,18 +10,25 @@ import {themes as prismThemes} from 'prism-react-renderer'; // Multi-version mode turns on automatically when a 'versioned_docs/' snapshot // is present: 'versioned_docs/version-1.x' is v1 (the default, served at the -// bare '/docs') and the current 'content/' is v2 (served at '/docs/v2'). With -// no snapshot - local development, per-branch preview builds, and the -// 'docusaurus docs:version' run that creates the snapshot - the site builds -// 'content/' as a single unversioned set, so the config never references a -// version that does not exist yet. The publish jobs assemble the snapshot in -// CI; it is never committed to a branch. +// bare '/docs') and the assembled docs are v2 (served at '/docs/v2'). With no +// snapshot - local development, per-branch preview builds, and the +// 'docusaurus docs:version' run that creates the snapshot - the site builds a +// single unversioned set, so the config never references a version that does +// not exist yet. The publish jobs assemble the snapshot in CI; it is never +// committed to a branch. const versioned = fs.existsSync('versioned_docs'); +// The docs the build reads. 'assemble-versioned-docs.sh' writes the other +// major's content into a disposable workspace and never modifies the tracked +// 'content/', so an assembled tree is picked up by its presence alone and a +// plain build of the branch is unaffected. +const assembledDocsPath = '.docusaurus-versioned/content'; +const docsPath = fs.existsSync(assembledDocsPath) ? assembledDocsPath : 'content'; + // The current major (the 'VORTEX_CURRENT_MAJOR' repository variable, default 1) // drives the whole site: its docs are a snapshot under 'versioned_docs/' served -// as the default at the bare '/docs', and the live 'content/' (pulled from the -// other major's '{N}.x' branch in CI) is served at '/docs/v{other}'. Bumping +// as the default at the bare '/docs', and the assembled docs (pulled from the +// other major's '{N}.x' branch in CI) are served at '/docs/v{other}'. Bumping // that one variable promotes a new major - nothing else changes here. const currentMajor = process.env.VORTEX_CURRENT_MAJOR || '1'; const otherMajor = currentMajor === '1' ? '2' : '1'; @@ -80,13 +87,21 @@ const config = { docs: { routeBasePath: '/docs', sidebarPath: './sidebars.js', - path: 'content', - // Please change this to your repo. + path: docsPath, // Remove this to remove the "edit this page" links. - editUrl: 'https://github.com/drevops/vortex/tree/main/.vortex/docs/', + // + // Both the snapshot and the assembled docs are built from copies that + // exist only for the build, so the link is composed from the branch + // that actually carries the page: the other major's docs live on its + // '{N}.x' branch, everything else on 'main'. + editUrl: ({version, docPath}) => { + const branch = versioned && version === 'current' ? `${otherMajor}.x` : 'main'; + + return `https://github.com/drevops/vortex/tree/${branch}/.vortex/docs/content/${docPath}`; + }, // In versioned (aggregate) builds the current major is the snapshot // in 'versioned_docs/' served at the bare '/docs' (the default), and - // the live 'content/' is the other major at '/docs/v{other}'. Both + // the assembled docs are the other major at '/docs/v{other}'. Both // are derived from 'VORTEX_CURRENT_MAJOR' - no manual edits to flip. ...(versioned ? { lastVersion: currentDocsVersion, @@ -125,7 +140,7 @@ const config = { ({ // @see https://github.com/easyops-cn/docusaurus-search-local#theme-options searchBarPosition: 'left', - docsDir: 'content', + docsDir: docsPath, docsRouteBasePath: '/docs', indexBlog: false, hashed: true, diff --git a/.vortex/docs/package.json b/.vortex/docs/package.json index 9470424f9c..050ec4fbf6 100644 --- a/.vortex/docs/package.json +++ b/.vortex/docs/package.json @@ -7,7 +7,7 @@ "start": "DOCUSAURUS_GENERATED_FILES_DIR_NAME=.docusaurus-dev docusaurus start", "build": "docusaurus build", "swizzle": "docusaurus swizzle", - "clear": "docusaurus clear && DOCUSAURUS_GENERATED_FILES_DIR_NAME=.docusaurus-dev docusaurus clear", + "clear": "docusaurus clear && DOCUSAURUS_GENERATED_FILES_DIR_NAME=.docusaurus-dev docusaurus clear && rm -rf .docusaurus-versioned versioned_docs versioned_sidebars versions.json", "serve": "docusaurus serve", "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids", From 05dbc90735fbd13c93d81883eda9f10b9bb834cc Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Fri, 21 Aug 2026 08:05:56 +1000 Subject: [PATCH 03/12] Addressed code review: corrected the 'assemble-docs' usage description. --- .vortex/.ahoy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vortex/.ahoy.yml b/.vortex/.ahoy.yml index 1560697da0..3dc02a5c74 100644 --- a/.vortex/.ahoy.yml +++ b/.vortex/.ahoy.yml @@ -138,7 +138,7 @@ commands: cmd: yarn --cwd=docs run update-variables assemble-docs: - usage: Assemble the multi-version documentation site. Replaces 'docs/content' with the other major's documentation. + usage: Assemble the multi-version documentation site into a disposable workspace. Does not modify 'docs/content'. cmd: yarn --cwd=docs run assemble-versions update-videos: From 85445b1486bb9f17cb07eb1843443df9b1676dab Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Fri, 21 Aug 2026 08:40:42 +1000 Subject: [PATCH 04/12] Served each major's static assets alongside its documentation. Assembly carried only 'content/' across, so the aggregate site served one 'static/' for both majors: 15 assets that share a name but differ between majors resolved to the current major's copy, and 3 that exist only on the other major returned 404. The other major's 'static/' is now staged under a '/v{N}' prefix and its asset references re-pointed to match. --- .../docs/.utils/assemble-versioned-docs.sh | 37 +++++++++++++++++-- .../maintenance/documentation.mdx | 6 +++ .vortex/docs/docusaurus.config.js | 8 ++++ 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/.vortex/docs/.utils/assemble-versioned-docs.sh b/.vortex/docs/.utils/assemble-versioned-docs.sh index 3824fa035a..2613ed7db8 100755 --- a/.vortex/docs/.utils/assemble-versioned-docs.sh +++ b/.vortex/docs/.utils/assemble-versioned-docs.sh @@ -20,8 +20,8 @@ set -o pipefail # The major this branch ships. Its docs become the site's default version. VORTEX_CURRENT_MAJOR="${VORTEX_CURRENT_MAJOR:-1}" -# Staging area for the docs the build reads. 'docusaurus.config.js' switches to -# it when it exists. +# Staging area for the docs and assets the build reads. 'docusaurus.config.js' +# switches to it when it exists. WORKSPACE_DIR=".docusaurus-versioned" ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../" && pwd)" @@ -37,6 +37,10 @@ case "${VORTEX_CURRENT_MAJOR}" in ;; esac +# Docusaurus copies every staged static directory into the build root, so the +# other major's assets land at '/v{other}/...'. +other_static_dir="${WORKSPACE_DIR}/static/v${other_major}" + git -C "${ROOT_DIR}" fetch origin "${other_major}.x" --depth=1 || { echo "ERROR: Failed to fetch the ${other_major}.x branch." >&2 exit 1 @@ -58,7 +62,7 @@ cp -R content "${WORKSPACE_DIR}/content" yarn docusaurus docs:version "${VORTEX_CURRENT_MAJOR}.x" rm -rf "${WORKSPACE_DIR}/content" -mkdir -p "${WORKSPACE_DIR}/content" +mkdir -p "${WORKSPACE_DIR}/content" "${other_static_dir}" git -C "${ROOT_DIR}" archive "origin/${other_major}.x:.vortex/docs/content" | tar -x -C "${WORKSPACE_DIR}/content" || { echo "ERROR: Failed to extract content from ${other_major}.x." >&2 @@ -70,6 +74,20 @@ git -C "${ROOT_DIR}" archive "origin/${other_major}.x:.vortex/docs/content" | ta exit 1 } +# Both majors record their own demo videos and diagrams under the same +# 'static/img' names, so the other major's assets are served from their own +# '/v{other}' prefix instead of overwriting - or silently losing to - the +# current major's. +git -C "${ROOT_DIR}" archive "origin/${other_major}.x:.vortex/docs/static" | tar -x -C "${other_static_dir}" || { + echo "ERROR: Failed to extract static assets from ${other_major}.x." >&2 + exit 1 +} + +[ -n "$(ls -A "${other_static_dir}")" ] || { + echo "ERROR: The ${other_major}.x branch carries no static assets." >&2 + exit 1 +} + # Every branch authors its docs against the bare '/docs' mount, so an absolute # link lands on the current major once the content is served at '/docs/v{other}'. # Re-point those links at the major they were written for. A link that already @@ -77,3 +95,16 @@ git -C "${ROOT_DIR}" archive "origin/${other_major}.x:.vortex/docs/content" | ta # a digit. find "${WORKSPACE_DIR}/content" -type f \( -name '*.md' -o -name '*.mdx' \) -exec \ sed -E "${sed_opts[@]}" "s%\]\(/docs([)#?]|/[^v]|/v[^0-9])%](/docs/v${other_major}\1%g" {} + + +# Asset references are written against the bare static root for the same reason. +# Each directory the other major ships is remapped, so a new asset directory on +# that branch is carried over without editing this script. Markdown targets and +# JSX attribute values are the two forms an asset path appears in. +for asset_dir in "${other_static_dir}"/*/; do + [ -d "${asset_dir}" ] || continue + + asset_name="$(basename "${asset_dir}")" + + find "${WORKSPACE_DIR}/content" -type f \( -name '*.md' -o -name '*.mdx' \) -exec \ + sed -E "${sed_opts[@]}" "s%([\"'(])/${asset_name}/%\1/v${other_major}/${asset_name}/%g" {} + +done diff --git a/.vortex/docs/content/contributing/maintenance/documentation.mdx b/.vortex/docs/content/contributing/maintenance/documentation.mdx index 4bcbbf4ac4..ca7d9f2abd 100644 --- a/.vortex/docs/content/contributing/maintenance/documentation.mdx +++ b/.vortex/docs/content/contributing/maintenance/documentation.mdx @@ -88,6 +88,12 @@ is assembled under `/docs/v{N}`, those links are re-pointed at the major they were written for, and a link that already names a version is left alone. Write links the way they read on their own branch. +Static assets work the same way. Both majors record their own demo videos and +diagrams under the same `static/img` names, so the other major's `static/` is +served from `/v{N}` and its asset references are re-pointed to match. Each +version therefore shows its own screenshots and recordings. Reference assets the +way they read on their own branch, from the bare `/img`. + To assemble and preview the aggregate site locally: ```shell diff --git a/.vortex/docs/docusaurus.config.js b/.vortex/docs/docusaurus.config.js index 78bf0ee027..b75d6c2495 100644 --- a/.vortex/docs/docusaurus.config.js +++ b/.vortex/docs/docusaurus.config.js @@ -25,6 +25,12 @@ const versioned = fs.existsSync('versioned_docs'); const assembledDocsPath = '.docusaurus-versioned/content'; const docsPath = fs.existsSync(assembledDocsPath) ? assembledDocsPath : 'content'; +// The other major records its own demo videos and diagrams under the same +// 'img/' names as this one, so its assets are staged under a '/v{other}' prefix +// and served alongside rather than colliding with 'static/'. +const assembledStaticPath = '.docusaurus-versioned/static'; +const staticDirectories = fs.existsSync(assembledStaticPath) ? ['static', assembledStaticPath] : ['static']; + // The current major (the 'VORTEX_CURRENT_MAJOR' repository variable, default 1) // drives the whole site: its docs are a snapshot under 'versioned_docs/' served // as the default at the bare '/docs', and the assembled docs (pulled from the @@ -67,6 +73,8 @@ const config = { organizationName: 'DrevOps', projectName: 'Vortex', + staticDirectories, + onBrokenLinks: 'throw', onBrokenMarkdownLinks: 'throw', onBrokenAnchors: 'warn', From b35d25f57822472d69a22ff396f86cc3efc325f5 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Fri, 21 Aug 2026 09:09:20 +1000 Subject: [PATCH 05/12] Built the combined documentation site in a single disposable directory. Assembly wrote into 'docs/' itself, spreading generated state across the tracked site. It now builds a self-contained copy in 'docs_combined/', where the version snapshot, both majors' content and both majors' assets live, leaving 'docs/' read-only. 'ahoy build-combined-docs' builds it and 'ahoy serve-combined-docs [port]' serves it over the PHP built-in server. --- .github/workflows/vortex-release.yml | 19 ++--- .github/workflows/vortex-test-docs.yml | 30 +++++--- .vortex/.ahoy.yml | 26 +++++-- .vortex/.gitignore | 4 + .vortex/docs/.gitignore | 9 +-- ...oned-docs.sh => assemble-combined-docs.sh} | 74 ++++++++++--------- .vortex/docs/.utils/serve-router.php | 55 ++++++++++++++ .../maintenance/documentation.mdx | 21 +++--- .vortex/docs/docusaurus.config.js | 35 +++------ .vortex/docs/package.json | 3 +- 10 files changed, 167 insertions(+), 109 deletions(-) create mode 100644 .vortex/.gitignore rename .vortex/docs/.utils/{assemble-versioned-docs.sh => assemble-combined-docs.sh} (55%) create mode 100644 .vortex/docs/.utils/serve-router.php diff --git a/.github/workflows/vortex-release.yml b/.github/workflows/vortex-release.yml index 307979bdbc..bb1b73abaf 100644 --- a/.github/workflows/vortex-release.yml +++ b/.github/workflows/vortex-release.yml @@ -212,17 +212,12 @@ jobs: run: yarn install --frozen-lockfile working-directory: .vortex/docs - # Production (GitHub Pages) serves the same aggregate site that - # 'vortex-test-docs.yml' builds for development deploys and previews. - - name: Assemble versioned docs - run: ./.utils/assemble-versioned-docs.sh - working-directory: .vortex/docs - env: - VORTEX_CURRENT_MAJOR: ${{ env.CURRENT_MAJOR }} - - - name: Build documentation site - run: yarn run build - working-directory: .vortex/docs + # Production (GitHub Pages) serves the same combined site that + # 'vortex-test-docs.yml' builds for development deploys and previews. The + # binaries copied above are picked up from 'docs/static'. + - name: Build the combined documentation site + run: ./docs/.utils/assemble-combined-docs.sh + working-directory: .vortex env: VORTEX_CURRENT_MAJOR: ${{ env.CURRENT_MAJOR }} @@ -243,7 +238,7 @@ jobs: - name: Upload documentation site uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5 with: - path: .vortex/docs/build + path: .vortex/docs_combined/build - name: Deploy to GitHub Pages uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5 diff --git a/.github/workflows/vortex-test-docs.yml b/.github/workflows/vortex-test-docs.yml index 3ab74e4d7c..dbf6317c8a 100644 --- a/.github/workflows/vortex-test-docs.yml +++ b/.github/workflows/vortex-test-docs.yml @@ -29,6 +29,9 @@ jobs: # binary from its own workflow, so the workflow name identifies the major # for pushes, branches and fork pull requests alike. BUILD_MAJOR: ${{ github.event.workflow_run.name == 'Vortex - Test CLI' && '2' || '1' }} + # Where the built site lands. The combined build assembles both majors + # into its own site directory; an other-major branch builds in place. + DOCS_BUILD_DIR: ${{ (github.event.workflow_run.name == 'Vortex - Test CLI' && '2' || '1') == (vars.VORTEX_CURRENT_MAJOR || '1') && '.vortex/docs_combined/build' || '.vortex/docs/build' }} BINARY_ARTIFACT: ${{ github.event.workflow_run.name == 'Vortex - Test CLI' && 'vortex-cli' || 'vortex-installer' }} BINARY_FILE: ${{ github.event.workflow_run.name == 'Vortex - Test CLI' && 'vortex.phar' || 'installer.phar' }} @@ -103,17 +106,6 @@ jobs: run: yarn run spellcheck working-directory: '${{ github.workspace }}/.vortex/docs' - # Every current-major build assembles, so a branch is checked against the - # same aggregate site its merge publishes. Other-major branches stay - # single-version previews: their docs reach production through the - # current-major branch, which builds them as '/docs/v{other}'. - - name: Assemble versioned docs - if: env.BUILD_MAJOR == env.CURRENT_MAJOR - run: ./.utils/assemble-versioned-docs.sh - working-directory: '${{ github.workspace }}/.vortex/docs' - env: - VORTEX_CURRENT_MAJOR: ${{ env.CURRENT_MAJOR }} - # On the main deploy, publish both majors to match the multi-version # docs: '/v1/install' and '/v2/install' are the stable per-major pins and # the bare '/install' is a copy of the current major's pin (the @@ -145,7 +137,21 @@ jobs: cp "vortex-v${OTHER_MAJOR}/.vortex/cli/.build/vortex.phar" ".vortex/docs/static/v${OTHER_MAJOR}/install" cp ".vortex/docs/static/v${CURRENT_MAJOR}/install" .vortex/docs/static/install + # Every current-major build assembles, so a branch is checked against the + # same combined site its merge publishes. It runs after the binaries are + # staged, which the assembly copies across with the rest of 'docs/'. + - name: Build the combined documentation site + if: env.BUILD_MAJOR == env.CURRENT_MAJOR + run: ./docs/.utils/assemble-combined-docs.sh + working-directory: '${{ github.workspace }}/.vortex' + env: + VORTEX_CURRENT_MAJOR: ${{ env.CURRENT_MAJOR }} + + # Other-major branches stay single-version previews: their docs reach + # production through the current-major branch, which builds them as + # '/docs/v{other}'. - name: Build documentation site + if: env.BUILD_MAJOR != env.CURRENT_MAJOR run: yarn run build working-directory: '${{ github.workspace }}/.vortex/docs' env: @@ -162,7 +168,7 @@ jobs: id: netlify uses: nwtgck/actions-netlify@d22a32a27c918fe470bbc562e984f80ec48c2668 # v4.0 with: - publish-dir: '.vortex/docs/build' + publish-dir: ${{ env.DOCS_BUILD_DIR }} production-deploy: ${{ github.event.workflow_run.head_branch == 'main' }} deploy-message: "Deploy from GitHub Actions" enable-pull-request-comment: false diff --git a/.vortex/.ahoy.yml b/.vortex/.ahoy.yml index 3dc02a5c74..c0c4052841 100644 --- a/.vortex/.ahoy.yml +++ b/.vortex/.ahoy.yml @@ -18,16 +18,16 @@ commands: yarn --cwd=docs install --frozen-lockfile docs: - name: Start documentation server. + usage: Start the documentation development server. Passes options through, e.g. 'ahoy docs --port 4000'. cmd: | [ ! -d ./docs/node_modules ] && yarn --cwd=docs install --frozen-lockfile - yarn --cwd=docs run start + yarn --cwd=docs run start "$@" docs-serve: - name: Serve built documentation. + usage: Serve the built documentation. Passes options through, e.g. 'ahoy docs-serve --port 4000'. cmd: | [ ! -d ./docs/node_modules ] && yarn --cwd=docs install --frozen-lockfile - yarn --cwd=docs run start + yarn --cwd=docs run serve "$@" build-docs: name: Build documentation. @@ -35,6 +35,20 @@ commands: [ ! -d ./docs/node_modules ] && yarn --cwd=docs install --frozen-lockfile yarn --cwd=docs run build + build-combined-docs: + usage: Build the combined site serving both majors into the disposable 'docs_combined' directory. + cmd: | + [ ! -d ./docs/node_modules ] && yarn --cwd=docs install --frozen-lockfile + ./docs/.utils/assemble-combined-docs.sh + + serve-combined-docs: + usage: Serve the built combined site. Pass a port as the first argument, e.g. 'ahoy serve-combined-docs 4000'. + cmd: | + [ -d ./docs_combined/build ] || { echo "No combined site found. Run 'ahoy build-combined-docs' first."; exit 1; } + port="${1:-8000}" + echo "Serving the combined documentation site at http://localhost:${port}" + php -S "localhost:${port}" -t ./docs_combined/build ./docs/.utils/serve-router.php + build-installer: name: Build documentation. cmd: | @@ -137,10 +151,6 @@ commands: usage: Update the documentation. cmd: yarn --cwd=docs run update-variables - assemble-docs: - usage: Assemble the multi-version documentation site into a disposable workspace. Does not modify 'docs/content'. - cmd: yarn --cwd=docs run assemble-versions - update-videos: usage: Update documentation videos. Pass names to record a subset (installer, build, provision, lint, test, test-bdd); default is all six. cmd: php docs/.utils/update-videos.php "$@" diff --git a/.vortex/.gitignore b/.vortex/.gitignore new file mode 100644 index 0000000000..ea210f24b2 --- /dev/null +++ b/.vortex/.gitignore @@ -0,0 +1,4 @@ +# The combined documentation site built by `ahoy build-combined-docs`. It is a +# generated copy of `docs/` carrying both majors, rebuilt from scratch on every +# run and safe to delete at any time. +/docs_combined diff --git a/.vortex/docs/.gitignore b/.vortex/docs/.gitignore index 5396bc3267..de5b217ffd 100644 --- a/.vortex/docs/.gitignore +++ b/.vortex/docs/.gitignore @@ -10,12 +10,9 @@ .docusaurus-dev .cache-loader -# Assembled docs versions - created by the CI publish step (or a local -# `ahoy assemble-docs` to preview the multi-version site) and never committed. -# The build auto-detects `versioned_docs/` to switch to multi-version and -# `.docusaurus-versioned/content` to read the other major's documentation, so -# the tracked `content/` is never used as a staging area. -/.docusaurus-versioned +# Version snapshots. Only the disposable `docs_combined/` site carries them, so +# these never appear here in normal use; the pattern stays as a guard against a +# stray local `docusaurus docs:version` run being committed. /versioned_docs /versioned_sidebars /versions.json diff --git a/.vortex/docs/.utils/assemble-versioned-docs.sh b/.vortex/docs/.utils/assemble-combined-docs.sh similarity index 55% rename from .vortex/docs/.utils/assemble-versioned-docs.sh rename to .vortex/docs/.utils/assemble-combined-docs.sh index 2613ed7db8..b5548ffe98 100755 --- a/.vortex/docs/.utils/assemble-versioned-docs.sh +++ b/.vortex/docs/.utils/assemble-combined-docs.sh @@ -1,17 +1,15 @@ #!/usr/bin/env bash ## -# Assemble the multi-version documentation site. +# Assemble the combined documentation site, serving both majors from one build. # -# Snapshots this branch's docs as the current major's version and stages the -# other major's docs from its '{N}.x' branch. Docusaurus serves the snapshot at -# the bare '/docs' and the staged docs at '/docs/v{other}'. -# -# Everything is written to disposable, git-ignored locations: the tracked -# 'content/' is only ever read. Delete 'WORKSPACE_DIR', 'versioned_docs', -# 'versioned_sidebars' and 'versions.json' to return to a single-version build. +# Produces a complete, self-contained Docusaurus site in 'docs_combined/': this +# branch's documentation as the default version at '/docs', and the other +# major's '{N}.x' branch documentation at '/docs/v{N}' with its own assets at +# '/v{N}'. Everything the assembly writes stays inside 'docs_combined/', which +# is disposable - 'docs/' is only ever read. # # @usage -# cd .vortex/docs && ./.utils/assemble-versioned-docs.sh +# cd .vortex && ./docs/.utils/assemble-combined-docs.sh set -eu set -o pipefail @@ -20,11 +18,9 @@ set -o pipefail # The major this branch ships. Its docs become the site's default version. VORTEX_CURRENT_MAJOR="${VORTEX_CURRENT_MAJOR:-1}" -# Staging area for the docs and assets the build reads. 'docusaurus.config.js' -# switches to it when it exists. -WORKSPACE_DIR=".docusaurus-versioned" - ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../" && pwd)" +DOCS_DIR="${ROOT_DIR}/.vortex/docs" +COMBINED_DIR="${ROOT_DIR}/.vortex/docs_combined" sed_opts=(-i) && [ "$(uname)" = "Darwin" ] && sed_opts=(-i '') @@ -37,10 +33,6 @@ case "${VORTEX_CURRENT_MAJOR}" in ;; esac -# Docusaurus copies every staged static directory into the build root, so the -# other major's assets land at '/v{other}/...'. -other_static_dir="${WORKSPACE_DIR}/static/v${other_major}" - git -C "${ROOT_DIR}" fetch origin "${other_major}.x" --depth=1 || { echo "ERROR: Failed to fetch the ${other_major}.x branch." >&2 exit 1 @@ -51,33 +43,45 @@ git -C "${ROOT_DIR}" rev-parse --verify "origin/${other_major}.x" >/dev/null || exit 1 } -# Start from a known state so a re-run cannot snapshot a previous assembly. -rm -rf "${WORKSPACE_DIR}" versioned_docs versioned_sidebars versions.json -mkdir -p "${WORKSPACE_DIR}" +rm -rf "${COMBINED_DIR}" +mkdir -p "${COMBINED_DIR}" + +# The combined site is a copy of this one, so it builds with the same config, +# components and sidebars. Generated and installed directories are rebuilt or +# linked below rather than copied. +rsync -a \ + --exclude '/node_modules' \ + --exclude '/build' \ + --exclude '/.docusaurus' \ + --exclude '/.docusaurus-dev' \ + --exclude '/.logs' \ + "${DOCS_DIR}/" "${COMBINED_DIR}/" -# Snapshot the current major from a copy, so 'docs:version' reads the branch's -# documentation without the tracked 'content/' being the staging area. -cp -R content "${WORKSPACE_DIR}/content" +ln -s "${DOCS_DIR}/node_modules" "${COMBINED_DIR}/node_modules" -yarn docusaurus docs:version "${VORTEX_CURRENT_MAJOR}.x" +# Snapshot this branch as the default version before 'content/' is handed over +# to the other major. +yarn --cwd="${COMBINED_DIR}" docusaurus docs:version "${VORTEX_CURRENT_MAJOR}.x" -rm -rf "${WORKSPACE_DIR}/content" -mkdir -p "${WORKSPACE_DIR}/content" "${other_static_dir}" +rm -rf "${COMBINED_DIR}/content" +mkdir -p "${COMBINED_DIR}/content" -git -C "${ROOT_DIR}" archive "origin/${other_major}.x:.vortex/docs/content" | tar -x -C "${WORKSPACE_DIR}/content" || { +git -C "${ROOT_DIR}" archive "origin/${other_major}.x:.vortex/docs/content" | tar -x -C "${COMBINED_DIR}/content" || { echo "ERROR: Failed to extract content from ${other_major}.x." >&2 exit 1 } -[ -n "$(ls -A "${WORKSPACE_DIR}/content")" ] || { +[ -n "$(ls -A "${COMBINED_DIR}/content")" ] || { echo "ERROR: The ${other_major}.x branch carries no documentation content." >&2 exit 1 } # Both majors record their own demo videos and diagrams under the same -# 'static/img' names, so the other major's assets are served from their own -# '/v{other}' prefix instead of overwriting - or silently losing to - the -# current major's. +# 'static/img' names, so the other major's assets get their own '/v{other}' +# prefix instead of losing to this major's copies. +other_static_dir="${COMBINED_DIR}/static/v${other_major}" +mkdir -p "${other_static_dir}" + git -C "${ROOT_DIR}" archive "origin/${other_major}.x:.vortex/docs/static" | tar -x -C "${other_static_dir}" || { echo "ERROR: Failed to extract static assets from ${other_major}.x." >&2 exit 1 @@ -93,7 +97,7 @@ git -C "${ROOT_DIR}" archive "origin/${other_major}.x:.vortex/docs/static" | tar # Re-point those links at the major they were written for. A link that already # names a version is left as authored, so the alternation skips '/v' followed by # a digit. -find "${WORKSPACE_DIR}/content" -type f \( -name '*.md' -o -name '*.mdx' \) -exec \ +find "${COMBINED_DIR}/content" -type f \( -name '*.md' -o -name '*.mdx' \) -exec \ sed -E "${sed_opts[@]}" "s%\]\(/docs([)#?]|/[^v]|/v[^0-9])%](/docs/v${other_major}\1%g" {} + # Asset references are written against the bare static root for the same reason. @@ -105,6 +109,10 @@ for asset_dir in "${other_static_dir}"/*/; do asset_name="$(basename "${asset_dir}")" - find "${WORKSPACE_DIR}/content" -type f \( -name '*.md' -o -name '*.mdx' \) -exec \ + find "${COMBINED_DIR}/content" -type f \( -name '*.md' -o -name '*.mdx' \) -exec \ sed -E "${sed_opts[@]}" "s%([\"'(])/${asset_name}/%\1/v${other_major}/${asset_name}/%g" {} + done + +VORTEX_CURRENT_MAJOR="${VORTEX_CURRENT_MAJOR}" yarn --cwd="${COMBINED_DIR}" run build + +echo "Combined documentation site built at ${COMBINED_DIR}/build" diff --git a/.vortex/docs/.utils/serve-router.php b/.vortex/docs/.utils/serve-router.php new file mode 100644 index 0000000000..5e94c98f63 --- /dev/null +++ b/.vortex/docs/.utils/serve-router.php @@ -0,0 +1,55 @@ + serve-router.php + */ + +declare(strict_types=1); + +$doc_root = $_SERVER['DOCUMENT_ROOT']; +$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); +$path = is_string($path) ? urldecode($path) : '/'; + +// Reject traversal outside the build directory before touching the filesystem. +if (str_contains($path, '..')) { + http_response_code(400); + + return TRUE; +} + +$target = $doc_root . $path; + +// Serve an existing file as-is and let the server set the content type. +if (is_file($target)) { + return FALSE; +} + +$index = rtrim($target, '/') . '/index.html'; + +if (is_file($index)) { + header('Content-Type: text/html; charset=UTF-8'); + readfile($index); + + return TRUE; +} + +// Fall back to the site's own 404 page so navigation errors look like the site. +$not_found = $doc_root . '/404.html'; + +http_response_code(404); + +if (is_file($not_found)) { + header('Content-Type: text/html; charset=UTF-8'); + readfile($not_found); +} + +return TRUE; diff --git a/.vortex/docs/content/contributing/maintenance/documentation.mdx b/.vortex/docs/content/contributing/maintenance/documentation.mdx index ca7d9f2abd..066bde812c 100644 --- a/.vortex/docs/content/contributing/maintenance/documentation.mdx +++ b/.vortex/docs/content/contributing/maintenance/documentation.mdx @@ -75,7 +75,7 @@ The 6 terminal demo videos embedded in the docs are regenerated with `ahoy update-videos [names]` from `.vortex/` - see [Installer > Installer video](installer.mdx#installer-video) for the pipeline. -### Multi-version site +### Combined site The published site serves two majors at once. The branch that ships the current major - selected by the `VORTEX_CURRENT_MAJOR` repository variable, default `1` - @@ -94,23 +94,22 @@ served from `/v{N}` and its asset references are re-pointed to match. Each version therefore shows its own screenshots and recordings. Reference assets the way they read on their own branch, from the bare `/img`. -To assemble and preview the aggregate site locally: +To build and browse the combined site locally: ```shell cd .vortex -# Stage both majors into a disposable workspace. -ahoy assemble-docs +# Build both majors into 'docs_combined'. Fails on broken internal links. +ahoy build-combined-docs -# Build both versions. The build fails on broken internal links. -ahoy build-docs +# Serve the result. The port defaults to 8000. +ahoy serve-combined-docs 4000 ``` -Assembling only ever reads the tracked `docs/content`. The snapshot and the -other major's documentation are staged in git-ignored directories, and the -build switches to them when they are present, so a branch's own files are never -modified. Run `yarn clear` in `.vortex/docs` to discard the assembly and return -to a single-version build. +Everything the build writes stays inside `docs_combined`, a git-ignored copy of +`docs` that is rebuilt from scratch on every run and safe to delete at any time. +The tracked `docs` directory is only ever read, so `ahoy docs` and +`ahoy build-docs` keep working on this branch's documentation alone. ### Publishing diff --git a/.vortex/docs/docusaurus.config.js b/.vortex/docs/docusaurus.config.js index b75d6c2495..3bc46c2090 100644 --- a/.vortex/docs/docusaurus.config.js +++ b/.vortex/docs/docusaurus.config.js @@ -10,31 +10,18 @@ import {themes as prismThemes} from 'prism-react-renderer'; // Multi-version mode turns on automatically when a 'versioned_docs/' snapshot // is present: 'versioned_docs/version-1.x' is v1 (the default, served at the -// bare '/docs') and the assembled docs are v2 (served at '/docs/v2'). With no -// snapshot - local development, per-branch preview builds, and the -// 'docusaurus docs:version' run that creates the snapshot - the site builds a -// single unversioned set, so the config never references a version that does -// not exist yet. The publish jobs assemble the snapshot in CI; it is never -// committed to a branch. +// bare '/docs') and 'content/' is v2 (served at '/docs/v2'). With no snapshot - +// local development and per-branch preview builds - the site builds 'content/' +// as a single unversioned set, so the config never references a version that +// does not exist yet. Only the disposable site that +// 'assemble-combined-docs.sh' writes to 'docs_combined/' carries a snapshot; +// it is never committed to a branch. const versioned = fs.existsSync('versioned_docs'); -// The docs the build reads. 'assemble-versioned-docs.sh' writes the other -// major's content into a disposable workspace and never modifies the tracked -// 'content/', so an assembled tree is picked up by its presence alone and a -// plain build of the branch is unaffected. -const assembledDocsPath = '.docusaurus-versioned/content'; -const docsPath = fs.existsSync(assembledDocsPath) ? assembledDocsPath : 'content'; - -// The other major records its own demo videos and diagrams under the same -// 'img/' names as this one, so its assets are staged under a '/v{other}' prefix -// and served alongside rather than colliding with 'static/'. -const assembledStaticPath = '.docusaurus-versioned/static'; -const staticDirectories = fs.existsSync(assembledStaticPath) ? ['static', assembledStaticPath] : ['static']; - // The current major (the 'VORTEX_CURRENT_MAJOR' repository variable, default 1) // drives the whole site: its docs are a snapshot under 'versioned_docs/' served -// as the default at the bare '/docs', and the assembled docs (pulled from the -// other major's '{N}.x' branch in CI) are served at '/docs/v{other}'. Bumping +// as the default at the bare '/docs', and 'content/' (replaced with the other +// major's docs in the combined site) is served at '/docs/v{other}'. Bumping // that one variable promotes a new major - nothing else changes here. const currentMajor = process.env.VORTEX_CURRENT_MAJOR || '1'; const otherMajor = currentMajor === '1' ? '2' : '1'; @@ -73,8 +60,6 @@ const config = { organizationName: 'DrevOps', projectName: 'Vortex', - staticDirectories, - onBrokenLinks: 'throw', onBrokenMarkdownLinks: 'throw', onBrokenAnchors: 'warn', @@ -95,7 +80,7 @@ const config = { docs: { routeBasePath: '/docs', sidebarPath: './sidebars.js', - path: docsPath, + path: 'content', // Remove this to remove the "edit this page" links. // // Both the snapshot and the assembled docs are built from copies that @@ -148,7 +133,7 @@ const config = { ({ // @see https://github.com/easyops-cn/docusaurus-search-local#theme-options searchBarPosition: 'left', - docsDir: docsPath, + docsDir: 'content', docsRouteBasePath: '/docs', indexBlog: false, hashed: true, diff --git a/.vortex/docs/package.json b/.vortex/docs/package.json index 050ec4fbf6..e914afdb49 100644 --- a/.vortex/docs/package.json +++ b/.vortex/docs/package.json @@ -7,13 +7,12 @@ "start": "DOCUSAURUS_GENERATED_FILES_DIR_NAME=.docusaurus-dev docusaurus start", "build": "docusaurus build", "swizzle": "docusaurus swizzle", - "clear": "docusaurus clear && DOCUSAURUS_GENERATED_FILES_DIR_NAME=.docusaurus-dev docusaurus clear && rm -rf .docusaurus-versioned versioned_docs versioned_sidebars versions.json", + "clear": "docusaurus clear && DOCUSAURUS_GENERATED_FILES_DIR_NAME=.docusaurus-dev docusaurus clear", "serve": "docusaurus serve", "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids", "spellcheck": "cspell \"content/**/*.mdx\"", "update-variables": "./.utils/update-docs.sh", - "assemble-versions": "./.utils/assemble-versioned-docs.sh", "test": "jest", "test:watch": "jest --watch", "test:coverage": "jest --coverage", From d87f84ee30814432ab2ac882f8ec00223a3e1ea1 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Fri, 21 Aug 2026 09:26:39 +1000 Subject: [PATCH 06/12] Installed the combined site's own dependencies instead of linking them. The combined site symlinked 'docs/node_modules', so it depended on the tracked directory being installed and was not self-contained. It now runs its own install from the copied lock file. --- .vortex/.ahoy.yml | 6 ++---- .vortex/docs/.utils/assemble-combined-docs.sh | 6 +++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.vortex/.ahoy.yml b/.vortex/.ahoy.yml index c0c4052841..385c9986fe 100644 --- a/.vortex/.ahoy.yml +++ b/.vortex/.ahoy.yml @@ -36,10 +36,8 @@ commands: yarn --cwd=docs run build build-combined-docs: - usage: Build the combined site serving both majors into the disposable 'docs_combined' directory. - cmd: | - [ ! -d ./docs/node_modules ] && yarn --cwd=docs install --frozen-lockfile - ./docs/.utils/assemble-combined-docs.sh + usage: Build the combined site serving both majors into the disposable, self-contained 'docs_combined' directory. + cmd: ./docs/.utils/assemble-combined-docs.sh serve-combined-docs: usage: Serve the built combined site. Pass a port as the first argument, e.g. 'ahoy serve-combined-docs 4000'. diff --git a/.vortex/docs/.utils/assemble-combined-docs.sh b/.vortex/docs/.utils/assemble-combined-docs.sh index b5548ffe98..fa2b9a7cc7 100755 --- a/.vortex/docs/.utils/assemble-combined-docs.sh +++ b/.vortex/docs/.utils/assemble-combined-docs.sh @@ -47,8 +47,8 @@ rm -rf "${COMBINED_DIR}" mkdir -p "${COMBINED_DIR}" # The combined site is a copy of this one, so it builds with the same config, -# components and sidebars. Generated and installed directories are rebuilt or -# linked below rather than copied. +# components and sidebars. Generated and installed directories are excluded and +# rebuilt inside it, so it depends on nothing outside itself. rsync -a \ --exclude '/node_modules' \ --exclude '/build' \ @@ -57,7 +57,7 @@ rsync -a \ --exclude '/.logs' \ "${DOCS_DIR}/" "${COMBINED_DIR}/" -ln -s "${DOCS_DIR}/node_modules" "${COMBINED_DIR}/node_modules" +yarn --cwd="${COMBINED_DIR}" install --frozen-lockfile # Snapshot this branch as the default version before 'content/' is handed over # to the other major. From 9a704dd7efe9eb8683234fe47cc1588d2bb2ce42 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Fri, 21 Aug 2026 09:41:51 +1000 Subject: [PATCH 07/12] Drove the combined docs site from an environment variable. 'docusaurus.config.js' detected the combined site by probing the filesystem and carried build-process commentary. It now reads 'VORTEX_DOCS_COMBINED' and describes only what it configures. The 'docs' and 'docs-serve' commands are unchanged; the combined pair is 'docs-combined' and 'docs-combined-serve'. --- .vortex/.ahoy.yml | 20 +++++------ .vortex/.gitignore | 2 +- .vortex/docs/.utils/assemble-combined-docs.sh | 5 +-- .../maintenance/documentation.mdx | 4 +-- .vortex/docs/docusaurus.config.js | 35 ++++++------------- 5 files changed, 27 insertions(+), 39 deletions(-) diff --git a/.vortex/.ahoy.yml b/.vortex/.ahoy.yml index 385c9986fe..63b0b6b0a8 100644 --- a/.vortex/.ahoy.yml +++ b/.vortex/.ahoy.yml @@ -18,16 +18,16 @@ commands: yarn --cwd=docs install --frozen-lockfile docs: - usage: Start the documentation development server. Passes options through, e.g. 'ahoy docs --port 4000'. + name: Start documentation server. cmd: | [ ! -d ./docs/node_modules ] && yarn --cwd=docs install --frozen-lockfile - yarn --cwd=docs run start "$@" + yarn --cwd=docs run start docs-serve: - usage: Serve the built documentation. Passes options through, e.g. 'ahoy docs-serve --port 4000'. + name: Serve built documentation. cmd: | [ ! -d ./docs/node_modules ] && yarn --cwd=docs install --frozen-lockfile - yarn --cwd=docs run serve "$@" + yarn --cwd=docs run start build-docs: name: Build documentation. @@ -35,14 +35,14 @@ commands: [ ! -d ./docs/node_modules ] && yarn --cwd=docs install --frozen-lockfile yarn --cwd=docs run build - build-combined-docs: - usage: Build the combined site serving both majors into the disposable, self-contained 'docs_combined' directory. + docs-combined: + usage: Build the combined site serving both majors into the disposable 'docs_combined' directory. cmd: ./docs/.utils/assemble-combined-docs.sh - serve-combined-docs: - usage: Serve the built combined site. Pass a port as the first argument, e.g. 'ahoy serve-combined-docs 4000'. + docs-combined-serve: + usage: Serve the built combined site. Pass a port as the first argument, e.g. 'ahoy docs-combined-serve 4000'. cmd: | - [ -d ./docs_combined/build ] || { echo "No combined site found. Run 'ahoy build-combined-docs' first."; exit 1; } + [ -d ./docs_combined/build ] || { echo "No combined site found. Run 'ahoy docs-combined' first."; exit 1; } port="${1:-8000}" echo "Serving the combined documentation site at http://localhost:${port}" php -S "localhost:${port}" -t ./docs_combined/build ./docs/.utils/serve-router.php @@ -150,7 +150,7 @@ commands: cmd: yarn --cwd=docs run update-variables update-videos: - usage: Update documentation videos. Pass names to record a subset (installer, build, provision, lint, test, test-bdd); default is all six. + usage: Update documentation videos. Pass names to record a subset (installer, build, provision, lint, test, test-bdd, info, doctor, doctor-info); default is all of them. Pass --verify to re-render the committed artefacts and compare instead of recording. cmd: php docs/.utils/update-videos.php "$@" entrypoint: diff --git a/.vortex/.gitignore b/.vortex/.gitignore index ea210f24b2..5949aeabba 100644 --- a/.vortex/.gitignore +++ b/.vortex/.gitignore @@ -1,4 +1,4 @@ -# The combined documentation site built by `ahoy build-combined-docs`. It is a +# The combined documentation site built by `ahoy docs-combined`. It is a # generated copy of `docs/` carrying both majors, rebuilt from scratch on every # run and safe to delete at any time. /docs_combined diff --git a/.vortex/docs/.utils/assemble-combined-docs.sh b/.vortex/docs/.utils/assemble-combined-docs.sh index fa2b9a7cc7..b82c4fdb52 100755 --- a/.vortex/docs/.utils/assemble-combined-docs.sh +++ b/.vortex/docs/.utils/assemble-combined-docs.sh @@ -60,7 +60,8 @@ rsync -a \ yarn --cwd="${COMBINED_DIR}" install --frozen-lockfile # Snapshot this branch as the default version before 'content/' is handed over -# to the other major. +# to the other major. 'VORTEX_DOCS_COMBINED' stays unset here: the snapshot the +# combined config expects does not exist until this command creates it. yarn --cwd="${COMBINED_DIR}" docusaurus docs:version "${VORTEX_CURRENT_MAJOR}.x" rm -rf "${COMBINED_DIR}/content" @@ -113,6 +114,6 @@ for asset_dir in "${other_static_dir}"/*/; do sed -E "${sed_opts[@]}" "s%([\"'(])/${asset_name}/%\1/v${other_major}/${asset_name}/%g" {} + done -VORTEX_CURRENT_MAJOR="${VORTEX_CURRENT_MAJOR}" yarn --cwd="${COMBINED_DIR}" run build +VORTEX_DOCS_COMBINED=1 VORTEX_CURRENT_MAJOR="${VORTEX_CURRENT_MAJOR}" yarn --cwd="${COMBINED_DIR}" run build echo "Combined documentation site built at ${COMBINED_DIR}/build" diff --git a/.vortex/docs/content/contributing/maintenance/documentation.mdx b/.vortex/docs/content/contributing/maintenance/documentation.mdx index 066bde812c..d3e3bab8c0 100644 --- a/.vortex/docs/content/contributing/maintenance/documentation.mdx +++ b/.vortex/docs/content/contributing/maintenance/documentation.mdx @@ -100,10 +100,10 @@ To build and browse the combined site locally: cd .vortex # Build both majors into 'docs_combined'. Fails on broken internal links. -ahoy build-combined-docs +ahoy docs-combined # Serve the result. The port defaults to 8000. -ahoy serve-combined-docs 4000 +ahoy docs-combined-serve 4000 ``` Everything the build writes stays inside `docs_combined`, a git-ignored copy of diff --git a/.vortex/docs/docusaurus.config.js b/.vortex/docs/docusaurus.config.js index 3bc46c2090..a5235d9a99 100644 --- a/.vortex/docs/docusaurus.config.js +++ b/.vortex/docs/docusaurus.config.js @@ -4,24 +4,19 @@ // There are various equivalent ways to declare your Docusaurus config. // @see https://docusaurus.io/docs/api/docusaurus-config -import fs from 'node:fs'; - import {themes as prismThemes} from 'prism-react-renderer'; -// Multi-version mode turns on automatically when a 'versioned_docs/' snapshot -// is present: 'versioned_docs/version-1.x' is v1 (the default, served at the -// bare '/docs') and 'content/' is v2 (served at '/docs/v2'). With no snapshot - -// local development and per-branch preview builds - the site builds 'content/' -// as a single unversioned set, so the config never references a version that -// does not exist yet. Only the disposable site that -// 'assemble-combined-docs.sh' writes to 'docs_combined/' carries a snapshot; -// it is never committed to a branch. -const versioned = fs.existsSync('versioned_docs'); +// Serving both majors from one site is opt-in through 'VORTEX_DOCS_COMBINED': +// 'versioned_docs/' then holds the current major at the bare '/docs' and +// 'content/' holds the other major at '/docs/v{other}'. Unset, the site builds +// 'content/' as a single unversioned set and never references a version that +// does not exist. +const versioned = process.env.VORTEX_DOCS_COMBINED === '1'; // The current major (the 'VORTEX_CURRENT_MAJOR' repository variable, default 1) // drives the whole site: its docs are a snapshot under 'versioned_docs/' served -// as the default at the bare '/docs', and 'content/' (replaced with the other -// major's docs in the combined site) is served at '/docs/v{other}'. Bumping +// as the default at the bare '/docs', and the live 'content/' (pulled from the +// other major's '{N}.x' branch in CI) is served at '/docs/v{other}'. Bumping // that one variable promotes a new major - nothing else changes here. const currentMajor = process.env.VORTEX_CURRENT_MAJOR || '1'; const otherMajor = currentMajor === '1' ? '2' : '1'; @@ -81,20 +76,12 @@ const config = { routeBasePath: '/docs', sidebarPath: './sidebars.js', path: 'content', + // Please change this to your repo. // Remove this to remove the "edit this page" links. - // - // Both the snapshot and the assembled docs are built from copies that - // exist only for the build, so the link is composed from the branch - // that actually carries the page: the other major's docs live on its - // '{N}.x' branch, everything else on 'main'. - editUrl: ({version, docPath}) => { - const branch = versioned && version === 'current' ? `${otherMajor}.x` : 'main'; - - return `https://github.com/drevops/vortex/tree/${branch}/.vortex/docs/content/${docPath}`; - }, + editUrl: 'https://github.com/drevops/vortex/tree/main/.vortex/docs/', // In versioned (aggregate) builds the current major is the snapshot // in 'versioned_docs/' served at the bare '/docs' (the default), and - // the assembled docs are the other major at '/docs/v{other}'. Both + // the live 'content/' is the other major at '/docs/v{other}'. Both // are derived from 'VORTEX_CURRENT_MAJOR' - no manual edits to flip. ...(versioned ? { lastVersion: currentDocsVersion, From fb822ca89b722e2c8a568f87e09c8c30ececd928 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Fri, 21 Aug 2026 10:06:40 +1000 Subject: [PATCH 08/12] Built the combined docs site on every branch and moved version resolution out of the config. The docs workflow branched on the branch's major, building either the combined site or a single-version one into different directories. Every branch now builds the combined site, which is what checks links across both majors, leaving one build step and one publish directory. Version resolution moved to 'versions.js', so 'docusaurus.config.js' carries none of it. --- .github/workflows/vortex-test-docs.yml | 26 ++-------- .vortex/.ahoy.yml | 4 +- .../maintenance/documentation.mdx | 4 +- .vortex/docs/docusaurus.config.js | 47 ++----------------- .vortex/docs/versions.js | 33 +++++++++++++ 5 files changed, 45 insertions(+), 69 deletions(-) create mode 100644 .vortex/docs/versions.js diff --git a/.github/workflows/vortex-test-docs.yml b/.github/workflows/vortex-test-docs.yml index dbf6317c8a..34908bd8a9 100644 --- a/.github/workflows/vortex-test-docs.yml +++ b/.github/workflows/vortex-test-docs.yml @@ -25,13 +25,6 @@ jobs: env: CURRENT_MAJOR: ${{ vars.VORTEX_CURRENT_MAJOR || '1' }} OTHER_MAJOR: ${{ (vars.VORTEX_CURRENT_MAJOR || '1') == '1' && '2' || '1' }} - # The major the triggering branch belongs to. Each major ships its own - # binary from its own workflow, so the workflow name identifies the major - # for pushes, branches and fork pull requests alike. - BUILD_MAJOR: ${{ github.event.workflow_run.name == 'Vortex - Test CLI' && '2' || '1' }} - # Where the built site lands. The combined build assembles both majors - # into its own site directory; an other-major branch builds in place. - DOCS_BUILD_DIR: ${{ (github.event.workflow_run.name == 'Vortex - Test CLI' && '2' || '1') == (vars.VORTEX_CURRENT_MAJOR || '1') && '.vortex/docs_combined/build' || '.vortex/docs/build' }} BINARY_ARTIFACT: ${{ github.event.workflow_run.name == 'Vortex - Test CLI' && 'vortex-cli' || 'vortex-installer' }} BINARY_FILE: ${{ github.event.workflow_run.name == 'Vortex - Test CLI' && 'vortex.phar' || 'installer.phar' }} @@ -137,26 +130,15 @@ jobs: cp "vortex-v${OTHER_MAJOR}/.vortex/cli/.build/vortex.phar" ".vortex/docs/static/v${OTHER_MAJOR}/install" cp ".vortex/docs/static/v${CURRENT_MAJOR}/install" .vortex/docs/static/install - # Every current-major build assembles, so a branch is checked against the - # same combined site its merge publishes. It runs after the binaries are - # staged, which the assembly copies across with the rest of 'docs/'. + # Building the combined site is what checks links across both majors, so + # every branch builds it. It runs after the binaries are staged, which the + # assembly copies across with the rest of 'docs/'. - name: Build the combined documentation site - if: env.BUILD_MAJOR == env.CURRENT_MAJOR run: ./docs/.utils/assemble-combined-docs.sh working-directory: '${{ github.workspace }}/.vortex' env: VORTEX_CURRENT_MAJOR: ${{ env.CURRENT_MAJOR }} - # Other-major branches stay single-version previews: their docs reach - # production through the current-major branch, which builds them as - # '/docs/v{other}'. - - name: Build documentation site - if: env.BUILD_MAJOR != env.CURRENT_MAJOR - run: yarn run build - working-directory: '${{ github.workspace }}/.vortex/docs' - env: - VORTEX_CURRENT_MAJOR: ${{ env.CURRENT_MAJOR }} - # This workflow runs via 'workflow_run', where 'github.ref' is always the # default branch, so the action's branch-based production detection cannot # distinguish a PR build from a 'main' build. Drive the decision explicitly @@ -168,7 +150,7 @@ jobs: id: netlify uses: nwtgck/actions-netlify@d22a32a27c918fe470bbc562e984f80ec48c2668 # v4.0 with: - publish-dir: ${{ env.DOCS_BUILD_DIR }} + publish-dir: '.vortex/docs_combined/build' production-deploy: ${{ github.event.workflow_run.head_branch == 'main' }} deploy-message: "Deploy from GitHub Actions" enable-pull-request-comment: false diff --git a/.vortex/.ahoy.yml b/.vortex/.ahoy.yml index 63b0b6b0a8..28a8354d36 100644 --- a/.vortex/.ahoy.yml +++ b/.vortex/.ahoy.yml @@ -40,10 +40,10 @@ commands: cmd: ./docs/.utils/assemble-combined-docs.sh docs-combined-serve: - usage: Serve the built combined site. Pass a port as the first argument, e.g. 'ahoy docs-combined-serve 4000'. + usage: Serve the built combined site on port 4000. Pass a port as the first argument to override. cmd: | [ -d ./docs_combined/build ] || { echo "No combined site found. Run 'ahoy docs-combined' first."; exit 1; } - port="${1:-8000}" + port="${1:-4000}" echo "Serving the combined documentation site at http://localhost:${port}" php -S "localhost:${port}" -t ./docs_combined/build ./docs/.utils/serve-router.php diff --git a/.vortex/docs/content/contributing/maintenance/documentation.mdx b/.vortex/docs/content/contributing/maintenance/documentation.mdx index d3e3bab8c0..0fa9605590 100644 --- a/.vortex/docs/content/contributing/maintenance/documentation.mdx +++ b/.vortex/docs/content/contributing/maintenance/documentation.mdx @@ -102,8 +102,8 @@ cd .vortex # Build both majors into 'docs_combined'. Fails on broken internal links. ahoy docs-combined -# Serve the result. The port defaults to 8000. -ahoy docs-combined-serve 4000 +# Serve the result on port 4000. Pass a port to override. +ahoy docs-combined-serve ``` Everything the build writes stays inside `docs_combined`, a git-ignored copy of diff --git a/.vortex/docs/docusaurus.config.js b/.vortex/docs/docusaurus.config.js index a5235d9a99..c5339d91f9 100644 --- a/.vortex/docs/docusaurus.config.js +++ b/.vortex/docs/docusaurus.config.js @@ -6,22 +6,7 @@ import {themes as prismThemes} from 'prism-react-renderer'; -// Serving both majors from one site is opt-in through 'VORTEX_DOCS_COMBINED': -// 'versioned_docs/' then holds the current major at the bare '/docs' and -// 'content/' holds the other major at '/docs/v{other}'. Unset, the site builds -// 'content/' as a single unversioned set and never references a version that -// does not exist. -const versioned = process.env.VORTEX_DOCS_COMBINED === '1'; - -// The current major (the 'VORTEX_CURRENT_MAJOR' repository variable, default 1) -// drives the whole site: its docs are a snapshot under 'versioned_docs/' served -// as the default at the bare '/docs', and the live 'content/' (pulled from the -// other major's '{N}.x' branch in CI) is served at '/docs/v{other}'. Bumping -// that one variable promotes a new major - nothing else changes here. -const currentMajor = process.env.VORTEX_CURRENT_MAJOR || '1'; -const otherMajor = currentMajor === '1' ? '2' : '1'; -const currentDocsVersion = `${currentMajor}.x`; -const otherIsNewer = Number(otherMajor) > Number(currentMajor); +import {docsVersions, versionNavbarItems, versionRedirects} from './versions.js'; /** @type {import('@docusaurus/types').Config} */ const config = { @@ -79,23 +64,7 @@ const config = { // Please change this to your repo. // Remove this to remove the "edit this page" links. editUrl: 'https://github.com/drevops/vortex/tree/main/.vortex/docs/', - // In versioned (aggregate) builds the current major is the snapshot - // in 'versioned_docs/' served at the bare '/docs' (the default), and - // the live 'content/' is the other major at '/docs/v{other}'. Both - // are derived from 'VORTEX_CURRENT_MAJOR' - no manual edits to flip. - ...(versioned ? { - lastVersion: currentDocsVersion, - versions: { - [currentDocsVersion]: { - label: `v${currentMajor}`, - }, - current: { - label: `v${otherMajor}`, - path: `v${otherMajor}`, - banner: otherIsNewer ? 'unreleased' : 'unmaintained', - }, - }, - } : {}), + ...docsVersions, }, blog: false, theme: { @@ -175,10 +144,7 @@ const config = { position: 'right', title: 'Join us on Slack', }, - ...(versioned ? [{ - type: 'docsVersionDropdown', - position: 'right', - }] : []), + ...versionNavbarItems, { type: 'search', position: 'right', @@ -252,12 +218,7 @@ const config = { '@docusaurus/plugin-client-redirects', { redirects: [ - // The current major is the default at the bare '/docs', so its - // explicit '/docs/v{current}' path redirects there. - ...(versioned ? [{ - from: `/docs/v${currentMajor}`, - to: '/docs', - }] : []), + ...versionRedirects, { from: ['/quickstart'], to: '/docs', diff --git a/.vortex/docs/versions.js b/.vortex/docs/versions.js new file mode 100644 index 0000000000..4c249fc62f --- /dev/null +++ b/.vortex/docs/versions.js @@ -0,0 +1,33 @@ +// Documentation version resolution. +// +// 'VORTEX_CURRENT_MAJOR' names the major served as the default version at the +// bare '/docs'; the other major is served at '/docs/v{other}'. Bumping it +// promotes a new major. 'VORTEX_DOCS_COMBINED' opts into serving both majors +// from one build - unset, the site is a single unversioned set and neither +// export contributes anything. + +const combined = process.env.VORTEX_DOCS_COMBINED === '1'; +const currentMajor = process.env.VORTEX_CURRENT_MAJOR || '1'; +const otherMajor = currentMajor === '1' ? '2' : '1'; +const currentVersion = `${currentMajor}.x`; + +/** Version options for the docs preset. */ +export const docsVersions = combined ? { + lastVersion: currentVersion, + versions: { + [currentVersion]: { + label: `v${currentMajor}`, + }, + current: { + label: `v${otherMajor}`, + path: `v${otherMajor}`, + banner: Number(otherMajor) > Number(currentMajor) ? 'unreleased' : 'unmaintained', + }, + }, +} : {}; + +/** Navbar items for the version switcher. */ +export const versionNavbarItems = combined ? [{type: 'docsVersionDropdown', position: 'right'}] : []; + +/** Redirects from the current major's explicit path to the default it is served at. */ +export const versionRedirects = combined ? [{from: `/docs/v${currentMajor}`, to: '/docs'}] : []; From ae3ac8f292a28c8969aa6f56004be2c42f9ec6d5 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Fri, 21 Aug 2026 10:21:45 +1000 Subject: [PATCH 09/12] Re-triggered CI to clear a flaky check. From 4147545b210af0bbd81dafc071cf8be1cb01132f Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Fri, 21 Aug 2026 10:26:24 +1000 Subject: [PATCH 10/12] Addressed code review: decoded served paths with 'rawurldecode()'. --- .vortex/docs/.utils/serve-router.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vortex/docs/.utils/serve-router.php b/.vortex/docs/.utils/serve-router.php index 5e94c98f63..288b1f5a8a 100644 --- a/.vortex/docs/.utils/serve-router.php +++ b/.vortex/docs/.utils/serve-router.php @@ -17,7 +17,7 @@ $doc_root = $_SERVER['DOCUMENT_ROOT']; $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); -$path = is_string($path) ? urldecode($path) : '/'; +$path = is_string($path) ? rawurldecode($path) : '/'; // Reject traversal outside the build directory before touching the filesystem. if (str_contains($path, '..')) { From 50c685b29146be244ecfc977aa87c46ddccbe949 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Fri, 21 Aug 2026 10:45:16 +1000 Subject: [PATCH 11/12] Sourced each major's documentation from the branch that ships it. The assembly always treated the checkout as the current major, so a branch off '2.x' was snapshotted as v1 and its own edits were never checked at the path they are served from. The checkout now supplies the major it ships, given by 'VORTEX_DOCS_MAJOR', and the other major is read from its branch: 'main' carries the current major and every other major lives on its '{N}.x' branch. --- .github/workflows/vortex-test-docs.yml | 5 + .vortex/docs/.utils/assemble-combined-docs.sh | 122 ++++++++++++------ 2 files changed, 85 insertions(+), 42 deletions(-) diff --git a/.github/workflows/vortex-test-docs.yml b/.github/workflows/vortex-test-docs.yml index 34908bd8a9..5f1bb771f6 100644 --- a/.github/workflows/vortex-test-docs.yml +++ b/.github/workflows/vortex-test-docs.yml @@ -25,6 +25,10 @@ jobs: env: CURRENT_MAJOR: ${{ vars.VORTEX_CURRENT_MAJOR || '1' }} OTHER_MAJOR: ${{ (vars.VORTEX_CURRENT_MAJOR || '1') == '1' && '2' || '1' }} + # The major the triggering commit ships. Each major builds its own binary + # from its own workflow, so the workflow name identifies the major for + # pushes, branches and fork pull requests alike. + DOCS_MAJOR: ${{ github.event.workflow_run.name == 'Vortex - Test CLI' && '2' || '1' }} BINARY_ARTIFACT: ${{ github.event.workflow_run.name == 'Vortex - Test CLI' && 'vortex-cli' || 'vortex-installer' }} BINARY_FILE: ${{ github.event.workflow_run.name == 'Vortex - Test CLI' && 'vortex.phar' || 'installer.phar' }} @@ -138,6 +142,7 @@ jobs: working-directory: '${{ github.workspace }}/.vortex' env: VORTEX_CURRENT_MAJOR: ${{ env.CURRENT_MAJOR }} + VORTEX_DOCS_MAJOR: ${{ env.DOCS_MAJOR }} # This workflow runs via 'workflow_run', where 'github.ref' is always the # default branch, so the action's branch-based production detection cannot diff --git a/.vortex/docs/.utils/assemble-combined-docs.sh b/.vortex/docs/.utils/assemble-combined-docs.sh index b82c4fdb52..a39a8fe43f 100755 --- a/.vortex/docs/.utils/assemble-combined-docs.sh +++ b/.vortex/docs/.utils/assemble-combined-docs.sh @@ -2,10 +2,15 @@ ## # Assemble the combined documentation site, serving both majors from one build. # -# Produces a complete, self-contained Docusaurus site in 'docs_combined/': this -# branch's documentation as the default version at '/docs', and the other -# major's '{N}.x' branch documentation at '/docs/v{N}' with its own assets at -# '/v{N}'. Everything the assembly writes stays inside 'docs_combined/', which +# Produces a complete, self-contained Docusaurus site in 'docs_combined/'. The +# current major is served at '/docs' with its assets at '/img'; the other major +# is served at '/docs/v{other}' with its assets at '/v{other}/img'. +# +# This checkout supplies the documentation for the major it ships +# ('VORTEX_DOCS_MAJOR'); the other major is read from its own branch. So a +# branch off 'main' is built as the current major and '{other}.x' is fetched, +# while a branch off '{other}.x' is built as the other major and 'main' is +# fetched. Everything the assembly writes stays inside 'docs_combined/', which # is disposable - 'docs/' is only ever read. # # @usage @@ -15,34 +20,84 @@ set -eu set -o pipefail [ "${VORTEX_DEBUG-}" = "1" ] && set -x -# The major this branch ships. Its docs become the site's default version. +# The major served as the site's default version at the bare '/docs'. VORTEX_CURRENT_MAJOR="${VORTEX_CURRENT_MAJOR:-1}" +# The major this checkout ships. Defaults to the current major, which is the +# major that lives on the default branch. +VORTEX_DOCS_MAJOR="${VORTEX_DOCS_MAJOR:-${VORTEX_CURRENT_MAJOR}}" + ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../" && pwd)" DOCS_DIR="${ROOT_DIR}/.vortex/docs" COMBINED_DIR="${ROOT_DIR}/.vortex/docs_combined" sed_opts=(-i) && [ "$(uname)" = "Darwin" ] && sed_opts=(-i '') +for major in "${VORTEX_CURRENT_MAJOR}" "${VORTEX_DOCS_MAJOR}"; do + case "${major}" in + 1 | 2) ;; + *) + echo "ERROR: Invalid major '${major}'. Expected 1 or 2." >&2 + exit 1 + ;; + esac +done + case "${VORTEX_CURRENT_MAJOR}" in 1) other_major=2 ;; 2) other_major=1 ;; - *) - echo "ERROR: Invalid VORTEX_CURRENT_MAJOR='${VORTEX_CURRENT_MAJOR}'. Expected 1 or 2." >&2 - exit 1 - ;; esac -git -C "${ROOT_DIR}" fetch origin "${other_major}.x" --depth=1 || { - echo "ERROR: Failed to fetch the ${other_major}.x branch." >&2 +# The current major lives on the default branch; every other major lives on its +# own '{N}.x' branch. Whichever side this checkout does not supply is fetched. +if [ "${VORTEX_DOCS_MAJOR}" = "${VORTEX_CURRENT_MAJOR}" ]; then + default_ref="" + other_ref="origin/${other_major}.x" +else + default_ref="origin/main" + other_ref="" +fi + +fetch_ref="${default_ref}${other_ref}" +fetch_branch="${fetch_ref#origin/}" + +git -C "${ROOT_DIR}" fetch origin "${fetch_branch}" --depth=1 || { + echo "ERROR: Failed to fetch the ${fetch_branch} branch." >&2 exit 1 } -git -C "${ROOT_DIR}" rev-parse --verify "origin/${other_major}.x" >/dev/null || { - echo "ERROR: The ${other_major}.x branch does not exist." >&2 +git -C "${ROOT_DIR}" rev-parse --verify "${fetch_ref}" >/dev/null || { + echo "ERROR: The ${fetch_branch} branch does not exist." >&2 exit 1 } +## +# Replace a directory with a major's documentation tree. +# +# $1 - directory to populate. +# $2 - subdirectory of the documentation to read ('content' or 'static'). +# $3 - git ref to read from, or empty to read this checkout. +## +populate() { + local dest="${1}" + local tree="${2}" + local ref="${3}" + + rm -rf "${dest}" + mkdir -p "${dest}" + + if [ -z "${ref}" ]; then + cp -R "${DOCS_DIR}/${tree}/." "${dest}/" + else + git -C "${ROOT_DIR}" archive "${ref}:.vortex/docs/${tree}" | tar -x -C "${dest}" + fi + + [ -n "$(ls -A "${dest}")" ] || { + echo "ERROR: No '${tree}' found in ${ref:-this checkout}." >&2 + exit 1 + } +} + rm -rf "${COMBINED_DIR}" mkdir -p "${COMBINED_DIR}" @@ -59,39 +114,22 @@ rsync -a \ yarn --cwd="${COMBINED_DIR}" install --frozen-lockfile -# Snapshot this branch as the default version before 'content/' is handed over -# to the other major. 'VORTEX_DOCS_COMBINED' stays unset here: the snapshot the -# combined config expects does not exist until this command creates it. -yarn --cwd="${COMBINED_DIR}" docusaurus docs:version "${VORTEX_CURRENT_MAJOR}.x" - -rm -rf "${COMBINED_DIR}/content" -mkdir -p "${COMBINED_DIR}/content" - -git -C "${ROOT_DIR}" archive "origin/${other_major}.x:.vortex/docs/content" | tar -x -C "${COMBINED_DIR}/content" || { - echo "ERROR: Failed to extract content from ${other_major}.x." >&2 - exit 1 -} +# Snapshot the current major, whose assets stay at the bare static root. +populate "${COMBINED_DIR}/content" content "${default_ref}" +populate "${COMBINED_DIR}/static" static "${default_ref}" -[ -n "$(ls -A "${COMBINED_DIR}/content")" ] || { - echo "ERROR: The ${other_major}.x branch carries no documentation content." >&2 - exit 1 -} +# 'VORTEX_DOCS_COMBINED' stays unset here: the snapshot the combined config +# expects does not exist until this command creates it. +yarn --cwd="${COMBINED_DIR}" docusaurus docs:version "${VORTEX_CURRENT_MAJOR}.x" -# Both majors record their own demo videos and diagrams under the same -# 'static/img' names, so the other major's assets get their own '/v{other}' -# prefix instead of losing to this major's copies. +# Hand 'content/' over to the other major. Both majors record their own demo +# videos and diagrams under the same 'static/img' names, so the other major's +# assets get their own '/v{other}' prefix instead of losing to the current +# major's copies. other_static_dir="${COMBINED_DIR}/static/v${other_major}" -mkdir -p "${other_static_dir}" - -git -C "${ROOT_DIR}" archive "origin/${other_major}.x:.vortex/docs/static" | tar -x -C "${other_static_dir}" || { - echo "ERROR: Failed to extract static assets from ${other_major}.x." >&2 - exit 1 -} -[ -n "$(ls -A "${other_static_dir}")" ] || { - echo "ERROR: The ${other_major}.x branch carries no static assets." >&2 - exit 1 -} +populate "${COMBINED_DIR}/content" content "${other_ref}" +populate "${other_static_dir}" static "${other_ref}" # Every branch authors its docs against the bare '/docs' mount, so an absolute # link lands on the current major once the content is served at '/docs/v{other}'. From 7b2e9c43d703ccf6376504a66b0e4a72042bbad4 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Fri, 21 Aug 2026 11:08:37 +1000 Subject: [PATCH 12/12] Addressed code review: kept the other major's binary through the static swap. The other major's binary is built into 'static/v{other}' before the assembly runs and is tracked on no branch, so replacing that directory with the branch's own static tree left '/v{other}/install' missing from the published site. --- .vortex/docs/.utils/assemble-combined-docs.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.vortex/docs/.utils/assemble-combined-docs.sh b/.vortex/docs/.utils/assemble-combined-docs.sh index a39a8fe43f..95776f8952 100755 --- a/.vortex/docs/.utils/assemble-combined-docs.sh +++ b/.vortex/docs/.utils/assemble-combined-docs.sh @@ -128,9 +128,17 @@ yarn --cwd="${COMBINED_DIR}" docusaurus docs:version "${VORTEX_CURRENT_MAJOR}.x" # major's copies. other_static_dir="${COMBINED_DIR}/static/v${other_major}" +# The other major's binary is built into 'static/v{other}' before the assembly +# runs and is not tracked on any branch, so replacing that directory with the +# branch's own static tree would discard it. +staged_other_install="${DOCS_DIR}/static/v${other_major}/install" +[ -f "${staged_other_install}" ] || staged_other_install="" + populate "${COMBINED_DIR}/content" content "${other_ref}" populate "${other_static_dir}" static "${other_ref}" +[ -z "${staged_other_install}" ] || cp "${staged_other_install}" "${other_static_dir}/install" + # Every branch authors its docs against the bare '/docs' mount, so an absolute # link lands on the current major once the content is served at '/docs/v{other}'. # Re-point those links at the major they were written for. A link that already