diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 99c33c45d..c307f4cfe 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -526,6 +526,56 @@ jobs:
- name: Build docs
uses: ./.github/actions/docs-build
+ release-line-docs:
+ name: Release-line Docs Snapshot
+ needs: changes
+ if: github.event_name == 'pull_request' && startsWith(github.base_ref, 'release-') && needs.changes.outputs.docs == 'true'
+ runs-on: *runner
+ steps:
+ - name: Checkout
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ fetch-depth: 0
+
+ - name: Verify release-line docs snapshot changed
+ env:
+ BASE_REF: ${{ github.base_ref }}
+ BASE_SHA: ${{ github.event.pull_request.base.sha }}
+ HEAD_SHA: ${{ github.event.pull_request.head.sha }}
+ run: |
+ set -euo pipefail
+
+ if [[ ! "${BASE_REF}" =~ ^release-([0-9]+)\.([0-9]+)$ ]]; then
+ echo "Release-line docs check only supports release-X.Y branches; got ${BASE_REF}" >&2
+ exit 1
+ fi
+
+ docs_version="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.0"
+ git fetch --no-tags --prune --depth=1 origin "${BASE_SHA}" || true
+
+ changed="$(git diff --name-only "${BASE_SHA}" "${HEAD_SHA}")"
+ docs_changed="$(grep -E '^(docs/|website/sidebars\.ts$)' <<< "${changed}" || true)"
+ if [[ -z "${docs_changed}" ]]; then
+ echo "No source docs changed."
+ exit 0
+ fi
+
+ snapshot_changed="$(
+ grep -E "^website/versioned_docs/version-${docs_version//./\\.}/|^website/versioned_sidebars/version-${docs_version//./\\.}-sidebars\\.json$|^website/versions\\.json$" <<< "${changed}" || true
+ )"
+ if [[ -n "${snapshot_changed}" ]]; then
+ echo "Release-line docs snapshot changed for ${docs_version}."
+ exit 0
+ fi
+
+ {
+ echo "PRs against ${BASE_REF} that change source docs must refresh the ${docs_version} release-line docs snapshot."
+ echo
+ echo "Run from the release branch and commit the generated files:"
+ echo " make docs-refresh-version DOCS_VERSION=${docs_version}"
+ } >&2
+ exit 1
+
helm:
name: Helm Chart
needs: changes
@@ -1008,6 +1058,7 @@ jobs:
- fuzz
- openbao-config-compat
- docs
+ - release-line-docs
- helm
- helm-e2e-smoke
- build-artifacts
@@ -1032,6 +1083,7 @@ jobs:
FUZZ: ${{ needs.fuzz.result }}
OPENBAO_CONFIG_COMPAT: ${{ needs.openbao-config-compat.result }}
DOCS: ${{ needs.docs.result }}
+ RELEASE_LINE_DOCS: ${{ needs.release-line-docs.result }}
HELM: ${{ needs.helm.result }}
HELM_E2E_SMOKE: ${{ needs.helm-e2e-smoke.result }}
BUILD_ARTIFACTS: ${{ needs.build-artifacts.result }}
@@ -1066,6 +1118,7 @@ jobs:
fuzz=${FUZZ}
openbao-config-compat=${OPENBAO_CONFIG_COMPAT}
docs=${DOCS}
+ release-line-docs=${RELEASE_LINE_DOCS}
helm=${HELM}
helm-e2e-smoke=${HELM_E2E_SMOKE}
build-artifacts=${BUILD_ARTIFACTS}
diff --git a/docs/contribute/release-management.md b/docs/contribute/release-management.md
index 99798b1f2..e3978ac09 100644
--- a/docs/contribute/release-management.md
+++ b/docs/contribute/release-management.md
@@ -79,7 +79,7 @@ journey: contribute
-Before merging the first stable `X.Y.0` release PR for a release line, snapshot the docs for that release line and commit the generated artifacts. Patch releases in the same line publish release notes and reuse the `X.Y.0` docs snapshot. Prereleases continue to use `/docs/next` and release notes only; do not add patch, `-alpha`, `-beta`, or `-rc` entries to `website/versions.json`.
+Before merging the first stable `X.Y.0` release PR for a release line, snapshot the docs for that release line and commit the generated artifacts. Patch releases in the same line reuse the `X.Y.0` docs version, but user-facing docs fixes for that patch must refresh the existing `X.Y.0` snapshot from the release branch. Prereleases continue to use `/docs/next` and release notes only; do not add patch, `-alpha`, `-beta`, or `-rc` entries to `website/versions.json`.
@@ -98,6 +98,16 @@ GitHub Actions runs workflow definitions from the branch that receives the push.
This updates `website/versioned_docs/`, `website/versioned_sidebars/`, and `website/versions.json`.
+
+ Run this from the release branch after backporting docs that apply to the patch release. This updates the existing release-line docs snapshot without adding a patch version to `website/versions.json`.
+
+
.
+ @test -n "$(DOCS_VERSION)" || { echo "DOCS_VERSION is required, for example: make docs-refresh-version DOCS_VERSION=1.2.0"; exit 1; }
+ @$(DOCS_NPM) --prefix "$(DOCS_DIR)" run refresh:docs-version -- "$(DOCS_VERSION)"
+
# TODO(user): To use a different vendor for e2e tests, modify the setup under 'tests/e2e'.
# The default setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally.
# CertManager is installed by default; skip with:
diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts
index b444578c4..8b534eb75 100644
--- a/website/docusaurus.config.ts
+++ b/website/docusaurus.config.ts
@@ -65,6 +65,12 @@ const config: Config = {
label: 'next',
path: 'next',
},
+ '0.2.0': {
+ label: '0.2.x',
+ },
+ '0.1.0': {
+ label: '0.1.x',
+ },
},
},
blog: false,
diff --git a/website/package.json b/website/package.json
index ca8768fa8..787705f39 100644
--- a/website/package.json
+++ b/website/package.json
@@ -16,6 +16,7 @@
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids",
"version:docs": "node ./scripts/snapshot-version.mjs",
+ "refresh:docs-version": "node ./scripts/refresh-version.mjs",
"verify:docs-version": "node ./scripts/verify-docs-version.mjs",
"typecheck": "tsc --noEmit",
"test:e2e": "playwright test"
diff --git a/website/scripts/refresh-version.mjs b/website/scripts/refresh-version.mjs
new file mode 100644
index 000000000..3eaf84392
--- /dev/null
+++ b/website/scripts/refresh-version.mjs
@@ -0,0 +1,121 @@
+import {spawnSync} from 'node:child_process';
+import fs from 'node:fs/promises';
+import path from 'node:path';
+
+const version = process.argv[2];
+const websiteRoot = process.cwd();
+const versionsPath = path.join(websiteRoot, 'versions.json');
+
+if (!version) {
+ console.error('Usage: npm run refresh:docs-version -- ');
+ process.exit(1);
+}
+
+function isStableLineVersion(candidate) {
+ return /^\d+\.\d+\.0$/.test(candidate);
+}
+
+function run(command, args) {
+ const result = spawnSync(command, args, {
+ cwd: websiteRoot,
+ stdio: 'inherit',
+ });
+
+ if (result.status !== 0) {
+ throw new Error(`${[command, ...args].join(' ')} failed with status ${result.status ?? 1}`);
+ }
+}
+
+async function pathExists(target) {
+ try {
+ await fs.access(target);
+ return true;
+ } catch (error) {
+ if (error?.code === 'ENOENT') {
+ return false;
+ }
+ throw error;
+ }
+}
+
+async function moveIfExists(from, to) {
+ if (await pathExists(from)) {
+ await fs.mkdir(path.dirname(to), {recursive: true});
+ await fs.rename(from, to);
+ return true;
+ }
+ return false;
+}
+
+async function restoreIfMoved(from, to, moved) {
+ if (!moved) {
+ return;
+ }
+ await fs.rm(to, {recursive: true, force: true});
+ await fs.mkdir(path.dirname(to), {recursive: true});
+ await fs.rename(from, to);
+}
+
+if (!isStableLineVersion(version)) {
+ console.error(
+ `Release-line docs refresh only supports stable release-line versions (X.Y.0). Patch releases update the existing release-line snapshot: ${version}`,
+ );
+ process.exit(1);
+}
+
+const raw = await fs.readFile(versionsPath, 'utf8');
+const originalVersions = JSON.parse(raw);
+
+if (!Array.isArray(originalVersions)) {
+ throw new Error('versions.json is not an array');
+}
+
+const dedupedOriginalVersions = [...new Set(originalVersions)];
+if (!dedupedOriginalVersions.includes(version)) {
+ console.error(
+ `Docs version ${version} is not present in versions.json. Create the release-line snapshot first with: make docs-version DOCS_VERSION=${version}`,
+ );
+ process.exit(1);
+}
+
+const tempRoot = path.join(websiteRoot, `.tmp-refresh-version-${process.pid}`);
+const versionedDocsDir = path.join(websiteRoot, 'versioned_docs', `version-${version}`);
+const versionedSidebarPath = path.join(
+ websiteRoot,
+ 'versioned_sidebars',
+ `version-${version}-sidebars.json`,
+);
+const backupDocsDir = path.join(tempRoot, 'versioned_docs', `version-${version}`);
+const backupSidebarPath = path.join(
+ tempRoot,
+ 'versioned_sidebars',
+ `version-${version}-sidebars.json`,
+);
+
+let movedDocs = false;
+let movedSidebar = false;
+
+try {
+ await fs.rm(tempRoot, {recursive: true, force: true});
+ movedDocs = await moveIfExists(versionedDocsDir, backupDocsDir);
+ movedSidebar = await moveIfExists(versionedSidebarPath, backupSidebarPath);
+ await fs.writeFile(
+ versionsPath,
+ `${JSON.stringify(dedupedOriginalVersions.filter((candidate) => candidate !== version), null, 2)}\n`,
+ );
+
+ run(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'prepare:contribute']);
+ run(process.platform === 'win32' ? 'npx.cmd' : 'npx', ['docusaurus', 'docs:version', version]);
+
+ await fs.writeFile(versionsPath, `${JSON.stringify(dedupedOriginalVersions, null, 2)}\n`);
+ await fs.rm(tempRoot, {recursive: true, force: true});
+} catch (error) {
+ await fs.rm(versionedDocsDir, {recursive: true, force: true});
+ await fs.rm(versionedSidebarPath, {force: true});
+ await restoreIfMoved(backupDocsDir, versionedDocsDir, movedDocs);
+ await restoreIfMoved(backupSidebarPath, versionedSidebarPath, movedSidebar);
+ await fs.writeFile(versionsPath, `${JSON.stringify(dedupedOriginalVersions, null, 2)}\n`);
+ await fs.rm(tempRoot, {recursive: true, force: true});
+ console.error(error.message);
+ process.exit(1);
+}
diff --git a/website/scripts/snapshot-version.mjs b/website/scripts/snapshot-version.mjs
index 2f2f564c9..08faff290 100644
--- a/website/scripts/snapshot-version.mjs
+++ b/website/scripts/snapshot-version.mjs
@@ -33,6 +33,18 @@ async function dedupeVersions() {
await fs.writeFile(versionsPath, `${JSON.stringify(deduped, null, 2)}\n`);
}
+const prepareResult = spawnSync(
+ process.platform === 'win32' ? 'npm.cmd' : 'npm',
+ ['run', 'prepare:contribute'],
+ {
+ stdio: 'inherit',
+ },
+);
+
+if (prepareResult.status !== 0) {
+ process.exit(prepareResult.status ?? 1);
+}
+
const result = spawnSync(
process.platform === 'win32' ? 'npx.cmd' : 'npx',
['docusaurus', 'docs:version', version],
diff --git a/website/tests/behavior.spec.ts b/website/tests/behavior.spec.ts
index 70cab5685..b338105b8 100644
--- a/website/tests/behavior.spec.ts
+++ b/website/tests/behavior.spec.ts
@@ -42,7 +42,7 @@ test('version dropdown switches from next docs to the stable release line', asyn
await versionDropdown.hover();
const archivedRelease = page.locator('.dropdown__menu').getByRole('link', {
- name: '0.2.0',
+ name: '0.2.x',
exact: true,
});
await expect(archivedRelease).toBeVisible();
@@ -50,7 +50,7 @@ test('version dropdown switches from next docs to the stable release line', asyn
await expect(page).toHaveURL(/\/openbao-operator\/docs\/get-started\/deployment-decision-guide$/);
await expect(page.getByText('Published release documentation')).toBeVisible();
- await expect(page.getByText('Version: 0.2.0')).toBeVisible();
+ await expect(page.getByText('Version: 0.2.x')).toBeVisible();
});
test.describe('curated legacy redirects stay alive', () => {
diff --git a/website/tests/smoke.spec.ts b/website/tests/smoke.spec.ts
index 6b415759f..24dc37462 100644
--- a/website/tests/smoke.spec.ts
+++ b/website/tests/smoke.spec.ts
@@ -46,7 +46,7 @@ test('stable docs expose the current release banner', async ({page}) => {
await expect(page.getByRole('heading', {name: 'OpenBao Operator documentation'})).toBeVisible();
await expect(page.getByText('Published release documentation')).toBeVisible();
- await expect(page.getByText('Version: 0.2.0')).toBeVisible();
+ await expect(page.getByText('Version: 0.2.x')).toBeVisible();
});
test('architecture section exposes grouped local navigation', async ({page}) => {
diff --git a/website/versioned_docs/version-0.2.0/contribute/release-management.md b/website/versioned_docs/version-0.2.0/contribute/release-management.md
index d5db90a62..7ce9ee8ff 100644
--- a/website/versioned_docs/version-0.2.0/contribute/release-management.md
+++ b/website/versioned_docs/version-0.2.0/contribute/release-management.md
@@ -71,7 +71,7 @@ journey: contribute
-Before merging the first stable `X.Y.0` release PR for a release line, snapshot the docs for that release line and commit the generated artifacts. Patch releases in the same line publish release notes and reuse the `X.Y.0` docs snapshot. Prereleases continue to use `/docs/next` and release notes only; do not add patch, `-alpha`, `-beta`, or `-rc` entries to `website/versions.json`.
+Before merging the first stable `X.Y.0` release PR for a release line, snapshot the docs for that release line and commit the generated artifacts. Patch releases in the same line reuse the `X.Y.0` docs version, but user-facing docs fixes for that patch must refresh the existing `X.Y.0` snapshot from the release branch. Prereleases continue to use `/docs/next` and release notes only; do not add patch, `-alpha`, `-beta`, or `-rc` entries to `website/versions.json`.
@@ -84,6 +84,16 @@ Before merging the first stable `X.Y.0` release PR for a release line, snapshot
This updates `website/versioned_docs/`, `website/versioned_sidebars/`, and `website/versions.json`.
+
+ Run this from the release branch after backporting docs that apply to the patch release. This updates the existing release-line docs snapshot without adding a patch version to `website/versions.json`.
+
+
|
| `autoDownload` _boolean_ | AutoDownload controls automatic plugin downloads from OCI registries. | | Optional: \{\}
|
| `autoRegister` _boolean_ | AutoRegister controls automatic plugin registration. | | Optional: \{\}
|
-| `downloadBehavior` _string_ | DownloadBehavior specifies how plugins are downloaded. | | Enum: [standard direct]
Optional: \{\}
|
+| `downloadBehavior` _string_ | DownloadBehavior controls whether OpenBao startup fails or continues when
declarative OCI plugin downloads fail. Valid values are "fail" and
"continue"; OpenBao defaults to "fail" when unset. | | Enum: [fail continue]
Optional: \{\}
|
#### PodMetadataConfig
diff --git a/website/versioned_docs/version-0.2.0/user-guide/openbaocluster/configuration/server.md b/website/versioned_docs/version-0.2.0/user-guide/openbaocluster/configuration/server.md
index 70469079b..5505ddbe9 100644
--- a/website/versioned_docs/version-0.2.0/user-guide/openbaocluster/configuration/server.md
+++ b/website/versioned_docs/version-0.2.0/user-guide/openbaocluster/configuration/server.md
@@ -145,16 +145,77 @@ description: Configure server-runtime defaults such as UI, listener behavior, au
configuration:
plugin:
autoDownload: true
- downloadBehavior: "direct"
+ downloadBehavior: "continue"
plugins:
- type: secret
name: aws
image: "ghcr.io/openbao/openbao-plugin-secrets-aws"
- version: "v1.0.0"
+ version: "v0.0.1"
binaryName: "openbao-plugin-secrets-aws"
+ sha256sum: "b98cb1cbfd0f567d7b614efb0621aaba10c4deda865f5e5b3d155609ada2482e"`}
+>
+ Use an `image` plugin when OpenBao should download the plugin from an OCI registry as part of server startup. The operator renders `plugin_directory = "/openbao/plugins"` and mounts a writable, pod-local volume at that path for OCI auto-download.
+
+
+
+ Use a `command` plugin when the binary is already available inside the OpenBao runtime image or another explicitly managed runtime path.
+
+
+
+
+
+OCI-downloaded plugins are stored under `/openbao/plugins` on an ephemeral pod-local volume. Treat that directory as a writable startup cache, not durable storage. If the cluster runs in a private or disconnected environment, mirror the plugin image and make sure OpenBao's runtime OCI client can authenticate to that registry; Kubernetes `imagePullSecrets` only cover Kubernetes image pulls.
+
+
+