From 1e82449220a5c062c771c30c8af6ebc82d1d2363 Mon Sep 17 00:00:00 2001 From: openhands Date: Sat, 3 Oct 2026 18:32:24 +0000 Subject: [PATCH] Guard generated Cookbook pages from enterprise-cookbook Fails PRs that edit cookbook/ outside the enterprise-cookbook sync branches, excludes cookbook/ from sync_code_blocks.py, and documents the flow in AGENTS.md. Co-authored-by: openhands --- .github/scripts/sync_code_blocks.py | 9 ++++++- .github/workflows/cookbook-generated.yml | 30 ++++++++++++++++++++++++ AGENTS.md | 12 ++++++++++ tests/test_sync_code_blocks.py | 20 +++++++++++++++- 4 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/cookbook-generated.yml diff --git a/.github/scripts/sync_code_blocks.py b/.github/scripts/sync_code_blocks.py index e57f65269..30838648e 100755 --- a/.github/scripts/sync_code_blocks.py +++ b/.github/scripts/sync_code_blocks.py @@ -20,10 +20,17 @@ from pathlib import Path +# Generated from OpenHands/enterprise-cookbook, whose code-block paths are +# relative to each example, not to software-agent-sdk. +EXCLUDED_DIRS = {"cookbook"} + + def find_mdx_files(docs_path: Path) -> list[Path]: """Find all MDX files in the docs directory.""" mdx_files: list[Path] = [] - for root, _, files in os.walk(docs_path): + for root, dirs, files in os.walk(docs_path): + if Path(root) == docs_path: + dirs[:] = [d for d in dirs if d not in EXCLUDED_DIRS] for file in files: if file.endswith(".mdx"): mdx_files.append(Path(root) / file) diff --git a/.github/workflows/cookbook-generated.yml b/.github/workflows/cookbook-generated.yml new file mode 100644 index 000000000..bb4d45f2a --- /dev/null +++ b/.github/workflows/cookbook-generated.yml @@ -0,0 +1,30 @@ +name: Cookbook is generated + +# Everything under cookbook/ is generated from OpenHands/enterprise-cookbook by its +# docs-preview and docs-publish workflows, which push only cookbook-preview/* and +# cookbook-sync branches. Any other change would be overwritten by the next sync. +on: + pull_request: + paths: + - 'cookbook/**' + +permissions: + contents: read + +jobs: + guard: + name: Cookbook pages come from enterprise-cookbook + runs-on: ubuntu-latest + steps: + - name: Allow only the enterprise-cookbook sync branches + env: + HEAD_REF: ${{ github.head_ref }} + HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} + run: | + if [ "$HEAD_REPO" = "$GITHUB_REPOSITORY" ]; then + case "$HEAD_REF" in + cookbook-sync|cookbook-preview/*) echo "Generated update from OpenHands/enterprise-cookbook."; exit 0 ;; + esac + fi + echo "::error::Pages under cookbook/ are generated from OpenHands/enterprise-cookbook. Change the example's README.md or example.yaml there instead: https://github.com/OpenHands/enterprise-cookbook/tree/main/tools/docs-render" + exit 1 diff --git a/AGENTS.md b/AGENTS.md index 0c2f8592b..8118950fd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -153,6 +153,18 @@ Workflow: `.github/workflows/sync-agent-sdk-openapi.yml` - Runs the agent-server OpenAPI generator - Updates `openapi/agent-sdk.json` via an automated PR +### 4) Cookbook tab generated from `OpenHands/enterprise-cookbook` + +Every page under `cookbook/` and the `Cookbook` tab in `docs.json` are generated from example READMEs in +`OpenHands/enterprise-cookbook` by its `tools/docs-render` converter. **Do not edit them here**; change the +example's `README.md` or `example.yaml` in that repository. + +- Each enterprise-cookbook PR gets a draft `cookbook-preview/pr-` PR here for its Mintlify preview. These + are never merged and close with the source PR. +- Merged changes arrive in a single `cookbook-sync` PR, opened by `openhands-release-bot` and refreshed nightly. +- `.github/workflows/cookbook-generated.yml` fails PRs from any other branch that touch `cookbook/`, and + `sync_code_blocks.py` skips `cookbook/` because its code-block paths are relative to each example. + ## Docs writing conventions - Most pages are `.mdx` with frontmatter: diff --git a/tests/test_sync_code_blocks.py b/tests/test_sync_code_blocks.py index 057a8c11a..31161672f 100644 --- a/tests/test_sync_code_blocks.py +++ b/tests/test_sync_code_blocks.py @@ -11,7 +11,12 @@ # Add the script directory to path for imports sys.path.insert(0, str(Path(__file__).parent.parent / ".github" / "scripts")) -from sync_code_blocks import escape_embedded_backticks, extract_code_blocks, normalize_content +from sync_code_blocks import ( + escape_embedded_backticks, + extract_code_blocks, + find_mdx_files, + normalize_content, +) class TestEscapeEmbeddedBackticks: @@ -171,3 +176,16 @@ def test_normalizes_line_endings(self): # splitlines() handles all line ending types lines = result.split('\n') assert len(lines) == 3 + + +def test_find_mdx_files_skips_generated_cookbook(tmp_path): + """cookbook/ is generated from enterprise-cookbook and must not be rewritten.""" + (tmp_path / "sdk" / "cookbook").mkdir(parents=True) + (tmp_path / "sdk" / "page.mdx").write_text("x") + (tmp_path / "sdk" / "cookbook" / "nested.mdx").write_text("x") + (tmp_path / "cookbook").mkdir() + (tmp_path / "cookbook" / "example.mdx").write_text("x") + + found = {p.relative_to(tmp_path).as_posix() for p in find_mdx_files(tmp_path)} + + assert found == {"sdk/page.mdx", "sdk/cookbook/nested.mdx"}