diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3008946e8..a95b44cc8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -522,6 +522,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 @@ -1004,6 +1054,7 @@ jobs: - fuzz - openbao-config-compat - docs + - release-line-docs - helm - helm-e2e-smoke - build-artifacts @@ -1028,6 +1079,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 }} @@ -1062,6 +1114,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..4fa19275b 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -1,3 +1,4 @@ +import fs from 'node:fs'; import {themes as prismThemes} from 'prism-react-renderer'; import type {Config} from '@docusaurus/types'; import type * as Preset from '@docusaurus/preset-classic'; @@ -11,6 +12,26 @@ const docsPluginDefaultExclude = [ '**/*.test.{js,jsx,ts,tsx}', '**/__tests__/**', ]; +const releaseLineVersionLabels = { + '0.2.0': '0.2.x', + '0.1.0': '0.1.x', +} as const; + +function readDocsVersions(): string[] { + try { + const versions = JSON.parse(fs.readFileSync(new URL('./versions.json', import.meta.url), 'utf8')); + return Array.isArray(versions) ? versions : []; + } catch { + return []; + } +} + +const docsVersions = new Set(readDocsVersions()); +const releaseLineVersions = Object.fromEntries( + Object.entries(releaseLineVersionLabels) + .filter(([version]) => docsVersions.has(version)) + .map(([version, label]) => [version, {label}]), +); const config: Config = { title: 'OpenBao Operator', @@ -65,6 +86,7 @@ const config: Config = { label: 'next', path: 'next', }, + ...releaseLineVersions, }, }, blog: false, diff --git a/website/package.json b/website/package.json index 86f6a2dfa..b71000f09 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. + + +