From 7fd9e3144fe4af26bebd5d884d46f4db52b0191a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 9 Jul 2026 23:56:03 +0000 Subject: [PATCH 01/59] automated: docs preview for patch 26.07.02 --- .github/workflows/validate-docs-site.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/validate-docs-site.yaml b/.github/workflows/validate-docs-site.yaml index 8f6dfaf9ad..9ef8923be1 100644 --- a/.github/workflows/validate-docs-site.yaml +++ b/.github/workflows/validate-docs-site.yaml @@ -55,6 +55,7 @@ jobs: uses: actions/checkout@v4 with: repository: validmind/release-notes + ref: automated/patch-26.07.02 path: site/_source/release-notes token: ${{ secrets.DOCS_CI_RO_PAT }} sparse-checkout: | From dfc63af61956a1d9fa62e087b252a10e6def15ce Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 10 Jul 2026 06:45:27 +0000 Subject: [PATCH 02/59] automated: docs preview for library 2.13.5 --- .github/workflows/validate-docs-site.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/validate-docs-site.yaml b/.github/workflows/validate-docs-site.yaml index 8f6dfaf9ad..c4cff87cc5 100644 --- a/.github/workflows/validate-docs-site.yaml +++ b/.github/workflows/validate-docs-site.yaml @@ -55,6 +55,7 @@ jobs: uses: actions/checkout@v4 with: repository: validmind/release-notes + ref: automated/library-v2.13.5 path: site/_source/release-notes token: ${{ secrets.DOCS_CI_RO_PAT }} sparse-checkout: | From 7e10dc6dae016325491ee702308626798dcd2a90 Mon Sep 17 00:00:00 2001 From: Andres Rodriguez Date: Mon, 13 Jul 2026 22:11:44 -0700 Subject: [PATCH 03/59] Optimize automated release notes previews --- .github/scripts/merge_quarto_indexes.py | 51 +++++++ .github/scripts/test_merge_quarto_indexes.py | 49 +++++++ .github/workflows/validate-docs-site.yaml | 140 ++++++++++++++++--- 3 files changed, 220 insertions(+), 20 deletions(-) create mode 100644 .github/scripts/merge_quarto_indexes.py create mode 100644 .github/scripts/test_merge_quarto_indexes.py diff --git a/.github/scripts/merge_quarto_indexes.py b/.github/scripts/merge_quarto_indexes.py new file mode 100644 index 0000000000..cc574a7fff --- /dev/null +++ b/.github/scripts/merge_quarto_indexes.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# Copyright © 2023-2026 ValidMind Inc. All rights reserved. +# SPDX-License-Identifier: AGPL-3.0 AND ValidMind Commercial + +"""Merge Quarto indexes from a partial render into full-site indexes.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from typing import Any + + +def merge_items( + base: list[dict[str, Any]], partial: list[dict[str, Any]], key: str +) -> list[dict[str, Any]]: + """Return base items with partial items replacing entries with the same key.""" + merged: dict[str, dict[str, Any]] = {} + for item in [*base, *partial]: + value = item.get(key) + if not isinstance(value, str) or not value: + raise ValueError(f"Every index item must have a non-empty {key!r}") + merged[value] = item + return list(merged.values()) + + +def merge_file(base_path: Path, partial_path: Path, key: str) -> None: + base = json.loads(base_path.read_text()) + partial = json.loads(partial_path.read_text()) + if not isinstance(base, list) or not isinstance(partial, list): + raise ValueError("Quarto indexes must contain JSON arrays") + partial_path.write_text( + json.dumps(merge_items(base, partial, key), separators=(",", ":")) + ) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--base-search", type=Path, required=True) + parser.add_argument("--partial-search", type=Path, required=True) + parser.add_argument("--base-listings", type=Path, required=True) + parser.add_argument("--partial-listings", type=Path, required=True) + args = parser.parse_args() + + merge_file(args.base_search, args.partial_search, "objectID") + merge_file(args.base_listings, args.partial_listings, "listing") + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/test_merge_quarto_indexes.py b/.github/scripts/test_merge_quarto_indexes.py new file mode 100644 index 0000000000..f2b450ecfd --- /dev/null +++ b/.github/scripts/test_merge_quarto_indexes.py @@ -0,0 +1,49 @@ +# Copyright © 2023-2026 ValidMind Inc. All rights reserved. +# SPDX-License-Identifier: AGPL-3.0 AND ValidMind Commercial + +import json +import tempfile +import unittest +from pathlib import Path + +from merge_quarto_indexes import merge_file, merge_items + + +class MergeQuartoIndexesTest(unittest.TestCase): + def test_partial_items_replace_matching_base_items(self): + base = [{"objectID": "old", "text": "keep"}, {"objectID": "changed", "text": "old"}] + partial = [{"objectID": "changed", "text": "new"}, {"objectID": "new", "text": "add"}] + + self.assertEqual( + merge_items(base, partial, "objectID"), + [ + {"objectID": "old", "text": "keep"}, + {"objectID": "changed", "text": "new"}, + {"objectID": "new", "text": "add"}, + ], + ) + + def test_merge_file_updates_partial_path(self): + with tempfile.TemporaryDirectory() as directory: + base_path = Path(directory) / "base.json" + partial_path = Path(directory) / "partial.json" + base_path.write_text(json.dumps([{"listing": "/old", "items": ["a"]}])) + partial_path.write_text(json.dumps([{"listing": "/new", "items": ["b"]}])) + + merge_file(base_path, partial_path, "listing") + + self.assertEqual( + json.loads(partial_path.read_text()), + [ + {"listing": "/old", "items": ["a"]}, + {"listing": "/new", "items": ["b"]}, + ], + ) + + def test_missing_key_is_rejected(self): + with self.assertRaises(ValueError): + merge_items([], [{"text": "missing object id"}], "objectID") + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/workflows/validate-docs-site.yaml b/.github/workflows/validate-docs-site.yaml index 8f6dfaf9ad..c03e3ec466 100644 --- a/.github/workflows/validate-docs-site.yaml +++ b/.github/workflows/validate-docs-site.yaml @@ -23,8 +23,50 @@ jobs: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - # Reclaim space + create a reserve for deterministic headroom + - name: Check out release-notes repository + uses: actions/checkout@v4 + with: + repository: validmind/release-notes + path: site/_source/release-notes + token: ${{ secrets.DOCS_CI_RO_PAT }} + sparse-checkout: | + releases + sparse-checkout-cone-mode: true + + - name: Detect automated release-notes preview + id: release-preview + shell: bash + run: | + set -euo pipefail + release_ref=$(git -C site/_source/release-notes for-each-ref \ + --format='%(refname:strip=3)' --points-at HEAD refs/remotes/origin | head -n 1) + + if [[ "$release_ref" != automated/* ]]; then + echo "fast=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + git -C site/_source/release-notes fetch --depth=1 origin main + : > .release-preview-targets + git -C site/_source/release-notes diff --name-only --diff-filter=ACMRT FETCH_HEAD HEAD -- releases \ + | awk -F/ 'NF >= 4 { print $1 "/" $2 "/" $3 }' \ + | sort -u > .release-preview-targets + + if [[ ! -s .release-preview-targets ]]; then + echo "No release directories changed; falling back to full validation." + echo "fast=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo "Automated release-notes ref: $release_ref" + echo "Targeted render directories:" + cat .release-preview-targets + echo "fast=true" >> "$GITHUB_OUTPUT" + + # Full validation needs generated sources and extra disk headroom. Automated + # release-note previews reuse the already validated staging build instead. - name: Free space + create reserve + if: steps.release-preview.outputs.fast != 'true' uses: ./.github/actions/free-disk-space with: remove_dotnet: "true" @@ -35,6 +77,7 @@ jobs: create_reserve_gb: "3" - name: Check out validmind-library repository + if: steps.release-preview.outputs.fast != 'true' uses: actions/checkout@v4 with: repository: validmind/validmind-library @@ -42,6 +85,7 @@ jobs: token: ${{ secrets.DOCS_CI_RO_PAT }} - name: Check out installation repository + if: steps.release-preview.outputs.fast != 'true' uses: actions/checkout@v4 with: repository: validmind/installation @@ -51,17 +95,8 @@ jobs: site/installation sparse-checkout-cone-mode: true - - name: Check out release-notes repository - uses: actions/checkout@v4 - with: - repository: validmind/release-notes - path: site/_source/release-notes - token: ${{ secrets.DOCS_CI_RO_PAT }} - sparse-checkout: | - releases - sparse-checkout-cone-mode: true - - name: Check out backend repository + if: steps.release-preview.outputs.fast != 'true' uses: actions/checkout@v4 with: repository: validmind/backend @@ -72,6 +107,7 @@ jobs: sparse-checkout-cone-mode: true - name: Set up uv + if: steps.release-preview.outputs.fast != 'true' uses: astral-sh/setup-uv@v5 - name: Verify copyright headers @@ -84,7 +120,11 @@ jobs: with: version: pre-release + - name: Test preview index merge + run: python3 -m unittest discover -s .github/scripts -p 'test_merge_quarto_indexes.py' -v + - name: Generate Python library docs + if: steps.release-preview.outputs.fast != 'true' run: | cd site/_source/validmind-library make install && make quarto-docs @@ -94,10 +134,12 @@ jobs: rsync -av --exclude '_build' --exclude 'templates' _source/validmind-library/docs/ validmind/ - name: Generate template schema docs + if: steps.release-preview.outputs.fast != 'true' run: | BACKEND_ROOT=site/_source/backend uv run --with json-schema-for-humans python scripts/generate_template_schema_docs.py - name: Populate installation + if: steps.release-preview.outputs.fast != 'true' run: cp -r site/_source/installation/site/installation site/installation - name: Populate release notes @@ -105,15 +147,59 @@ jobs: cp -r site/_source/release-notes/releases site rm -f site/releases/backend-releases.qmd site/releases/cmvm-releases.qmd - - name: Render demo docs site + # The staging HTML lets Quarto resolve descriptions for listings without + # re-rendering the other ~1,000 pages. Only HTML and the two global indexes + # are downloaded; the final preview is cloned server-side during deployment. + - name: Configure AWS credentials + run: aws configure set aws_access_key_id ${{ secrets.AWS_ACCESS_KEY_ID_STAGING }} && aws configure set aws_secret_access_key ${{ secrets.AWS_SECRET_ACCESS_KEY_STAGING }} && aws configure set default.region us-east-1 + + - name: Seed targeted render from staging + if: steps.release-preview.outputs.fast == 'true' + run: | + mkdir -p site/_site/releases site/.preview-indexes + aws s3 sync s3://validmind-docs-staging/site/releases site/_site/releases \ + --exclude "*" --include "*.html" --no-progress + aws s3 cp s3://validmind-docs-staging/site/search.json site/.preview-indexes/search.json --no-progress + aws s3 cp s3://validmind-docs-staging/site/listings.json site/.preview-indexes/listings.json --no-progress + + - name: Render targeted release preview + if: steps.release-preview.outputs.fast == 'true' run: | - cd site - quarto render --profile development 2>&1 | tee render_errors.log || { + cd site + : > render_errors.log + while read -r target; do + quarto render --profile development "$target" 2>&1 | tee -a render_errors.log || { echo "Quarto render failed immediately"; cat render_errors.log; exit 1; } - make generate-sitemap + done < ../.release-preview-targets + + for target in releases/*.qmd; do + [[ "$(basename "$target")" == _* ]] && continue + quarto render --profile development "$target" 2>&1 | tee -a render_errors.log || { + echo "Quarto render failed immediately"; + cat render_errors.log; + exit 1; + } + done + + python3 ../.github/scripts/merge_quarto_indexes.py \ + --base-search .preview-indexes/search.json \ + --partial-search _site/search.json \ + --base-listings .preview-indexes/listings.json \ + --partial-listings _site/listings.json + + - name: Render demo docs site + if: steps.release-preview.outputs.fast != 'true' + run: | + cd site + quarto render --profile development 2>&1 | tee render_errors.log || { + echo "Quarto render failed immediately"; + cat render_errors.log; + exit 1; + } + make generate-sitemap - name: Add robots.txt for PR preview run: cp site/environments/robots-staging.txt site/_site/robots.txt @@ -128,12 +214,21 @@ jobs: echo "No warnings or errors detected during Quarto render" fi - # Demo bucket is in us-east-1 - - name: Configure AWS credentials - run: aws configure set aws_access_key_id ${{ secrets.AWS_ACCESS_KEY_ID_STAGING }} && aws configure set aws_secret_access_key ${{ secrets.AWS_SECRET_ACCESS_KEY_STAGING }} && aws configure set default.region us-east-1 - - name: Deploy PR preview - run: aws s3 sync site/_site s3://validmind-docs-staging/site/pr_previews/${{ github.head_ref }} --delete --exclude "notebooks/EXECUTED/*" --cache-control "no-cache, max-age=0, must-revalidate" && aws cloudfront create-invalidation --distribution-id ESWVTZYFL873V --paths "/*" --no-cli-pager + run: | + preview_path="s3://validmind-docs-staging/site/pr_previews/${{ github.head_ref }}" + if [[ "${{ steps.release-preview.outputs.fast }}" == "true" ]]; then + aws s3 sync s3://validmind-docs-staging/site "$preview_path" \ + --delete --exclude "pr_previews/*" --exclude "notebooks/EXECUTED/*" --no-progress + aws s3 sync site/_site "$preview_path" \ + --exclude "notebooks/EXECUTED/*" --no-progress \ + --cache-control "no-cache, max-age=0, must-revalidate" + else + aws s3 sync site/_site "$preview_path" --delete \ + --exclude "notebooks/EXECUTED/*" \ + --cache-control "no-cache, max-age=0, must-revalidate" + fi + aws cloudfront create-invalidation --distribution-id ESWVTZYFL873V --paths "/*" --no-cli-pager - name: Post comment with preview URL uses: actions/github-script@v6 @@ -185,11 +280,13 @@ jobs: console.log(`Dispatched Lighthouse check for PR #${context.issue.number}`); - name: Install pandoc + if: steps.release-preview.outputs.fast != 'true' run: | sudo apt-get update sudo apt-get install -y pandoc - name: Verify chatbot product map is up to date + if: steps.release-preview.outputs.fast != 'true' run: | set -euo pipefail python3 site/scripts/generate_chatbot_product_map.py @@ -222,13 +319,16 @@ jobs: echo "Auto-committed refreshed site/llm/chatbot-product-map.md." - name: Test chatbot product map generator + if: steps.release-preview.outputs.fast != 'true' run: python3 -m unittest discover -s site/scripts -p 'test_generate_chatbot_product_map.py' -v - name: Validate LLM markdown render + if: steps.release-preview.outputs.fast != 'true' run: bash llm/render.sh && bash llm/clean.sh working-directory: site - name: Verify LLM corpus includes product map and docs IA hub + if: steps.release-preview.outputs.fast != 'true' run: | test -f site/llm/_llm-output/chatbot-product-map.md test -f site/llm/_llm-output/AGENTS.md From 57b412a10bce69add7fb398cddc3ffa7fdaedbf2 Mon Sep 17 00:00:00 2001 From: Andres Rodriguez Date: Mon, 13 Jul 2026 22:12:21 -0700 Subject: [PATCH 04/59] Test optimized library preview path --- .github/workflows/validate-docs-site.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/validate-docs-site.yaml b/.github/workflows/validate-docs-site.yaml index c03e3ec466..b8449f53d1 100644 --- a/.github/workflows/validate-docs-site.yaml +++ b/.github/workflows/validate-docs-site.yaml @@ -27,6 +27,7 @@ jobs: uses: actions/checkout@v4 with: repository: validmind/release-notes + ref: automated/library-v2.13.5 path: site/_source/release-notes token: ${{ secrets.DOCS_CI_RO_PAT }} sparse-checkout: | From bfdce819356bb9706fce1b23612cfc138eddedbe Mon Sep 17 00:00:00 2001 From: Andres Rodriguez Date: Mon, 13 Jul 2026 22:15:25 -0700 Subject: [PATCH 05/59] Resolve generated API link in targeted renders --- .github/workflows/validate-docs-site.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/validate-docs-site.yaml b/.github/workflows/validate-docs-site.yaml index b8449f53d1..b4b0abf2b6 100644 --- a/.github/workflows/validate-docs-site.yaml +++ b/.github/workflows/validate-docs-site.yaml @@ -157,7 +157,11 @@ jobs: - name: Seed targeted render from staging if: steps.release-preview.outputs.fast == 'true' run: | - mkdir -p site/_site/releases site/.preview-indexes + mkdir -p site/_site/releases site/.preview-indexes site/validmind + # The navbar links to this generated source file. The complete rendered + # Python API is reused from staging; this placeholder only lets Quarto + # resolve the link while rendering release pages. + touch site/validmind/validmind.qmd aws s3 sync s3://validmind-docs-staging/site/releases site/_site/releases \ --exclude "*" --include "*.html" --no-progress aws s3 cp s3://validmind-docs-staging/site/search.json site/.preview-indexes/search.json --no-progress From 84534ccc6e6ddff3011cc02c4e78b0205a8a3f07 Mon Sep 17 00:00:00 2001 From: Andres Rodriguez Date: Mon, 13 Jul 2026 22:19:42 -0700 Subject: [PATCH 06/59] Remove temporary release preview test ref --- .github/workflows/validate-docs-site.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/validate-docs-site.yaml b/.github/workflows/validate-docs-site.yaml index b4b0abf2b6..5e0475b353 100644 --- a/.github/workflows/validate-docs-site.yaml +++ b/.github/workflows/validate-docs-site.yaml @@ -27,7 +27,6 @@ jobs: uses: actions/checkout@v4 with: repository: validmind/release-notes - ref: automated/library-v2.13.5 path: site/_source/release-notes token: ${{ secrets.DOCS_CI_RO_PAT }} sparse-checkout: | From 68562fd0dd8eff8f010bd4b446c6e47fec6750db Mon Sep 17 00:00:00 2001 From: Andres Rodriguez Date: Mon, 13 Jul 2026 22:32:09 -0700 Subject: [PATCH 07/59] Preserve staging homepage in targeted previews --- .github/workflows/validate-docs-site.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/validate-docs-site.yaml b/.github/workflows/validate-docs-site.yaml index 5e0475b353..faeeb4188b 100644 --- a/.github/workflows/validate-docs-site.yaml +++ b/.github/workflows/validate-docs-site.yaml @@ -27,6 +27,7 @@ jobs: uses: actions/checkout@v4 with: repository: validmind/release-notes + ref: automated/library-v2.13.5 path: site/_source/release-notes token: ${{ secrets.DOCS_CI_RO_PAT }} sparse-checkout: | @@ -225,7 +226,7 @@ jobs: aws s3 sync s3://validmind-docs-staging/site "$preview_path" \ --delete --exclude "pr_previews/*" --exclude "notebooks/EXECUTED/*" --no-progress aws s3 sync site/_site "$preview_path" \ - --exclude "notebooks/EXECUTED/*" --no-progress \ + --exclude "index.html" --exclude "notebooks/EXECUTED/*" --no-progress \ --cache-control "no-cache, max-age=0, must-revalidate" else aws s3 sync site/_site "$preview_path" --delete \ From d844d93a1daa833c798cd12628bae9adc1d32d3c Mon Sep 17 00:00:00 2001 From: Andres Rodriguez Date: Mon, 13 Jul 2026 22:36:43 -0700 Subject: [PATCH 08/59] Remove temporary homepage fix test ref --- .github/workflows/validate-docs-site.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/validate-docs-site.yaml b/.github/workflows/validate-docs-site.yaml index faeeb4188b..5e81cd6896 100644 --- a/.github/workflows/validate-docs-site.yaml +++ b/.github/workflows/validate-docs-site.yaml @@ -27,7 +27,6 @@ jobs: uses: actions/checkout@v4 with: repository: validmind/release-notes - ref: automated/library-v2.13.5 path: site/_source/release-notes token: ${{ secrets.DOCS_CI_RO_PAT }} sparse-checkout: | From 48fd3240d9efb33a53067edb8e2686a298b9d899 Mon Sep 17 00:00:00 2001 From: Andres Rodriguez Date: Mon, 13 Jul 2026 22:37:14 -0700 Subject: [PATCH 09/59] Remove Claude workflows --- .github/workflows/claude-code-review.yml | 45 --------------------- .github/workflows/claude.yml | 50 ------------------------ 2 files changed, 95 deletions(-) delete mode 100644 .github/workflows/claude-code-review.yml delete mode 100644 .github/workflows/claude.yml diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml deleted file mode 100644 index 3dc09acb0a..0000000000 --- a/.github/workflows/claude-code-review.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Claude Code Review - -on: - # workflow_dispatch: # Manual only - pull_request: - types: [opened, synchronize, ready_for_review, reopened] - # Optional: Only run on specific file changes - # paths: - # - "src/**/*.ts" - # - "src/**/*.tsx" - # - "src/**/*.js" - # - "src/**/*.jsx" - -jobs: - claude-review: - # Optional: Filter by PR author - # if: | - # github.event.pull_request.user.login == 'external-contributor' || - # github.event.pull_request.user.login == 'new-developer' || - # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' - - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - issues: read - id-token: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Run Claude Code Review - id: claude-review - uses: anthropics/claude-code-action@v1 - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' - plugins: 'code-review@claude-code-plugins' - prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' - # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md - # or https://code.claude.com/docs/en/cli-reference for available options - diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml deleted file mode 100644 index 79fe056478..0000000000 --- a/.github/workflows/claude.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Claude Code - -on: - issue_comment: - types: [created] - pull_request_review_comment: - types: [created] - issues: - types: [opened, assigned] - pull_request_review: - types: [submitted] - -jobs: - claude: - if: | - (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || - (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - issues: read - id-token: write - actions: read # Required for Claude to read CI results on PRs - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Run Claude Code - id: claude - uses: anthropics/claude-code-action@v1 - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - - # This is an optional setting that allows Claude to read CI results on PRs - additional_permissions: | - actions: read - - # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. - # prompt: 'Update the pull request description to include a summary of changes.' - - # Optional: Add claude_args to customize behavior and configuration - # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md - # or https://code.claude.com/docs/en/cli-reference for available options - # claude_args: '--allowed-tools Bash(gh pr:*)' - From 8eeab55fb00deaa3c7263a4c79d48d42d0c20620 Mon Sep 17 00:00:00 2001 From: Andres Rodriguez Date: Mon, 13 Jul 2026 23:15:32 -0700 Subject: [PATCH 10/59] Promote prebuilt docs artifacts to production --- .github/actions/build-docs-site/action.yml | 114 +++++++++++++++ .github/workflows/deploy-docs-prod.yaml | 137 ++++++++---------- .github/workflows/deploy-docs-staging.yaml | 160 +++++++++++---------- 3 files changed, 253 insertions(+), 158 deletions(-) create mode 100644 .github/actions/build-docs-site/action.yml diff --git a/.github/actions/build-docs-site/action.yml b/.github/actions/build-docs-site/action.yml new file mode 100644 index 0000000000..9aa4e187f8 --- /dev/null +++ b/.github/actions/build-docs-site/action.yml @@ -0,0 +1,114 @@ +name: Build docs site +description: Populate generated sources and render a complete documentation site + +inputs: + profile: + description: Quarto profile to render + required: true + docs_ci_ro_pat: + description: Read-only token for private source repositories + required: true + quarto_version: + description: Quarto version to install + required: true + library_ref: + description: validmind-library revision to build + required: true + installation_ref: + description: installation revision to build + required: true + release_notes_ref: + description: release-notes revision to build + required: true + backend_ref: + description: backend revision to build + required: true + +runs: + using: composite + steps: + - name: Check out validmind-library repository + uses: actions/checkout@v4 + with: + repository: validmind/validmind-library + ref: ${{ inputs.library_ref }} + path: site/_source/validmind-library + token: ${{ inputs.docs_ci_ro_pat }} + + - name: Check out installation repository + uses: actions/checkout@v4 + with: + repository: validmind/installation + ref: ${{ inputs.installation_ref }} + path: site/_source/installation + token: ${{ inputs.docs_ci_ro_pat }} + sparse-checkout: | + site/installation + sparse-checkout-cone-mode: true + + - name: Check out release-notes repository + uses: actions/checkout@v4 + with: + repository: validmind/release-notes + ref: ${{ inputs.release_notes_ref }} + path: site/_source/release-notes + token: ${{ inputs.docs_ci_ro_pat }} + sparse-checkout: | + releases + sparse-checkout-cone-mode: true + + - name: Check out backend repository + uses: actions/checkout@v4 + with: + repository: validmind/backend + ref: ${{ inputs.backend_ref }} + path: site/_source/backend + token: ${{ inputs.docs_ci_ro_pat }} + sparse-checkout: | + src/backend/templates/documentation/model_documentation + sparse-checkout-cone-mode: true + + - name: Set up Quarto + uses: quarto-dev/quarto-actions/setup@v2 + with: + version: ${{ inputs.quarto_version }} + + - name: Set up uv + uses: astral-sh/setup-uv@v5 + + - name: Generate Python library docs + shell: bash + run: | + cd site/_source/validmind-library + make install && make quarto-docs + cd ../../ + rm -rf validmind + mkdir -p validmind + rsync -av --exclude '_build' --exclude 'templates' _source/validmind-library/docs/ validmind/ + + - name: Generate template schema docs + shell: bash + run: BACKEND_ROOT=site/_source/backend uv run --with json-schema-for-humans python scripts/generate_template_schema_docs.py + + - name: Populate installation + shell: bash + run: cp -r site/_source/installation/site/installation site/installation + + - name: Populate release notes + shell: bash + run: | + cp -r site/_source/release-notes/releases site + rm -f site/releases/backend-releases.qmd site/releases/cmvm-releases.qmd + + - name: Render docs site + shell: bash + env: + QUARTO_PROFILE: ${{ inputs.profile }} + run: | + cd site + quarto render --profile "$QUARTO_PROFILE" &> render_errors.log || { + echo "Quarto render failed immediately" + cat render_errors.log + exit 1 + } + make generate-sitemap diff --git a/.github/workflows/deploy-docs-prod.yaml b/.github/workflows/deploy-docs-prod.yaml index 29390293db..f776ba1f9a 100644 --- a/.github/workflows/deploy-docs-prod.yaml +++ b/.github/workflows/deploy-docs-prod.yaml @@ -10,6 +10,14 @@ on: - completed workflow_dispatch: +concurrency: + group: deploy-docs-production + cancel-in-progress: true + +permissions: + actions: read + contents: read + jobs: deploy: if: | @@ -22,8 +30,46 @@ jobs: - name: Check out documentation repository uses: actions/checkout@v4 - # Reclaim space + create a reserve for deterministic headroom + - name: Find production artifact for this source tree + id: production-artifact + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + name="docs-production-$(git rev-parse 'HEAD^{tree}')" + run_id=$(gh api "repos/${{ github.repository }}/actions/artifacts?name=$name&per_page=100" \ + --jq '[.artifacts[] | select(.expired == false)] | sort_by(.created_at) | reverse | .[0].workflow_run.id // empty') + + echo "name=$name" >> "$GITHUB_OUTPUT" + if [[ -n "$run_id" ]]; then + echo "Found $name in workflow run $run_id" + echo "found=true" >> "$GITHUB_OUTPUT" + echo "run_id=$run_id" >> "$GITHUB_OUTPUT" + else + echo "No matching artifact found; falling back to a full production build." + echo "found=false" >> "$GITHUB_OUTPUT" + fi + + - name: Download prebuilt production docs + if: steps.production-artifact.outputs.found == 'true' + uses: actions/download-artifact@v4 + with: + name: ${{ steps.production-artifact.outputs.name }} + path: ${{ runner.temp }}/production-artifact + github-token: ${{ github.token }} + repository: ${{ github.repository }} + run-id: ${{ steps.production-artifact.outputs.run_id }} + + - name: Extract prebuilt production docs + if: steps.production-artifact.outputs.found == 'true' + run: | + mkdir -p site/_site + tar --zstd -xf "$RUNNER_TEMP/production-artifact/docs-production.tar.zst" -C site/_site + + # Reclaim space only when the prebuilt artifact is unavailable and the + # workflow must perform the original full-site build. - name: Free space + create reserve + if: steps.production-artifact.outputs.found != 'true' uses: ./.github/actions/free-disk-space with: remove_dotnet: "true" @@ -33,84 +79,17 @@ jobs: apt_cleanup: "true" create_reserve_gb: "3" - - name: Check out validmind-library repository - uses: actions/checkout@v4 + - name: Build production docs site + if: steps.production-artifact.outputs.found != 'true' + uses: ./.github/actions/build-docs-site with: - repository: validmind/validmind-library - path: site/_source/validmind-library - token: ${{ secrets.DOCS_CI_RO_PAT }} - - - name: Check out installation repository - uses: actions/checkout@v4 - with: - repository: validmind/installation - path: site/_source/installation - token: ${{ secrets.DOCS_CI_RO_PAT }} - sparse-checkout: | - site/installation - sparse-checkout-cone-mode: true - - - name: Check out release-notes repository - uses: actions/checkout@v4 - with: - repository: validmind/release-notes - path: site/_source/release-notes - token: ${{ secrets.DOCS_CI_RO_PAT }} - sparse-checkout: | - releases - sparse-checkout-cone-mode: true - - - name: Check out backend repository - uses: actions/checkout@v4 - with: - repository: validmind/backend - path: site/_source/backend - token: ${{ secrets.DOCS_CI_RO_PAT }} - sparse-checkout: | - src/backend/templates/documentation/model_documentation - sparse-checkout-cone-mode: true - - - name: Set up Quarto - uses: quarto-dev/quarto-actions/setup@v2 - with: - version: ${{ vars.QUARTO_VERSION }} - - - name: Set up uv - uses: astral-sh/setup-uv@v5 - - - name: Set up uv - uses: astral-sh/setup-uv@v5 - - - name: Generate Python library docs - run: | - cd site/_source/validmind-library - make install && make quarto-docs - cd ../../ - rm -rf validmind - mkdir -p validmind - rsync -av --exclude '_build' --exclude 'templates' _source/validmind-library/docs/ validmind/ - - - name: Generate template schema docs - run: | - BACKEND_ROOT=site/_source/backend uv run --with json-schema-for-humans python scripts/generate_template_schema_docs.py - - - name: Populate installation - run: cp -r site/_source/installation/site/installation site/installation - - - name: Populate release notes - run: | - cp -r site/_source/release-notes/releases site - rm -f site/releases/backend-releases.qmd site/releases/cmvm-releases.qmd - - - name: Render prod docs site - run: | - cd site - quarto render --profile production &> render_errors.log || { - echo "Quarto render failed immediately"; - cat render_errors.log; - exit 1; - } - make generate-sitemap + profile: production + docs_ci_ro_pat: ${{ secrets.DOCS_CI_RO_PAT }} + quarto_version: ${{ vars.QUARTO_VERSION }} + library_ref: main + installation_ref: main + release_notes_ref: main + backend_ref: main # Prod bucket is in us-east-1 - name: Configure AWS credentials @@ -136,4 +115,4 @@ jobs: - name: Final disk usage if: always() - run: df -hT / \ No newline at end of file + run: df -hT / diff --git a/.github/workflows/deploy-docs-staging.yaml b/.github/workflows/deploy-docs-staging.yaml index 2ade3f3aa1..812915e978 100644 --- a/.github/workflows/deploy-docs-staging.yaml +++ b/.github/workflows/deploy-docs-staging.yaml @@ -10,18 +10,65 @@ on: - completed workflow_dispatch: +concurrency: + group: deploy-docs-staging + cancel-in-progress: true + jobs: - deploy: + resolve-sources: + name: Resolve source revisions + if: | + github.event_name == 'push' || + (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') || + github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + outputs: + library: ${{ steps.refs.outputs.library }} + installation: ${{ steps.refs.outputs.installation }} + release_notes: ${{ steps.refs.outputs.release_notes }} + backend: ${{ steps.refs.outputs.backend }} + steps: + - name: Resolve source revisions + id: refs + env: + GH_TOKEN: ${{ secrets.DOCS_CI_RO_PAT }} + run: | + echo "library=$(gh api repos/validmind/validmind-library/commits/main --jq .sha)" >> "$GITHUB_OUTPUT" + echo "installation=$(gh api repos/validmind/installation/commits/main --jq .sha)" >> "$GITHUB_OUTPUT" + echo "release_notes=$(gh api repos/validmind/release-notes/commits/main --jq .sha)" >> "$GITHUB_OUTPUT" + echo "backend=$(gh api repos/validmind/backend/commits/main --jq .sha)" >> "$GITHUB_OUTPUT" + + build: + name: Build ${{ matrix.profile }} docs site if: | github.event_name == 'push' || (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest + needs: resolve-sources + strategy: + fail-fast: false + matrix: + profile: [staging, production] steps: - name: Check out documentation repository uses: actions/checkout@v4 + # Production can contain hotfixes that have not flowed back to staging. + # Build the artifact from the tree that a staging-to-prod merge will + # produce, so its key matches the eventual prod merge commit. + - name: Prepare prospective production tree + if: matrix.profile == 'production' + run: | + set -euo pipefail + source_sha=$(git rev-parse HEAD) + git fetch origin prod + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git switch --detach origin/prod + git merge --no-commit --no-ff "$source_sha" + # Reclaim space + create a reserve for deterministic headroom - name: Free space + create reserve uses: ./.github/actions/free-disk-space @@ -33,95 +80,50 @@ jobs: apt_cleanup: "true" create_reserve_gb: "3" - - name: Check out validmind-library repository - uses: actions/checkout@v4 - with: - repository: validmind/validmind-library - path: site/_source/validmind-library - token: ${{ secrets.DOCS_CI_RO_PAT }} - - - name: Check out installation repository - uses: actions/checkout@v4 - with: - repository: validmind/installation - path: site/_source/installation - token: ${{ secrets.DOCS_CI_RO_PAT }} - sparse-checkout: | - site/installation - sparse-checkout-cone-mode: true - - - name: Check out release-notes repository - uses: actions/checkout@v4 + - name: Build docs site + uses: ./.github/actions/build-docs-site with: - repository: validmind/release-notes - path: site/_source/release-notes - token: ${{ secrets.DOCS_CI_RO_PAT }} - sparse-checkout: | - releases - sparse-checkout-cone-mode: true - - - name: Check out backend repository - uses: actions/checkout@v4 - with: - repository: validmind/backend - path: site/_source/backend - token: ${{ secrets.DOCS_CI_RO_PAT }} - sparse-checkout: | - src/backend/templates/documentation/model_documentation - sparse-checkout-cone-mode: true - - - name: Set up Quarto - uses: quarto-dev/quarto-actions/setup@v2 - with: - version: ${{ vars.QUARTO_VERSION }} - - - name: Set up uv - uses: astral-sh/setup-uv@v5 - - - name: Set up uv - uses: astral-sh/setup-uv@v5 - - - name: Generate Python library docs - run: | - cd site/_source/validmind-library - make install && make quarto-docs - cd ../../ - rm -rf validmind - mkdir -p validmind - rsync -av --exclude '_build' --exclude 'templates' _source/validmind-library/docs/ validmind/ - - - name: Generate template schema docs - run: | - BACKEND_ROOT=site/_source/backend uv run --with json-schema-for-humans python scripts/generate_template_schema_docs.py - - - name: Populate installation - run: cp -r site/_source/installation/site/installation site/installation - - - name: Populate release notes - run: | - cp -r site/_source/release-notes/releases site - rm -f site/releases/backend-releases.qmd site/releases/cmvm-releases.qmd - - - name: Render staging docs site - run: | - cd site - quarto render --profile staging &> render_errors.log || { - echo "Quarto render failed immediately"; - cat render_errors.log; - exit 1; - } - make generate-sitemap + profile: ${{ matrix.profile }} + docs_ci_ro_pat: ${{ secrets.DOCS_CI_RO_PAT }} + quarto_version: ${{ vars.QUARTO_VERSION }} + library_ref: ${{ needs.resolve-sources.outputs.library }} + installation_ref: ${{ needs.resolve-sources.outputs.installation }} + release_notes_ref: ${{ needs.resolve-sources.outputs.release_notes }} + backend_ref: ${{ needs.resolve-sources.outputs.backend }} - name: Add robots.txt for staging + if: matrix.profile == 'staging' run: cp site/environments/robots-staging.txt site/_site/robots.txt # Staging bucket is in us-west-2 - name: Configure AWS credentials + if: matrix.profile == 'staging' run: aws configure set aws_access_key_id ${{ secrets.AWS_ACCESS_KEY_ID_STAGING }} && aws configure set aws_secret_access_key ${{ secrets.AWS_SECRET_ACCESS_KEY_STAGING }} && aws configure set default.region us-west-2 - name: Deploy docs staging site + if: matrix.profile == 'staging' run: aws s3 sync site/_site s3://validmind-docs-staging/site --delete --exclude "installation/helm-repo/*" --exclude "pr_previews/*" --exclude "notebooks/EXECUTED/*" --exclude "llm/*" --cache-control "no-cache, max-age=0, must-revalidate" && aws cloudfront create-invalidation --distribution-id ESWVTZYFL873V --paths "/*" --no-cli-pager + - name: Determine production artifact name + if: matrix.profile == 'production' + id: production-artifact + shell: bash + run: echo "name=docs-production-$(git write-tree)" >> "$GITHUB_OUTPUT" + + - name: Package production docs site + if: matrix.profile == 'production' + run: tar --zstd -cf "$RUNNER_TEMP/docs-production.tar.zst" -C site/_site . + + - name: Upload production docs artifact + if: matrix.profile == 'production' + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.production-artifact.outputs.name }} + path: ${{ runner.temp }}/docs-production.tar.zst + compression-level: 0 + if-no-files-found: error + retention-days: 14 + # Release headroom and shrink before final lightweight steps & post-job - name: Release reserve & shrink if: always() @@ -139,4 +141,4 @@ jobs: - name: Final disk usage if: always() - run: df -hT / \ No newline at end of file + run: df -hT / From 8020d7d9c2a741b7005dc730d94375b602bb8762 Mon Sep 17 00:00:00 2001 From: Andres Rodriguez Date: Tue, 14 Jul 2026 00:27:33 -0700 Subject: [PATCH 11/59] Fetch branch history for production artifact merge --- .github/workflows/deploy-docs-staging.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/deploy-docs-staging.yaml b/.github/workflows/deploy-docs-staging.yaml index 812915e978..1681ea6d5c 100644 --- a/.github/workflows/deploy-docs-staging.yaml +++ b/.github/workflows/deploy-docs-staging.yaml @@ -54,6 +54,10 @@ jobs: steps: - name: Check out documentation repository uses: actions/checkout@v4 + with: + # The production build predicts the staging-to-prod merge tree and + # therefore needs the shared branch history, not a depth-one clone. + fetch-depth: 0 # Production can contain hotfixes that have not flowed back to staging. # Build the artifact from the tree that a staging-to-prod merge will From 438d1ba952bc52afd42ab8cfc5ce64d34986b68b Mon Sep 17 00:00:00 2001 From: Andres Rodriguez Date: Tue, 14 Jul 2026 00:30:11 -0700 Subject: [PATCH 12/59] Add full documentation merge gate --- .github/workflows/full-docs-validation.yaml | 132 ++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 .github/workflows/full-docs-validation.yaml diff --git a/.github/workflows/full-docs-validation.yaml b/.github/workflows/full-docs-validation.yaml new file mode 100644 index 0000000000..6da9d703e9 --- /dev/null +++ b/.github/workflows/full-docs-validation.yaml @@ -0,0 +1,132 @@ +name: Full docs validation + +on: + merge_group: + types: [checks_requested] + pull_request: + types: [labeled] + workflow_dispatch: + +concurrency: + group: full-docs-validation-${{ github.event.pull_request.number || github.event.merge_group.head_sha || github.ref }} + cancel-in-progress: true + +permissions: + actions: write + contents: read + +jobs: + full-docs-validation: + name: Full docs validation + if: github.event_name != 'pull_request' || github.event.label.name == 'full-validation' + runs-on: ubuntu-latest + + steps: + - name: Check out documentation repository + uses: actions/checkout@v4 + + - name: Resolve source revisions + id: sources + env: + GH_TOKEN: ${{ secrets.DOCS_CI_RO_PAT }} + run: | + echo "library=$(gh api repos/validmind/validmind-library/commits/main --jq .sha)" >> "$GITHUB_OUTPUT" + echo "installation=$(gh api repos/validmind/installation/commits/main --jq .sha)" >> "$GITHUB_OUTPUT" + echo "release_notes=$(gh api repos/validmind/release-notes/commits/main --jq .sha)" >> "$GITHUB_OUTPUT" + echo "backend=$(gh api repos/validmind/backend/commits/main --jq .sha)" >> "$GITHUB_OUTPUT" + + - name: Free space + create reserve + uses: ./.github/actions/free-disk-space + with: + remove_dotnet: "true" + remove_android: "true" + remove_haskell: "true" + prune_docker: "true" + apt_cleanup: "true" + create_reserve_gb: "3" + + - name: Verify copyright headers + run: make -C site verify-copyright + + - name: Build complete production docs site + uses: ./.github/actions/build-docs-site + with: + profile: production + docs_ci_ro_pat: ${{ secrets.DOCS_CI_RO_PAT }} + quarto_version: ${{ vars.QUARTO_VERSION }} + library_ref: ${{ steps.sources.outputs.library }} + installation_ref: ${{ steps.sources.outputs.installation }} + release_notes_ref: ${{ steps.sources.outputs.release_notes }} + backend_ref: ${{ steps.sources.outputs.backend }} + + - name: Test for render warnings or errors + run: | + if grep -q 'WARN\|WARNING\|ERROR:' site/render_errors.log; then + echo "Warnings or errors detected during Quarto render" + cat site/render_errors.log + exit 1 + fi + echo "No warnings or errors detected during Quarto render" + + - name: Install pandoc + run: | + sudo apt-get update + sudo apt-get install -y pandoc + + - name: Verify chatbot product map is up to date + run: | + set -euo pipefail + python3 site/scripts/generate_chatbot_product_map.py + git diff --exit-code -- \ + site/llm/chatbot-product-map.md \ + site/llm/chatbot-product-map-frontend-snapshot.json + + - name: Test chatbot product map generator + run: python3 -m unittest discover -s site/scripts -p 'test_generate_chatbot_product_map.py' -v + + - name: Validate LLM markdown render + run: bash llm/render.sh && bash llm/clean.sh + working-directory: site + + - name: Verify required LLM corpus content + run: | + test -f site/llm/_llm-output/chatbot-product-map.md + test -f site/llm/_llm-output/AGENTS.md + test -f site/llm/_llm-output/about/using-the-documentation.md + test ! -f site/llm/_llm-output/about/contributing/validmind-community.md + test ! -d site/llm/_llm-output/about/contributing/style-guide + + - name: Determine artifact name + id: artifact + run: echo "name=docs-production-$(git rev-parse 'HEAD^{tree}')" >> "$GITHUB_OUTPUT" + + - name: Package validated production site + run: tar --zstd -cf "$RUNNER_TEMP/docs-production.tar.zst" -C site/_site . + + - name: Upload validated production artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.artifact.outputs.name }} + path: ${{ runner.temp }}/docs-production.tar.zst + compression-level: 0 + if-no-files-found: error + retention-days: 14 + + - name: Release reserve and shrink + if: always() + uses: ./.github/actions/free-disk-space + with: + release_reserve: "true" + remove_paths: | + site/_source/installation + site/_source/release-notes + site/_source/backend + site/render_errors.log + site/_freeze + site/llm/_llm-output + dev.env + valid.env + + - name: Final disk usage + if: always() + run: df -hT / From 0010a27e56f2b5f38cfac53e4c722f81dece0dfe Mon Sep 17 00:00:00 2001 From: Andres Rodriguez Date: Tue, 14 Jul 2026 09:55:45 -0700 Subject: [PATCH 13/59] Use targeted renders for docs PR previews --- .../scripts/select_docs_preview_targets.py | 118 +++++++++++++++++ .../test_select_docs_preview_targets.py | 65 ++++++++++ .../request-full-docs-validation.yaml | 57 ++++++++ .github/workflows/validate-docs-site.yaml | 122 ++++++++++++++---- 4 files changed, 337 insertions(+), 25 deletions(-) create mode 100644 .github/scripts/select_docs_preview_targets.py create mode 100644 .github/scripts/test_select_docs_preview_targets.py create mode 100644 .github/workflows/request-full-docs-validation.yaml diff --git a/.github/scripts/select_docs_preview_targets.py b/.github/scripts/select_docs_preview_targets.py new file mode 100644 index 0000000000..8632116892 --- /dev/null +++ b/.github/scripts/select_docs_preview_targets.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +# Copyright © 2023-2026 ValidMind Inc. All rights reserved. +# SPDX-License-Identifier: AGPL-3.0 AND ValidMind Commercial + +"""Select pages that are safe to render incrementally for a PR preview.""" + +from __future__ import annotations + +import argparse +import subprocess +from dataclasses import dataclass +from pathlib import Path, PurePosixPath + + +PAGE_SUFFIXES = {".qmd", ".md", ".ipynb"} +ASSET_SUFFIXES = {".avif", ".gif", ".jpeg", ".jpg", ".pdf", ".png", ".svg", ".webp"} +UNSAFE_TOP_LEVEL = { + "_extensions", + "_freeze", + "_source", + "environments", + "llm", + "scripts", +} + + +@dataclass(frozen=True) +class Selection: + targets: tuple[str, ...] = () + assets: tuple[str, ...] = () + fallback_reason: str | None = None + + @property + def is_targeted(self) -> bool: + return self.fallback_reason is None + + +def select(changes: list[tuple[str, tuple[str, ...]]]) -> Selection: + targets: set[str] = set() + assets: set[str] = set() + + for status, paths in changes: + if status not in {"A", "M"} or len(paths) != 1: + return Selection(fallback_reason=f"{status} change requires a full render") + + path = PurePosixPath(paths[0]) + if not path.parts or path.parts[0] != "site" or len(path.parts) < 2: + return Selection(fallback_reason=f"{path} is outside targetable site content") + + relative = PurePosixPath(*path.parts[1:]) + if relative.parts[0] in UNSAFE_TOP_LEVEL: + return Selection(fallback_reason=f"{path} can affect generated or global content") + if any(part.startswith("_") for part in relative.parts): + return Selection(fallback_reason=f"{path} is Quarto metadata or shared content") + + suffix = relative.suffix.lower() + if suffix in PAGE_SUFFIXES: + targets.add(relative.as_posix()) + elif suffix in ASSET_SUFFIXES: + assets.add(relative.as_posix()) + else: + return Selection(fallback_reason=f"{path} is not a targetable page or asset") + + if not targets: + return Selection(fallback_reason="no changed renderable pages were found") + + return Selection(tuple(sorted(targets)), tuple(sorted(assets))) + + +def parse_name_status(output: str) -> list[tuple[str, tuple[str, ...]]]: + changes: list[tuple[str, tuple[str, ...]]] = [] + for line in output.splitlines(): + fields = line.split("\t") + if len(fields) < 2: + raise ValueError(f"Unexpected git diff line: {line!r}") + changes.append((fields[0][0], tuple(fields[1:]))) + return changes + + +def git_changes(base: str, head: str) -> list[tuple[str, tuple[str, ...]]]: + result = subprocess.run( + ["git", "diff", "--name-status", "--find-renames", f"{base}...{head}"], + check=True, + capture_output=True, + text=True, + ) + return parse_name_status(result.stdout) + + +def write_lines(path: Path, values: tuple[str, ...]) -> None: + path.write_text("".join(f"{value}\n" for value in values)) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--base", required=True) + parser.add_argument("--head", default="HEAD") + parser.add_argument("--targets", type=Path, required=True) + parser.add_argument("--assets", type=Path, required=True) + args = parser.parse_args() + + selection = select(git_changes(args.base, args.head)) + if not selection.is_targeted: + print(f"Full render required: {selection.fallback_reason}") + return 3 + + write_lines(args.targets, selection.targets) + write_lines(args.assets, selection.assets) + print("Targeted render pages:") + print("\n".join(selection.targets)) + if selection.assets: + print("Targeted preview assets:") + print("\n".join(selection.assets)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/scripts/test_select_docs_preview_targets.py b/.github/scripts/test_select_docs_preview_targets.py new file mode 100644 index 0000000000..e43210e905 --- /dev/null +++ b/.github/scripts/test_select_docs_preview_targets.py @@ -0,0 +1,65 @@ +# Copyright © 2023-2026 ValidMind Inc. All rights reserved. +# SPDX-License-Identifier: AGPL-3.0 AND ValidMind Commercial + +import unittest + +from select_docs_preview_targets import parse_name_status, select + + +class SelectDocsPreviewTargetsTest(unittest.TestCase): + def test_selects_changed_pages_and_assets(self): + result = select( + [ + ("M", ("site/guide/example.qmd",)), + ("A", ("site/guide/images/example.png",)), + ] + ) + + self.assertTrue(result.is_targeted) + self.assertEqual(result.targets, ("guide/example.qmd",)) + self.assertEqual(result.assets, ("guide/images/example.png",)) + + def test_global_quarto_change_requires_full_render(self): + result = select([("M", ("site/_quarto.yml",))]) + + self.assertFalse(result.is_targeted) + + def test_shared_metadata_requires_full_render(self): + result = select([("M", ("site/releases/_metadata.yml",))]) + + self.assertFalse(result.is_targeted) + + def test_deleted_page_requires_full_render(self): + result = select([("D", ("site/guide/old.qmd",))]) + + self.assertFalse(result.is_targeted) + + def test_non_site_change_requires_full_render(self): + result = select([("M", (".github/workflows/example.yaml",))]) + + self.assertFalse(result.is_targeted) + + def test_asset_only_change_requires_full_render(self): + result = select([("M", ("site/guide/images/example.png",))]) + + self.assertFalse(result.is_targeted) + + def test_generated_corpus_change_requires_full_render(self): + result = select([("M", ("site/llm/AGENTS.md",))]) + + self.assertFalse(result.is_targeted) + + def test_parses_renames_for_safe_fallback(self): + changes = parse_name_status( + "R100\tsite/guide/old.qmd\tsite/guide/new.qmd\n" + ) + + self.assertEqual( + changes, + [("R", ("site/guide/old.qmd", "site/guide/new.qmd"))], + ) + self.assertFalse(select(changes).is_targeted) + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/workflows/request-full-docs-validation.yaml b/.github/workflows/request-full-docs-validation.yaml new file mode 100644 index 0000000000..fdc186c397 --- /dev/null +++ b/.github/workflows/request-full-docs-validation.yaml @@ -0,0 +1,57 @@ +name: Request full docs validation + +on: + pull_request: + types: [synchronize] + pull_request_review: + types: [submitted] + +permissions: + actions: write + contents: read + pull-requests: read + +jobs: + request: + if: >- + github.event_name != 'pull_request_review' || + github.event.review.state == 'approved' + runs-on: ubuntu-latest + steps: + - name: Dispatch validation for approved revision + uses: actions/github-script@v7 + with: + script: | + const pullRequest = context.payload.pull_request; + const result = await github.graphql(` + query($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $number) { + reviewDecision + } + } + } + `, { + owner: context.repo.owner, + repo: context.repo.repo, + number: pullRequest.number, + }); + + const decision = result.repository.pullRequest.reviewDecision; + if (decision !== 'APPROVED') { + console.log(`PR #${pullRequest.number} is ${decision || 'not approved'}; no full validation requested.`); + return; + } + + if (pullRequest.head.repo.full_name !== context.payload.repository.full_name) { + core.setFailed('Approved fork PRs require a maintainer branch before full validation can run.'); + return; + } + + await github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'full-docs-validation.yaml', + ref: pullRequest.head.ref, + }); + console.log(`Requested full validation for PR #${pullRequest.number} at ${pullRequest.head.sha}.`); diff --git a/.github/workflows/validate-docs-site.yaml b/.github/workflows/validate-docs-site.yaml index 5e81cd6896..886e357e50 100644 --- a/.github/workflows/validate-docs-site.yaml +++ b/.github/workflows/validate-docs-site.yaml @@ -63,10 +63,46 @@ jobs: cat .release-preview-targets echo "fast=true" >> "$GITHUB_OUTPUT" + - name: Determine preview render scope + id: preview + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: | + set -euo pipefail + if [[ "${{ steps.release-preview.outputs.fast }}" == "true" ]]; then + echo "targeted=true" >> "$GITHUB_OUTPUT" + echo "mode=release" >> "$GITHUB_OUTPUT" + exit 0 + fi + + set +e + python3 .github/scripts/select_docs_preview_targets.py \ + --base "$BASE_SHA" \ + --head HEAD \ + --targets .preview-targets \ + --assets .preview-assets + selector_status=$? + set -e + + case "$selector_status" in + 0) + echo "targeted=true" >> "$GITHUB_OUTPUT" + echo "mode=docs" >> "$GITHUB_OUTPUT" + ;; + 3) + echo "targeted=false" >> "$GITHUB_OUTPUT" + echo "mode=full" >> "$GITHUB_OUTPUT" + ;; + *) + echo "Preview target selection failed with status $selector_status" + exit "$selector_status" + ;; + esac + # Full validation needs generated sources and extra disk headroom. Automated - # release-note previews reuse the already validated staging build instead. + # and safely targeted previews reuse the validated staging build instead. - name: Free space + create reserve - if: steps.release-preview.outputs.fast != 'true' + if: steps.preview.outputs.targeted != 'true' uses: ./.github/actions/free-disk-space with: remove_dotnet: "true" @@ -77,7 +113,7 @@ jobs: create_reserve_gb: "3" - name: Check out validmind-library repository - if: steps.release-preview.outputs.fast != 'true' + if: steps.preview.outputs.targeted != 'true' uses: actions/checkout@v4 with: repository: validmind/validmind-library @@ -85,7 +121,7 @@ jobs: token: ${{ secrets.DOCS_CI_RO_PAT }} - name: Check out installation repository - if: steps.release-preview.outputs.fast != 'true' + if: steps.preview.outputs.targeted != 'true' uses: actions/checkout@v4 with: repository: validmind/installation @@ -96,7 +132,7 @@ jobs: sparse-checkout-cone-mode: true - name: Check out backend repository - if: steps.release-preview.outputs.fast != 'true' + if: steps.preview.outputs.targeted != 'true' uses: actions/checkout@v4 with: repository: validmind/backend @@ -107,7 +143,7 @@ jobs: sparse-checkout-cone-mode: true - name: Set up uv - if: steps.release-preview.outputs.fast != 'true' + if: steps.preview.outputs.targeted != 'true' uses: astral-sh/setup-uv@v5 - name: Verify copyright headers @@ -120,11 +156,13 @@ jobs: with: version: pre-release - - name: Test preview index merge - run: python3 -m unittest discover -s .github/scripts -p 'test_merge_quarto_indexes.py' -v + - name: Test preview selection and index merge + run: | + python3 -m unittest discover -s .github/scripts -p 'test_select_docs_preview_targets.py' -v + python3 -m unittest discover -s .github/scripts -p 'test_merge_quarto_indexes.py' -v - name: Generate Python library docs - if: steps.release-preview.outputs.fast != 'true' + if: steps.preview.outputs.targeted != 'true' run: | cd site/_source/validmind-library make install && make quarto-docs @@ -134,12 +172,12 @@ jobs: rsync -av --exclude '_build' --exclude 'templates' _source/validmind-library/docs/ validmind/ - name: Generate template schema docs - if: steps.release-preview.outputs.fast != 'true' + if: steps.preview.outputs.targeted != 'true' run: | BACKEND_ROOT=site/_source/backend uv run --with json-schema-for-humans python scripts/generate_template_schema_docs.py - name: Populate installation - if: steps.release-preview.outputs.fast != 'true' + if: steps.preview.outputs.targeted != 'true' run: cp -r site/_source/installation/site/installation site/installation - name: Populate release notes @@ -154,20 +192,24 @@ jobs: run: aws configure set aws_access_key_id ${{ secrets.AWS_ACCESS_KEY_ID_STAGING }} && aws configure set aws_secret_access_key ${{ secrets.AWS_SECRET_ACCESS_KEY_STAGING }} && aws configure set default.region us-east-1 - name: Seed targeted render from staging - if: steps.release-preview.outputs.fast == 'true' + if: steps.preview.outputs.targeted == 'true' run: | mkdir -p site/_site/releases site/.preview-indexes site/validmind # The navbar links to this generated source file. The complete rendered # Python API is reused from staging; this placeholder only lets Quarto # resolve the link while rendering release pages. touch site/validmind/validmind.qmd - aws s3 sync s3://validmind-docs-staging/site/releases site/_site/releases \ - --exclude "*" --include "*.html" --no-progress aws s3 cp s3://validmind-docs-staging/site/search.json site/.preview-indexes/search.json --no-progress aws s3 cp s3://validmind-docs-staging/site/listings.json site/.preview-indexes/listings.json --no-progress + - name: Seed release listing descriptions from staging + if: steps.preview.outputs.mode == 'release' + run: | + aws s3 sync s3://validmind-docs-staging/site/releases site/_site/releases \ + --exclude "*" --include "*.html" --no-progress + - name: Render targeted release preview - if: steps.release-preview.outputs.fast == 'true' + if: steps.preview.outputs.mode == 'release' run: | cd site : > render_errors.log @@ -194,8 +236,32 @@ jobs: --base-listings .preview-indexes/listings.json \ --partial-listings _site/listings.json + - name: Render targeted documentation preview + if: steps.preview.outputs.mode == 'docs' + run: | + cd site + : > render_errors.log + while read -r target; do + quarto render --profile development "$target" 2>&1 | tee -a render_errors.log || { + echo "Quarto render failed immediately" + cat render_errors.log + exit 1 + } + done < ../.preview-targets + + while read -r asset; do + [[ -z "$asset" ]] && continue + install -D "$asset" "_site/$asset" + done < ../.preview-assets + + python3 ../.github/scripts/merge_quarto_indexes.py \ + --base-search .preview-indexes/search.json \ + --partial-search _site/search.json \ + --base-listings .preview-indexes/listings.json \ + --partial-listings _site/listings.json + - name: Render demo docs site - if: steps.release-preview.outputs.fast != 'true' + if: steps.preview.outputs.targeted != 'true' run: | cd site quarto render --profile development 2>&1 | tee render_errors.log || { @@ -221,12 +287,18 @@ jobs: - name: Deploy PR preview run: | preview_path="s3://validmind-docs-staging/site/pr_previews/${{ github.head_ref }}" - if [[ "${{ steps.release-preview.outputs.fast }}" == "true" ]]; then + if [[ "${{ steps.preview.outputs.targeted }}" == "true" ]]; then aws s3 sync s3://validmind-docs-staging/site "$preview_path" \ --delete --exclude "pr_previews/*" --exclude "notebooks/EXECUTED/*" --no-progress - aws s3 sync site/_site "$preview_path" \ - --exclude "index.html" --exclude "notebooks/EXECUTED/*" --no-progress \ - --cache-control "no-cache, max-age=0, must-revalidate" + if [[ "${{ steps.preview.outputs.mode }}" == "release" ]]; then + aws s3 sync site/_site "$preview_path" \ + --exclude "index.html" --exclude "notebooks/EXECUTED/*" --no-progress \ + --cache-control "no-cache, max-age=0, must-revalidate" + else + aws s3 sync site/_site "$preview_path" \ + --exclude "notebooks/EXECUTED/*" --no-progress \ + --cache-control "no-cache, max-age=0, must-revalidate" + fi else aws s3 sync site/_site "$preview_path" --delete \ --exclude "notebooks/EXECUTED/*" \ @@ -284,13 +356,13 @@ jobs: console.log(`Dispatched Lighthouse check for PR #${context.issue.number}`); - name: Install pandoc - if: steps.release-preview.outputs.fast != 'true' + if: steps.preview.outputs.targeted != 'true' run: | sudo apt-get update sudo apt-get install -y pandoc - name: Verify chatbot product map is up to date - if: steps.release-preview.outputs.fast != 'true' + if: steps.preview.outputs.targeted != 'true' run: | set -euo pipefail python3 site/scripts/generate_chatbot_product_map.py @@ -323,16 +395,16 @@ jobs: echo "Auto-committed refreshed site/llm/chatbot-product-map.md." - name: Test chatbot product map generator - if: steps.release-preview.outputs.fast != 'true' + if: steps.preview.outputs.targeted != 'true' run: python3 -m unittest discover -s site/scripts -p 'test_generate_chatbot_product_map.py' -v - name: Validate LLM markdown render - if: steps.release-preview.outputs.fast != 'true' + if: steps.preview.outputs.targeted != 'true' run: bash llm/render.sh && bash llm/clean.sh working-directory: site - name: Verify LLM corpus includes product map and docs IA hub - if: steps.release-preview.outputs.fast != 'true' + if: steps.preview.outputs.targeted != 'true' run: | test -f site/llm/_llm-output/chatbot-product-map.md test -f site/llm/_llm-output/AGENTS.md From d9b47cb454b5fa837649dc1b26872094afa4300c Mon Sep 17 00:00:00 2001 From: Andres Rodriguez Date: Tue, 14 Jul 2026 09:56:43 -0700 Subject: [PATCH 14/59] Isolate full validation label concurrency --- .github/workflows/full-docs-validation.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/full-docs-validation.yaml b/.github/workflows/full-docs-validation.yaml index 6da9d703e9..4f25ff7893 100644 --- a/.github/workflows/full-docs-validation.yaml +++ b/.github/workflows/full-docs-validation.yaml @@ -8,7 +8,7 @@ on: workflow_dispatch: concurrency: - group: full-docs-validation-${{ github.event.pull_request.number || github.event.merge_group.head_sha || github.ref }} + group: full-docs-validation-${{ github.event.pull_request.number || github.event.merge_group.head_sha || github.ref }}-${{ github.event.label.name || 'run' }} cancel-in-progress: true permissions: From a5560f783d283472b25d21198bfc1f1640aff111 Mon Sep 17 00:00:00 2001 From: Andres Rodriguez Date: Tue, 14 Jul 2026 09:59:49 -0700 Subject: [PATCH 15/59] Avoid full-history checkout for preview scope --- .../scripts/select_docs_preview_targets.py | 37 ++++++++++--------- .../test_select_docs_preview_targets.py | 8 ++-- .github/workflows/validate-docs-site.yaml | 16 +++++--- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/.github/scripts/select_docs_preview_targets.py b/.github/scripts/select_docs_preview_targets.py index 8632116892..6365556153 100644 --- a/.github/scripts/select_docs_preview_targets.py +++ b/.github/scripts/select_docs_preview_targets.py @@ -7,7 +7,7 @@ from __future__ import annotations import argparse -import subprocess +import sys from dataclasses import dataclass from pathlib import Path, PurePosixPath @@ -22,6 +22,14 @@ "llm", "scripts", } +STATUS_MAP = { + "added": "A", + "modified": "M", + "removed": "D", + "renamed": "R", + "copied": "C", + "changed": "T", +} @dataclass(frozen=True) @@ -67,39 +75,34 @@ def select(changes: list[tuple[str, tuple[str, ...]]]) -> Selection: return Selection(tuple(sorted(targets)), tuple(sorted(assets))) -def parse_name_status(output: str) -> list[tuple[str, tuple[str, ...]]]: +def parse_changed_files(output: str) -> list[tuple[str, tuple[str, ...]]]: changes: list[tuple[str, tuple[str, ...]]] = [] for line in output.splitlines(): fields = line.split("\t") if len(fields) < 2: - raise ValueError(f"Unexpected git diff line: {line!r}") - changes.append((fields[0][0], tuple(fields[1:]))) + raise ValueError(f"Unexpected changed-file line: {line!r}") + status = STATUS_MAP.get(fields[0], fields[0][0].upper()) + changes.append((status, tuple(field for field in fields[1:] if field))) return changes -def git_changes(base: str, head: str) -> list[tuple[str, tuple[str, ...]]]: - result = subprocess.run( - ["git", "diff", "--name-status", "--find-renames", f"{base}...{head}"], - check=True, - capture_output=True, - text=True, - ) - return parse_name_status(result.stdout) - - def write_lines(path: Path, values: tuple[str, ...]) -> None: path.write_text("".join(f"{value}\n" for value in values)) def main() -> int: parser = argparse.ArgumentParser() - parser.add_argument("--base", required=True) - parser.add_argument("--head", default="HEAD") + parser.add_argument("--changes", default="-") parser.add_argument("--targets", type=Path, required=True) parser.add_argument("--assets", type=Path, required=True) args = parser.parse_args() - selection = select(git_changes(args.base, args.head)) + if args.changes == "-": + changed_files = sys.stdin.read() + else: + changed_files = Path(args.changes).read_text() + + selection = select(parse_changed_files(changed_files)) if not selection.is_targeted: print(f"Full render required: {selection.fallback_reason}") return 3 diff --git a/.github/scripts/test_select_docs_preview_targets.py b/.github/scripts/test_select_docs_preview_targets.py index e43210e905..63edb3be40 100644 --- a/.github/scripts/test_select_docs_preview_targets.py +++ b/.github/scripts/test_select_docs_preview_targets.py @@ -3,7 +3,7 @@ import unittest -from select_docs_preview_targets import parse_name_status, select +from select_docs_preview_targets import parse_changed_files, select class SelectDocsPreviewTargetsTest(unittest.TestCase): @@ -50,13 +50,13 @@ def test_generated_corpus_change_requires_full_render(self): self.assertFalse(result.is_targeted) def test_parses_renames_for_safe_fallback(self): - changes = parse_name_status( - "R100\tsite/guide/old.qmd\tsite/guide/new.qmd\n" + changes = parse_changed_files( + "renamed\tsite/guide/new.qmd\tsite/guide/old.qmd\n" ) self.assertEqual( changes, - [("R", ("site/guide/old.qmd", "site/guide/new.qmd"))], + [("R", ("site/guide/new.qmd", "site/guide/old.qmd"))], ) self.assertFalse(select(changes).is_targeted) diff --git a/.github/workflows/validate-docs-site.yaml b/.github/workflows/validate-docs-site.yaml index 886e357e50..48e506435a 100644 --- a/.github/workflows/validate-docs-site.yaml +++ b/.github/workflows/validate-docs-site.yaml @@ -10,6 +10,10 @@ permissions: issues: write pull-requests: write +concurrency: + group: validate-docs-${{ github.event.pull_request.number }} + cancel-in-progress: true + jobs: validate: runs-on: ubuntu-latest @@ -20,7 +24,7 @@ jobs: uses: actions/checkout@v4 with: ref: ${{ github.head_ref }} - fetch-depth: 0 + fetch-depth: 1 token: ${{ secrets.GITHUB_TOKEN }} - name: Check out release-notes repository @@ -66,7 +70,8 @@ jobs: - name: Determine preview render scope id: preview env: - BASE_SHA: ${{ github.event.pull_request.base.sha }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} run: | set -euo pipefail if [[ "${{ steps.release-preview.outputs.fast }}" == "true" ]]; then @@ -76,9 +81,10 @@ jobs: fi set +e - python3 .github/scripts/select_docs_preview_targets.py \ - --base "$BASE_SHA" \ - --head HEAD \ + gh api --paginate "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files" \ + --jq '.[] | [.status, .filename, (.previous_filename // "")] | @tsv' \ + | python3 .github/scripts/select_docs_preview_targets.py \ + --changes - \ --targets .preview-targets \ --assets .preview-assets selector_status=$? From 5dec66e04ad5271fa1bd1c9fd4233160a84c5bd7 Mon Sep 17 00:00:00 2001 From: Andres Rodriguez Date: Tue, 14 Jul 2026 10:11:52 -0700 Subject: [PATCH 16/59] Gate production deploys on validated artifacts --- .github/workflows/deploy-docs-prod.yaml | 62 ++++++++----------- .github/workflows/deploy-docs-staging.yaml | 48 ++++++++++++++ .github/workflows/full-docs-validation.yaml | 4 +- .../request-full-docs-validation.yaml | 57 ----------------- 4 files changed, 75 insertions(+), 96 deletions(-) delete mode 100644 .github/workflows/request-full-docs-validation.yaml diff --git a/.github/workflows/deploy-docs-prod.yaml b/.github/workflows/deploy-docs-prod.yaml index f776ba1f9a..a5cf93268d 100644 --- a/.github/workflows/deploy-docs-prod.yaml +++ b/.github/workflows/deploy-docs-prod.yaml @@ -37,21 +37,31 @@ jobs: run: | set -euo pipefail name="docs-production-$(git rev-parse 'HEAD^{tree}')" - run_id=$(gh api "repos/${{ github.repository }}/actions/artifacts?name=$name&per_page=100" \ - --jq '[.artifacts[] | select(.expired == false)] | sort_by(.created_at) | reverse | .[0].workflow_run.id // empty') - echo "name=$name" >> "$GITHUB_OUTPUT" - if [[ -n "$run_id" ]]; then - echo "Found $name in workflow run $run_id" - echo "found=true" >> "$GITHUB_OUTPUT" - echo "run_id=$run_id" >> "$GITHUB_OUTPUT" - else - echo "No matching artifact found; falling back to a full production build." - echo "found=false" >> "$GITHUB_OUTPUT" + + run_id="" + while read -r candidate; do + [[ -z "$candidate" ]] && continue + run=$(gh api "repos/${{ github.repository }}/actions/runs/$candidate") + run_path=$(jq -r .path <<< "$run") + conclusion=$(jq -r .conclusion <<< "$run") + if [[ "$run_path" == ".github/workflows/deploy-docs-staging.yaml" && "$conclusion" == "success" ]]; then + run_id="$candidate" + break + fi + echo "Ignoring $name from untrusted or unsuccessful workflow run $candidate ($run_path: $conclusion)" + done < <(gh api "repos/${{ github.repository }}/actions/artifacts?name=$name&per_page=100" \ + --jq '[.artifacts[] | select(.expired == false)] | sort_by(.created_at) | reverse | .[].workflow_run.id') + + if [[ -z "$run_id" ]]; then + echo "::error::No fully validated production artifact named $name was produced by a successful staging workflow. Production was not modified." + exit 1 fi + echo "Found validated $name in staging workflow run $run_id" + echo "run_id=$run_id" >> "$GITHUB_OUTPUT" + - name: Download prebuilt production docs - if: steps.production-artifact.outputs.found == 'true' uses: actions/download-artifact@v4 with: name: ${{ steps.production-artifact.outputs.name }} @@ -61,35 +71,15 @@ jobs: run-id: ${{ steps.production-artifact.outputs.run_id }} - name: Extract prebuilt production docs - if: steps.production-artifact.outputs.found == 'true' run: | mkdir -p site/_site tar --zstd -xf "$RUNNER_TEMP/production-artifact/docs-production.tar.zst" -C site/_site - # Reclaim space only when the prebuilt artifact is unavailable and the - # workflow must perform the original full-site build. - - name: Free space + create reserve - if: steps.production-artifact.outputs.found != 'true' - uses: ./.github/actions/free-disk-space - with: - remove_dotnet: "true" - remove_android: "true" - remove_haskell: "true" - prune_docker: "true" - apt_cleanup: "true" - create_reserve_gb: "3" - - - name: Build production docs site - if: steps.production-artifact.outputs.found != 'true' - uses: ./.github/actions/build-docs-site - with: - profile: production - docs_ci_ro_pat: ${{ secrets.DOCS_CI_RO_PAT }} - quarto_version: ${{ vars.QUARTO_VERSION }} - library_ref: main - installation_ref: main - release_notes_ref: main - backend_ref: main + - name: Verify production artifact contents + run: | + test -s site/_site/index.html + test -s site/_site/search.json + test -s site/_site/listings.json # Prod bucket is in us-east-1 - name: Configure AWS credentials diff --git a/.github/workflows/deploy-docs-staging.yaml b/.github/workflows/deploy-docs-staging.yaml index 1681ea6d5c..38d88dbb4b 100644 --- a/.github/workflows/deploy-docs-staging.yaml +++ b/.github/workflows/deploy-docs-staging.yaml @@ -73,6 +73,10 @@ jobs: git switch --detach origin/prod git merge --no-commit --no-ff "$source_sha" + - name: Verify copyright headers + if: matrix.profile == 'production' + run: make -C site verify-copyright + # Reclaim space + create a reserve for deterministic headroom - name: Free space + create reserve uses: ./.github/actions/free-disk-space @@ -95,6 +99,49 @@ jobs: release_notes_ref: ${{ needs.resolve-sources.outputs.release_notes }} backend_ref: ${{ needs.resolve-sources.outputs.backend }} + - name: Test production render for warnings or errors + if: matrix.profile == 'production' + run: | + if grep -q 'WARN\|WARNING\|ERROR:' site/render_errors.log; then + echo "Warnings or errors detected during the production render" + cat site/render_errors.log + exit 1 + fi + echo "No warnings or errors detected during the production render" + + - name: Install pandoc + if: matrix.profile == 'production' + run: | + sudo apt-get update + sudo apt-get install -y pandoc + + - name: Verify chatbot product map is up to date + if: matrix.profile == 'production' + run: | + set -euo pipefail + python3 site/scripts/generate_chatbot_product_map.py + git diff --exit-code -- \ + site/llm/chatbot-product-map.md \ + site/llm/chatbot-product-map-frontend-snapshot.json + + - name: Test chatbot product map generator + if: matrix.profile == 'production' + run: python3 -m unittest discover -s site/scripts -p 'test_generate_chatbot_product_map.py' -v + + - name: Validate LLM markdown render + if: matrix.profile == 'production' + run: bash llm/render.sh && bash llm/clean.sh + working-directory: site + + - name: Verify required LLM corpus content + if: matrix.profile == 'production' + run: | + test -f site/llm/_llm-output/chatbot-product-map.md + test -f site/llm/_llm-output/AGENTS.md + test -f site/llm/_llm-output/about/using-the-documentation.md + test ! -f site/llm/_llm-output/about/contributing/validmind-community.md + test ! -d site/llm/_llm-output/about/contributing/style-guide + - name: Add robots.txt for staging if: matrix.profile == 'staging' run: cp site/environments/robots-staging.txt site/_site/robots.txt @@ -140,6 +187,7 @@ jobs: site/_source/backend site/render_errors.log site/_freeze + site/llm/_llm-output dev.env valid.env diff --git a/.github/workflows/full-docs-validation.yaml b/.github/workflows/full-docs-validation.yaml index 4f25ff7893..68900beca3 100644 --- a/.github/workflows/full-docs-validation.yaml +++ b/.github/workflows/full-docs-validation.yaml @@ -1,14 +1,12 @@ name: Full docs validation on: - merge_group: - types: [checks_requested] pull_request: types: [labeled] workflow_dispatch: concurrency: - group: full-docs-validation-${{ github.event.pull_request.number || github.event.merge_group.head_sha || github.ref }}-${{ github.event.label.name || 'run' }} + group: full-docs-validation-${{ github.event.pull_request.number || github.ref }}-${{ github.event.label.name || 'run' }} cancel-in-progress: true permissions: diff --git a/.github/workflows/request-full-docs-validation.yaml b/.github/workflows/request-full-docs-validation.yaml deleted file mode 100644 index fdc186c397..0000000000 --- a/.github/workflows/request-full-docs-validation.yaml +++ /dev/null @@ -1,57 +0,0 @@ -name: Request full docs validation - -on: - pull_request: - types: [synchronize] - pull_request_review: - types: [submitted] - -permissions: - actions: write - contents: read - pull-requests: read - -jobs: - request: - if: >- - github.event_name != 'pull_request_review' || - github.event.review.state == 'approved' - runs-on: ubuntu-latest - steps: - - name: Dispatch validation for approved revision - uses: actions/github-script@v7 - with: - script: | - const pullRequest = context.payload.pull_request; - const result = await github.graphql(` - query($owner: String!, $repo: String!, $number: Int!) { - repository(owner: $owner, name: $repo) { - pullRequest(number: $number) { - reviewDecision - } - } - } - `, { - owner: context.repo.owner, - repo: context.repo.repo, - number: pullRequest.number, - }); - - const decision = result.repository.pullRequest.reviewDecision; - if (decision !== 'APPROVED') { - console.log(`PR #${pullRequest.number} is ${decision || 'not approved'}; no full validation requested.`); - return; - } - - if (pullRequest.head.repo.full_name !== context.payload.repository.full_name) { - core.setFailed('Approved fork PRs require a maintainer branch before full validation can run.'); - return; - } - - await github.rest.actions.createWorkflowDispatch({ - owner: context.repo.owner, - repo: context.repo.repo, - workflow_id: 'full-docs-validation.yaml', - ref: pullRequest.head.ref, - }); - console.log(`Requested full validation for PR #${pullRequest.number} at ${pullRequest.head.sha}.`); From c1dcb6f2a1d9e09151b7652fba2b6655af641d72 Mon Sep 17 00:00:00 2001 From: Andres Rodriguez Date: Tue, 14 Jul 2026 10:14:14 -0700 Subject: [PATCH 17/59] Bridge targeted validation into merge queue --- .../workflows/validate-docs-merge-group.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/validate-docs-merge-group.yaml diff --git a/.github/workflows/validate-docs-merge-group.yaml b/.github/workflows/validate-docs-merge-group.yaml new file mode 100644 index 0000000000..dd05a64f4b --- /dev/null +++ b/.github/workflows/validate-docs-merge-group.yaml @@ -0,0 +1,18 @@ +name: Validate docs merge group + +on: + merge_group: + types: [checks_requested] + +permissions: + contents: read + +jobs: + validate: + name: validate + runs-on: ubuntu-latest + steps: + - name: Confirm queued revision + run: | + echo "The pull request revision passed its targeted preview validation." + echo "The complete production site will be built and validated after merge before production deployment is allowed." From 0a5e8c4da8022cf0917374635ed775d27d32dd73 Mon Sep 17 00:00:00 2001 From: Andres Rodriguez Date: Tue, 14 Jul 2026 10:47:44 -0700 Subject: [PATCH 18/59] Document docs CI and deployment invariants --- AGENTS.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 52f3c75e8c..3a98ccb3d9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,3 +48,32 @@ Documentation is written in Quarto Markdown (`.qmd`). Key conventions: - Variables use `{{< var name >}}` syntax (defined in `_variables.yml`) - Cross-references use relative paths ending in `.qmd` - Images are stored alongside their `.qmd` files + +## Pull requests and release notes + +Documentation pull requests must follow the repository's release-note policy: + +- Internal workflow, tooling, or maintenance changes use the `internal` label. +- External changes use an appropriate release-note label and include content in the pull request's release-notes section. + +The required `validate` check is the pull-request feedback gate. For ordinary content changes, it renders only safely targetable changed pages and assets and builds the preview on top of the validated staging site. It falls back to a complete preview render for changes that can have global or ambiguous effects, including Quarto configuration or metadata, generated content, deletions, and renames. + +## Documentation delivery + +Documentation moves through `main` → `staging` → `prod`: + +1. Pull requests into `main` receive preview validation and normal review. +2. After merge, the staging workflow renders the complete staging site and the prospective production site in parallel. +3. The prospective production build runs the complete production-profile validation and uploads an immutable artifact keyed to the exact Git tree that a `staging` → `prod` merge will create. +4. The production workflow deploys only that exact-tree artifact from a successful staging workflow run. If the artifact is missing, expired, or came from another workflow, production deployment must fail before loading AWS credentials or modifying production. + +The merge-queue `validate` bridge does not render the site again. It records that the pull-request revision passed preview validation; the complete production safety boundary is the post-merge staging artifact. + +## CI invariants + +When changing documentation workflows, preserve these constraints: + +- Do not add a fallback build to the production deployment workflow. Missing validated artifacts must fail closed. +- Keep full Git history available when preparing the prospective production tree. A shallow checkout cannot establish the shared `staging`/`prod` history and causes Git to reject the merge as unrelated histories. +- Keep targeted preview runs cancelable so a newer commit supersedes obsolete work. +- Treat preview rendering and production validation as different responsibilities: previews provide fast author feedback; only the complete post-merge production-profile build can authorize a production artifact. From 5ee968e75259bf24c85a7aa1b1620f8c59b4e4c0 Mon Sep 17 00:00:00 2001 From: Andres Rodriguez Date: Tue, 14 Jul 2026 13:34:57 -0700 Subject: [PATCH 19/59] chore: use release-notes main for v2.13.5 preview --- .github/workflows/validate-docs-site.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/validate-docs-site.yaml b/.github/workflows/validate-docs-site.yaml index c7982743e8..48e506435a 100644 --- a/.github/workflows/validate-docs-site.yaml +++ b/.github/workflows/validate-docs-site.yaml @@ -31,7 +31,6 @@ jobs: uses: actions/checkout@v4 with: repository: validmind/release-notes - ref: automated/library-v2.13.5 path: site/_source/release-notes token: ${{ secrets.DOCS_CI_RO_PAT }} sparse-checkout: | From 74648660aac31942cfc727839f29f288374ab4ba Mon Sep 17 00:00:00 2001 From: Andres Rodriguez Date: Tue, 14 Jul 2026 17:56:36 -0700 Subject: [PATCH 20/59] ci: add reusable release notes preview workflow --- .github/workflows/release-notes-preview.yaml | 217 +++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 .github/workflows/release-notes-preview.yaml diff --git a/.github/workflows/release-notes-preview.yaml b/.github/workflows/release-notes-preview.yaml new file mode 100644 index 0000000000..eb2c1aeea7 --- /dev/null +++ b/.github/workflows/release-notes-preview.yaml @@ -0,0 +1,217 @@ +name: Release notes preview + +run-name: Release notes preview | ${{ github.event.client_payload.correlation_id || inputs.correlation_id }} + +on: + workflow_call: + inputs: + release_notes_sha: + description: Immutable commit from validmind/release-notes + required: true + type: string + release_notes_ref: + description: Human-readable source branch + required: true + type: string + release_notes_pr: + description: Pull request number in validmind/release-notes + required: true + type: string + correlation_id: + description: Unique identifier used by callers to find this run + required: true + type: string + outputs: + preview_url: + description: Deployed documentation preview URL + value: ${{ jobs.preview.outputs.preview_url }} + secrets: + docs_ci_ro_pat: + required: true + aws_access_key_id_staging: + required: true + aws_secret_access_key_staging: + required: true + repository_dispatch: + types: [release-notes-preview] + +permissions: + contents: read + +concurrency: + group: release-notes-preview-${{ github.event.client_payload.release_notes_pr || inputs.release_notes_pr }} + cancel-in-progress: true + +jobs: + preview: + runs-on: ubuntu-latest + outputs: + preview_url: ${{ steps.source.outputs.preview_url }} + steps: + - name: Normalize and validate source request + id: source + env: + CALL_SHA: ${{ inputs.release_notes_sha }} + CALL_REF: ${{ inputs.release_notes_ref }} + CALL_PR: ${{ inputs.release_notes_pr }} + DISPATCH_SHA: ${{ github.event.client_payload.release_notes_sha }} + DISPATCH_REF: ${{ github.event.client_payload.release_notes_ref }} + DISPATCH_PR: ${{ github.event.client_payload.release_notes_pr }} + GH_TOKEN: ${{ secrets.DOCS_CI_RO_PAT || secrets.docs_ci_ro_pat }} + run: | + set -euo pipefail + sha="${DISPATCH_SHA:-$CALL_SHA}" + ref="${DISPATCH_REF:-$CALL_REF}" + pr="${DISPATCH_PR:-$CALL_PR}" + + [[ "$sha" =~ ^[0-9a-f]{40}$ ]] || { echo "Invalid release-notes SHA"; exit 1; } + [[ "$pr" =~ ^[0-9]+$ ]] || { echo "Invalid release-notes PR number"; exit 1; } + [[ "$ref" == automated/* || "$ref" == codex/* ]] || { + echo "Release-notes preview refs must use an approved branch prefix" + exit 1 + } + + actual_sha=$(gh api "repos/validmind/release-notes/pulls/$pr" --jq .head.sha) + actual_ref=$(gh api "repos/validmind/release-notes/pulls/$pr" --jq .head.ref) + actual_repo=$(gh api "repos/validmind/release-notes/pulls/$pr" --jq .head.repo.full_name) + state=$(gh api "repos/validmind/release-notes/pulls/$pr" --jq .state) + [[ "$actual_sha" == "$sha" && "$actual_ref" == "$ref" ]] || { + echo "Dispatch payload does not match release-notes PR #$pr" + exit 1 + } + [[ "$actual_repo" == "validmind/release-notes" && "$state" == "open" ]] || { + echo "Only open, same-repository release-notes PRs may deploy previews" + exit 1 + } + + preview_key="release-notes/pr-$pr" + echo "sha=$sha" >> "$GITHUB_OUTPUT" + echo "ref=$ref" >> "$GITHUB_OUTPUT" + echo "pr=$pr" >> "$GITHUB_OUTPUT" + echo "preview_key=$preview_key" >> "$GITHUB_OUTPUT" + echo "preview_url=https://docs-staging.validmind.ai/pr_previews/$preview_key/index.html" >> "$GITHUB_OUTPUT" + + - name: Check out documentation repository + uses: actions/checkout@v4 + with: + ref: main + fetch-depth: 1 + + - name: Check out release-notes revision + uses: actions/checkout@v4 + with: + repository: validmind/release-notes + ref: ${{ steps.source.outputs.sha }} + path: site/_source/release-notes + token: ${{ secrets.DOCS_CI_RO_PAT || secrets.docs_ci_ro_pat }} + sparse-checkout: | + releases + sparse-checkout-cone-mode: true + + - name: Select changed release directories + env: + GH_TOKEN: ${{ secrets.DOCS_CI_RO_PAT || secrets.docs_ci_ro_pat }} + run: | + set -euo pipefail + git -C site/_source/release-notes fetch --depth=1 origin main + git -C site/_source/release-notes diff --name-only --diff-filter=ACMRT \ + FETCH_HEAD HEAD -- releases \ + | awk -F/ 'NF >= 4 { print $1 "/" $2 "/" $3 }' \ + | sort -u > .release-preview-targets + if [[ ! -s .release-preview-targets ]]; then + echo "No changed release directories found" + exit 1 + fi + echo "Targeted release directories:" + cat .release-preview-targets + + - name: Verify copyright headers + run: make -C site verify-copyright + + - name: Set up Quarto + uses: quarto-dev/quarto-actions/setup@v2 + with: + version: pre-release + + - name: Test preview index merge + run: python3 -m unittest discover -s .github/scripts -p 'test_merge_quarto_indexes.py' -v + + - name: Populate release notes + run: | + cp -r site/_source/release-notes/releases site + rm -f site/releases/backend-releases.qmd site/releases/cmvm-releases.qmd + + - name: Configure AWS credentials + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID_STAGING || secrets.aws_access_key_id_staging }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY_STAGING || secrets.aws_secret_access_key_staging }} + run: | + aws configure set aws_access_key_id "$AWS_ACCESS_KEY_ID" + aws configure set aws_secret_access_key "$AWS_SECRET_ACCESS_KEY" + aws configure set default.region us-east-1 + + - name: Seed targeted render from staging + run: | + mkdir -p site/_site/releases site/.preview-indexes site/validmind + touch site/validmind/validmind.qmd + aws s3 cp s3://validmind-docs-staging/site/search.json site/.preview-indexes/search.json --no-progress + aws s3 cp s3://validmind-docs-staging/site/listings.json site/.preview-indexes/listings.json --no-progress + aws s3 sync s3://validmind-docs-staging/site/releases site/_site/releases \ + --exclude "*" --include "*.html" --no-progress + + - name: Render targeted release preview + run: | + set -euo pipefail + cd site + : > render_errors.log + while read -r target; do + quarto render --profile development "$target" 2>&1 | tee -a render_errors.log + done < ../.release-preview-targets + + for target in releases/*.qmd; do + [[ "$(basename "$target")" == _* ]] && continue + quarto render --profile development "$target" 2>&1 | tee -a render_errors.log + done + + python3 ../.github/scripts/merge_quarto_indexes.py \ + --base-search .preview-indexes/search.json \ + --partial-search _site/search.json \ + --base-listings .preview-indexes/listings.json \ + --partial-listings _site/listings.json + + - name: Add robots.txt + run: cp site/environments/robots-staging.txt site/_site/robots.txt + + - name: Test for warnings or errors + run: | + if grep -q 'WARN\|WARNING\|ERROR:' site/render_errors.log; then + echo "Warnings or errors detected during Quarto render" + cat site/render_errors.log + exit 1 + fi + echo "No warnings or errors detected during Quarto render" + + - name: Deploy release-notes preview + env: + PREVIEW_KEY: ${{ steps.source.outputs.preview_key }} + run: | + preview_path="s3://validmind-docs-staging/site/pr_previews/$PREVIEW_KEY" + aws s3 sync s3://validmind-docs-staging/site "$preview_path" \ + --delete --exclude "pr_previews/*" --exclude "notebooks/EXECUTED/*" --no-progress + aws s3 sync site/_site "$preview_path" \ + --exclude "index.html" --exclude "notebooks/EXECUTED/*" --no-progress \ + --cache-control "no-cache, max-age=0, must-revalidate" + aws cloudfront create-invalidation \ + --distribution-id ESWVTZYFL873V --paths "/*" --no-cli-pager + + - name: Publish workflow summary + env: + PREVIEW_URL: ${{ steps.source.outputs.preview_url }} + RELEASE_NOTES_PR: ${{ steps.source.outputs.pr }} + run: | + { + echo "## Release notes preview" + echo + echo "- Source: validmind/release-notes#$RELEASE_NOTES_PR" + echo "- Preview: $PREVIEW_URL" + } >> "$GITHUB_STEP_SUMMARY" From 1e4ef61d0ecb25fd03603c820e7aa14b71149a43 Mon Sep 17 00:00:00 2001 From: Andres Rodriguez Date: Tue, 14 Jul 2026 17:58:57 -0700 Subject: [PATCH 21/59] fix: remove stale release notes preview ref --- .github/workflows/validate-docs-site.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/validate-docs-site.yaml b/.github/workflows/validate-docs-site.yaml index 3bae790891..48e506435a 100644 --- a/.github/workflows/validate-docs-site.yaml +++ b/.github/workflows/validate-docs-site.yaml @@ -31,7 +31,6 @@ jobs: uses: actions/checkout@v4 with: repository: validmind/release-notes - ref: automated/patch-26.07.02 path: site/_source/release-notes token: ${{ secrets.DOCS_CI_RO_PAT }} sparse-checkout: | From e60aa79da26a4f57d76b3a23b4db00aea6e34417 Mon Sep 17 00:00:00 2001 From: Andres Rodriguez Date: Tue, 14 Jul 2026 21:22:57 -0700 Subject: [PATCH 22/59] ci: deploy staging for published release notes --- .github/workflows/deploy-docs-staging.yaml | 9 +++++++++ AGENTS.md | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/.github/workflows/deploy-docs-staging.yaml b/.github/workflows/deploy-docs-staging.yaml index 38d88dbb4b..f9f6348ced 100644 --- a/.github/workflows/deploy-docs-staging.yaml +++ b/.github/workflows/deploy-docs-staging.yaml @@ -1,5 +1,7 @@ name: Deploy docs site to staging +run-name: Deploy docs site to staging | ${{ github.event.client_payload.correlation_id || github.sha }} + on: push: branches: @@ -8,6 +10,8 @@ on: workflows: ["Merge main into staging"] types: - completed + repository_dispatch: + types: [release-notes-published] workflow_dispatch: concurrency: @@ -20,6 +24,7 @@ jobs: if: | github.event_name == 'push' || (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') || + github.event_name == 'repository_dispatch' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest outputs: @@ -43,6 +48,7 @@ jobs: if: | github.event_name == 'push' || (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') || + github.event_name == 'repository_dispatch' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest needs: resolve-sources @@ -55,6 +61,9 @@ jobs: - name: Check out documentation repository uses: actions/checkout@v4 with: + # A release-notes publication changes an external source, not this + # repository. Build it against the currently promoted staging tree. + ref: ${{ github.event_name == 'repository_dispatch' && 'staging' || '' }} # The production build predicts the staging-to-prod merge tree and # therefore needs the shared branch history, not a depth-one clone. fetch-depth: 0 diff --git a/AGENTS.md b/AGENTS.md index 3a98ccb3d9..414362585d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -67,6 +67,12 @@ Documentation moves through `main` → `staging` → `prod`: 3. The prospective production build runs the complete production-profile validation and uploads an immutable artifact keyed to the exact Git tree that a `staging` → `prod` merge will create. 4. The production workflow deploys only that exact-tree artifact from a successful staging workflow run. If the artifact is missing, expired, or came from another workflow, production deployment must fail before loading AWS credentials or modifying production. +Release-note content is sourced from `validmind/release-notes`, so merging a +release-notes pull request does not create or merge a documentation pull request. +Instead, the release-notes repository dispatches the staging deployment directly; +that deployment uses the current documentation `staging` tree and the latest +release-notes `main` revision. + The merge-queue `validate` bridge does not render the site again. It records that the pull-request revision passed preview validation; the complete production safety boundary is the post-merge staging artifact. ## CI invariants From a9b37e11ee95dd6608fca15dad36ef31006bc2e6 Mon Sep 17 00:00:00 2001 From: Andres Rodriguez Date: Tue, 14 Jul 2026 21:33:15 -0700 Subject: [PATCH 23/59] ci: preserve main-to-staging publication chain --- .github/workflows/deploy-docs-staging.yaml | 9 +-------- .github/workflows/merge-main-into-staging.yaml | 6 +++++- AGENTS.md | 7 ++++--- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/.github/workflows/deploy-docs-staging.yaml b/.github/workflows/deploy-docs-staging.yaml index f9f6348ced..872c378bf1 100644 --- a/.github/workflows/deploy-docs-staging.yaml +++ b/.github/workflows/deploy-docs-staging.yaml @@ -1,6 +1,6 @@ name: Deploy docs site to staging -run-name: Deploy docs site to staging | ${{ github.event.client_payload.correlation_id || github.sha }} +run-name: Deploy docs site to staging | ${{ github.event.workflow_run.id || github.sha }} on: push: @@ -10,8 +10,6 @@ on: workflows: ["Merge main into staging"] types: - completed - repository_dispatch: - types: [release-notes-published] workflow_dispatch: concurrency: @@ -24,7 +22,6 @@ jobs: if: | github.event_name == 'push' || (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') || - github.event_name == 'repository_dispatch' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest outputs: @@ -48,7 +45,6 @@ jobs: if: | github.event_name == 'push' || (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') || - github.event_name == 'repository_dispatch' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest needs: resolve-sources @@ -61,9 +57,6 @@ jobs: - name: Check out documentation repository uses: actions/checkout@v4 with: - # A release-notes publication changes an external source, not this - # repository. Build it against the currently promoted staging tree. - ref: ${{ github.event_name == 'repository_dispatch' && 'staging' || '' }} # The production build predicts the staging-to-prod merge tree and # therefore needs the shared branch history, not a depth-one clone. fetch-depth: 0 diff --git a/.github/workflows/merge-main-into-staging.yaml b/.github/workflows/merge-main-into-staging.yaml index fd825af4e6..d132355b18 100644 --- a/.github/workflows/merge-main-into-staging.yaml +++ b/.github/workflows/merge-main-into-staging.yaml @@ -1,9 +1,13 @@ name: Merge main into staging +run-name: Merge main into staging | ${{ github.event.client_payload.correlation_id || github.sha }} + on: push: branches: - main + repository_dispatch: + types: [release-notes-published] permissions: contents: write @@ -77,4 +81,4 @@ jobs: if: ${{ success() && steps.pr-number.outputs.pull-request-number != '' }} run: gh api -X DELETE "repos/${{ github.repository }}/git/refs/heads/update-staging-${{ github.run_id }}" env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/AGENTS.md b/AGENTS.md index 414362585d..dd7fa67ec4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -69,9 +69,10 @@ Documentation moves through `main` → `staging` → `prod`: Release-note content is sourced from `validmind/release-notes`, so merging a release-notes pull request does not create or merge a documentation pull request. -Instead, the release-notes repository dispatches the staging deployment directly; -that deployment uses the current documentation `staging` tree and the latest -release-notes `main` revision. +Instead, the release-notes repository dispatches the existing **Merge main into +staging** workflow. Its successful completion triggers the staging deployment, +which builds the promoted documentation tree with the latest release-notes +`main` revision. The merge-queue `validate` bridge does not render the site again. It records that the pull-request revision passed preview validation; the complete production safety boundary is the post-merge staging artifact. From 9b9f95a259929e36874611aeb754b5bfd5f0d3d7 Mon Sep 17 00:00:00 2001 From: Juan Date: Wed, 22 Jul 2026 18:12:43 +0200 Subject: [PATCH 24/59] Add risk tiering user guide Add a new Risk tiering section under Guides covering the Risk Tier Engine: an overview page plus how-tos for managing templates, configuring tier calculation, and managing assessments. Wire the section into the guide sidebar, the Guides landing page, and the footer navigation. Co-Authored-By: Claude Opus 4.8 (1M context) --- site/_quarto.yml | 1 + site/guide/_sidebar.yaml | 8 + site/guide/guides.qmd | 16 ++ .../configure-risk-tier-calculation.qmd | 268 ++++++++++++++++++ .../manage-risk-tier-assessments.qmd | 179 ++++++++++++ .../manage-risk-tier-templates.qmd | 128 +++++++++ .../working-with-risk-tiering.qmd | 96 +++++++ 7 files changed, 696 insertions(+) create mode 100644 site/guide/risk-tiering/configure-risk-tier-calculation.qmd create mode 100644 site/guide/risk-tiering/manage-risk-tier-assessments.qmd create mode 100644 site/guide/risk-tiering/manage-risk-tier-templates.qmd create mode 100644 site/guide/risk-tiering/working-with-risk-tiering.qmd diff --git a/site/_quarto.yml b/site/_quarto.yml index 9cdad331c7..7a9d1a8903 100644 --- a/site/_quarto.yml +++ b/site/_quarto.yml @@ -205,6 +205,7 @@ website:
  • Integrations
  • Workflows
  • Inventory
  • +
  • Risk tiering