diff --git a/.github/upstream-projects.yaml b/.github/upstream-projects.yaml index eaea19bf..0419bfc2 100644 --- a/.github/upstream-projects.yaml +++ b/.github/upstream-projects.yaml @@ -147,8 +147,25 @@ projects: - source: enterprise/ai-gateway/docs/operator-guide/management-api.openapi.yaml destination: static/api-specs/ai-gateway-management-api.yaml public_sources: false + helm_version_source: enterprise/stacklok-enterprise-platform/helm/Chart.yaml helm: - source: enterprise/stacklok-enterprise-platform/helm + release_overrides: + - since: v0.26.0 + description_overrides: + workspace.production: >- + Enforce production requirements for Workspace, including external + PostgreSQL and object storage, credentials, and resource limits. + workspace.image.repository: Container image repository for Workspace. + workspace.postgres.enabled: >- + Deploy the development PostgreSQL instance for Workspace. + Requires workspace.production to be false. + workspace.objectStore.enabled: >- + Deploy the development MinIO object store for Workspace. + Requires workspace.production to be false. + workspace.networkPolicy.cloudUI.enabled: >- + Allow console pods to reach Workspace when + workspace.networkPolicy.enabled is true. page: docs/platform/reference/helm/platform.mdx title: Platform Helm values description: Values, types, and defaults for the Stacklok Enterprise platform chart. @@ -209,6 +226,7 @@ projects: - label: Deploy the platform href: ../../enterprise-platform/deployment.mdx - source: enterprise/ai-gateway/helm/operator + before: v0.26.0 page: docs/platform/reference/helm/ai-gateway-operator.mdx title: Legacy AI Gateway operator Helm values description: Values, types, and defaults for the legacy AI Gateway operator chart in Stacklok Enterprise. @@ -219,6 +237,7 @@ projects: - label: Deploy the platform href: ../../enterprise-platform/deployment.mdx - source: enterprise/ai-gateway/helm/operator-crds + before: v0.26.0 page: docs/platform/reference/helm/ai-gateway-operator-crds.mdx title: Legacy AI Gateway operator CRD Helm values description: Values, types, and defaults for the legacy AI Gateway operator CRD chart in Stacklok Enterprise. @@ -239,6 +258,9 @@ projects: - label: Deploy the platform href: ../../enterprise-platform/deployment.mdx - source: enterprise/sprout/helm + release_overrides: + - since: v0.26.0 + source: enterprise/ai-gateway/helm page: docs/platform/reference/helm/ai-gateway.mdx title: AI Gateway Helm values description: Values, types, and defaults for the AI Gateway chart in Stacklok Enterprise. diff --git a/scripts/upstream-release/generate-helm-reference.mjs b/scripts/upstream-release/generate-helm-reference.mjs index debc3890..22747b56 100644 --- a/scripts/upstream-release/generate-helm-reference.mjs +++ b/scripts/upstream-release/generate-helm-reference.mjs @@ -15,6 +15,68 @@ import yaml from 'yaml'; const repoRoot = fileURLToPath(new URL('../../', import.meta.url)); +// Select chart paths from the source release's version, including SHA-based exports. +export function resolveHelmCharts(project, clone) { + const charts = project.helm || []; + if (!project.helm_version_source) return charts; + const metadata = yaml.parse( + fs.readFileSync(path.join(clone, project.helm_version_source), 'utf8') + ); + const version = (value) => { + const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?$/.exec(value); + if (!match) throw new Error(`Invalid Helm release version: ${value}`); + return match.slice(1, 4).map(Number); + }; + const current = version(metadata.version); + const atLeast = (value) => { + const target = version(value); + for (let i = 0; i < 3; i++) { + if (current[i] !== target[i]) return current[i] > target[i]; + } + return true; + }; + return charts + .filter((chart) => !chart.before || !atLeast(chart.before)) + .map((chart) => { + const overrides = (chart.release_overrides || []) + .filter((override) => atLeast(override.since)) + .sort((a, b) => { + const left = version(a.since); + const right = version(b.since); + return left[0] - right[0] || left[1] - right[1] || left[2] - right[2]; + }); + return overrides.reduce( + (resolved, override) => ({ + ...resolved, + ...override, + description_overrides: { + ...resolved.description_overrides, + ...override.description_overrides, + }, + }), + chart + ); + }); +} + +export function validateHelmInputs(charts, clone) { + const missing = charts.flatMap((chart) => + ['Chart.yaml', 'values.yaml'] + .map((file) => path.join(chart.source, file)) + .filter((file) => { + const input = path.join(clone, file); + return ( + !fs.existsSync(input) || + !fs.statSync(input).isFile() || + !fs.statSync(input).size + ); + }) + ); + if (missing.length) { + throw new Error(`Missing release Helm inputs:\n${missing.join('\n')}`); + } +} + // Preserve inline code while escaping MDX expressions and JSX in upstream prose. export function mdxText(value) { return String(value).replace( @@ -274,11 +336,13 @@ function main(argv) { if (!args.clone || (!args.check && !tag)) throw new Error('Helm generation requires --clone and a release tag'); + const charts = resolveHelmCharts(project, args.clone); + validateHelmInputs(charts, args.clone); const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'helm-reference-')); try { // Copy only documentation inputs: helm-docs must never overwrite upstream READMEs. // Prepare every page before replacing outputs, so incomplete annotations fail early. - const inputs = project.helm.map((chart, index) => { + const inputs = charts.map((chart, index) => { const dir = path.join(tmp, String(index)); fs.mkdirSync(dir); for (const file of ['Chart.yaml', 'values.yaml']) { diff --git a/scripts/upstream-release/generate-helm-reference.test.mjs b/scripts/upstream-release/generate-helm-reference.test.mjs index c41cf80e..4f4c4732 100644 --- a/scripts/upstream-release/generate-helm-reference.test.mjs +++ b/scripts/upstream-release/generate-helm-reference.test.mjs @@ -15,6 +15,8 @@ import { renderPage, renderDescription, validateDescriptions, + resolveHelmCharts, + validateHelmInputs, } from './generate-helm-reference.mjs'; const chart = { @@ -91,6 +93,80 @@ test('coverage validation reports every missing value across charts', () => { ); }); +test('release chart selection supports legacy and current gateway layouts', () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'helm-release-layout-')); + try { + const { projects } = yaml.parse( + fs.readFileSync( + new URL('../../.github/upstream-projects.yaml', import.meta.url), + 'utf8' + ) + ); + const project = projects.find( + (p) => p.id === 'stacklok-enterprise-platform' + ); + const metadata = path.join(root, project.helm_version_source); + fs.mkdirSync(path.dirname(metadata), { recursive: true }); + for (const [version, legacy] of [ + ['0.25.0', true], + ['0.26.0', false], + ['0.27.0', false], + ['0.100.0', false], + ]) { + fs.writeFileSync(metadata, yaml.stringify({ version })); + const charts = resolveHelmCharts(project, root); + const sources = charts.map((chart) => chart.source); + assert.equal( + sources.includes('enterprise/ai-gateway/helm/operator'), + legacy + ); + assert.equal( + sources.includes('enterprise/ai-gateway/helm/operator-crds'), + legacy + ); + assert.equal(sources.includes('enterprise/sprout/helm'), legacy); + assert.equal(sources.includes('enterprise/ai-gateway/helm'), !legacy); + assert.equal(charts.length, legacy ? 10 : 8); + const platform = charts.find((chart) => + chart.page.endsWith('/platform.mdx') + ); + assert.equal( + Boolean(platform.description_overrides?.['workspace.production']), + !legacy + ); + } + fs.writeFileSync(metadata, 'version: invalid\n'); + assert.throws( + () => resolveHelmCharts(project, root), + /Invalid Helm release version/ + ); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test('missing Helm inputs are reported together before generation', () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'helm-missing-inputs-')); + try { + fs.mkdirSync(path.join(root, 'first')); + fs.writeFileSync(path.join(root, 'first/Chart.yaml'), 'name: first\n'); + fs.writeFileSync(path.join(root, 'first/values.yaml'), ''); + assert.throws( + () => + validateHelmInputs([{ source: 'first' }, { source: 'second' }], root), + (error) => { + assert.match(error.message, /first\/values.yaml/); + assert.match(error.message, /second\/Chart.yaml/); + assert.match(error.message, /second\/values.yaml/); + assert.doesNotMatch(error.message, /first\/Chart.yaml/); + return true; + } + ); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + test('check mode validates all charts without writing pages or formatting', () => { const root = fs.realpathSync( fs.mkdtempSync(path.join(os.tmpdir(), 'helm-reference-check-'))