diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ed7362d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,152 @@ +# Contributing to the Enterprise Cookbook + +Each directory in this repository is a standalone, runnable example for the +OpenHands API. Examples are read in two places: here on GitHub, and as pages in +the [Cookbook](https://docs.openhands.dev/cookbook) tab of docs.openhands.dev, +which is generated from each example's `README.md`. + +This guide covers the process: how a change gets from a pull request to the docs +site. For how to write an example and its README, see +[STYLEGUIDE.md](./STYLEGUIDE.md). + +## Adding or Upleveling an Example + +**A new example** gets its own top-level directory with a kebab-case name, a +`README.md`, and the code it runs. Follow the layout and README structure in the +style guide, and add the example to its category in the root +[README.md](./README.md#examples). + +**Upleveling an existing example** means bringing it up to the style guide so it +can be published. Work through the +[upleveling checklist](./STYLEGUIDE.md#upleveling-checklist), one example per +pull request, so each preview shows a single page. [`command-blacklist`](./command-blacklist/) +is the reference. + +## Publishing to docs.openhands.dev + +An example is published once its directory has an `example.yaml`: + +```yaml +title: Command Blacklist +description: Block known-dangerous shell commands with a PreToolUse hook bundled in a plugin. +category: Guardrails +icon: shield-halved +``` + +Examples without one stay GitHub-only, which is the right choice for +experimental or work-in-progress examples. The fields are described in the +[style guide](./STYLEGUIDE.md#exampleyaml). + +`category` must be one of the categories in [`cookbook.yaml`](./cookbook.yaml), +which also sets the tab's title, position, and overview page. To add a category, +add it there and to the root README in the same pull request. + +## What Happens on a Pull Request + +| Check | What it does | Required to merge | +|---|---|---| +| `pre-commit` | Formatting and linting: ruff, pycodestyle, pyright, yamlfmt, whitespace | Yes | +| `test` | `uv run pytest` | Yes | +| `render` (Docs render) | Converts every published example and fails on anything the converter rejects | No | +| `docs-preview` (Docs preview) | Builds a preview of the docs site with your change | No | +| `pr-review` (PR Review by OpenHands) | An AI code review | No | + +Treat `render` and `docs-preview` as required for any change to a published +example, even though GitHub doesn't enforce them. + +### The Docs Preview + +For each pull request, the **Docs preview** workflow: + +1. Renders the published examples from your branch. +2. Opens or updates a draft pull request in + [OpenHands/docs](https://github.com/OpenHands/docs) on a + `cookbook-preview/pr-` branch, titled "Cookbook preview: … (do not + merge)". Mintlify builds a preview site for it. +3. Waits for the Mintlify deployment and the docs repository's checks, such as + internal links and link rot. +4. Comments on your pull request with the preview URL and a link to each page + that changed, then sets the `docs-preview` status. + +Each push updates the same docs pull request and the same comment. If your +change doesn't affect any published page, no docs pull request is opened. +Changed pages are listed relative to what is live on docs.openhands.dev. + +When `docs-preview` fails, the status links to the workflow run: + +- **Render failed**: the log names the file and line, for example + `conversation-tags/README.md:129: link "./NOTES.md" … does not exist`. Fix the + README and push. +- **Docs checks failed**: open the draft docs pull request linked in the comment + to see which check failed, usually a broken link. + +When your pull request is merged or closed, the draft docs pull request is closed +and its branch deleted. It is never merged. + +### Pull Requests From Forks + +The preview and the AI review need repository secrets, so they only run for +branches in this repository. A pull request from a fork still gets `render`, so +README errors are caught. If you have write access, push your branch here +instead of to a fork to get a preview. + +### AI Review + +The OpenHands review runs when a pull request is opened (unless it's a draft) or +marked ready for review. To run it again, add the `review-this` label or request +a review from `openhands-agent`. + +## After Merge + +On every push to `main`, nightly, and on demand, the **Docs publish** workflow +renders all published examples and keeps a single pull request in OpenHands/docs +up to date on the `cookbook-sync` branch. A maintainer of the docs repository +merges it to put the changes live. Until then, several merged changes accumulate +in that one pull request. + +The nightly run rebuilds the pull request on top of the latest docs, so it stays +mergeable when other docs changes touch `docs.json`. If nothing changed, the run +does nothing. To run it by hand, use **Run workflow** on the +[Docs publish](https://github.com/OpenHands/enterprise-cookbook/actions/workflows/docs-publish.yml) page. + +The workflows authenticate to OpenHands/docs as `openhands-release-bot`. The job +that holds that credential only copies rendered files; it never installs +dependencies or runs code from the pull request. + +## The Docs Repository + +Everything under `cookbook/` in OpenHands/docs, and the Cookbook tab in its +`docs.json`, is generated from this repository: + +- Don't edit those files in the docs repository. The next sync overwrites them. + Make the change here. +- Don't merge the draft `cookbook-preview/*` pull requests. +- To fix something on a published page, change the example's `README.md` or + `example.yaml` here. + +## Local Checks + +```bash +uv sync --group dev +uv run pre-commit install # run the hooks on every commit +uv run pre-commit run --all-files +uv run pytest +``` + +To check the docs rendering, with Node.js 22 as in CI: + +```bash +cd tools/docs-render +npm ci +npm run check +``` + +To see the pages on a local copy of the docs site, see +[tools/docs-render](./tools/docs-render/README.md#previewing-locally). + +## Reviews and Merging + +Changes to `main` go through a pull request with at least one approving review +and passing `pre-commit` and `test` checks. Pull requests are squash-merged, so +the pull request title becomes the commit message on `main`. Write it as a short +description of the change. diff --git a/README.md b/README.md index 4ddad30..a978987 100644 --- a/README.md +++ b/README.md @@ -102,8 +102,8 @@ under its primary topic above. Examples with an `example.yaml` are also published as pages in the [Cookbook](https://docs.openhands.dev/cookbook) tab of the OpenHands docs. Pull requests get a preview link for those pages before merge. See -[tools/docs-render](./tools/docs-render/) for how to publish an example and -write a README that renders well in both places. +[CONTRIBUTING.md](./CONTRIBUTING.md) for how changes are published and +[STYLEGUIDE.md](./STYLEGUIDE.md) for how to write an example. ## API Versions diff --git a/STYLEGUIDE.md b/STYLEGUIDE.md new file mode 100644 index 0000000..460aee9 --- /dev/null +++ b/STYLEGUIDE.md @@ -0,0 +1,300 @@ +# Cookbook Style Guide + +How to write an example and its README so it works on GitHub and renders well as +a page on docs.openhands.dev. For how changes are reviewed and published, see +[CONTRIBUTING.md](./CONTRIBUTING.md). + +[`command-blacklist`](./command-blacklist/) is the reference example. When this +guide and an existing example disagree, follow this guide. + +## Writing Style + +Follow the docs site's +[Documentation Style Guide](https://github.com/OpenHands/docs/blob/main/openhands/DOC_STYLE_GUIDE.md). +In short: + +- Use **Title Case** for first- and second-level headings (`#`, `##`). +- Keep sentences short. Start with the simplest way to run the example, then add + options and variations. +- Use bullet lists for options and numbered lists for steps that must happen in + order. +- Use tabs when there is more than one way to do the same thing, and notes or + warnings for asides. In a README you write these as the GitHub Markdown + described in [Docs Components](#docs-components). + +The rest of this guide covers what is specific to cookbook examples. + +## Example Layout + +```text +my-example/ +├── README.md # what it does and how to run it +├── example.yaml # publishes it to docs.openhands.dev (optional) +├── my_example.py # the script the README runs +└── my-plugin/ # any plugin, skill, or config the example loads +``` + +- Name the directory in kebab-case after what the example shows, not how it is + built: `conversation-tags`, not `tags-api-script`. +- Keep the example self-contained. Link to another example rather than importing + from it. +- Put tests in `tests/` only when the example is a reusable package; see + [`conversation-metrics`](./conversation-metrics/). + +## README Structure + +```markdown +# Command Blacklist + +One or two paragraphs: what the example does and when you would use it. + +## How It Works + +## Prerequisites + +## Run It + +## + +## APIs Used + +## Related +``` + +| Section | Required | What goes in it | +|---|---|---| +| `#` title | Yes | The example's name in Title Case. On the docs site it is replaced by the `title` in `example.yaml`, so make them match. | +| Introduction | Yes | What the example does and why you'd use it, before any heading. On the docs site it follows the `description` from `example.yaml`, so don't repeat that sentence. | +| How It Works | Yes | The flow, in a few sentences or a diagram. Use a mermaid sequence diagram when several parties are involved. | +| Prerequisites | When needed | Anything beyond an OpenHands API key and `pip install requests`, such as a sandbox spec, a GitHub token, or another example's plugin. | +| Run It | Yes | The shortest command that runs the example, then what it prints, then options. Use tabs for alternative ways to run it. | +| Topic sections | Optional | Explanations specific to the example, such as "The Hook", "Tag Rules", or "Plugin Structure". | +| APIs Used | When it calls the API | A table of the endpoints the example calls and what for. | +| Related | Yes | Links to related examples and docs pages, as cards. | + +Use these exact section names, so readers find the same thing in the same place +in every example. Avoid "Usage" or "Try It" for Run It, and "Related Examples" or +"Related Documentation" for Related. + +## Docs Components + +Write ordinary GitHub Markdown. Headings, lists, tables, links, images, and code +blocks pass through to the docs site unchanged. The constructs below also become +[Mintlify components](https://mintlify.com/docs/components) on the docs site, +while still reading well on GitHub. + +| In the README | On the docs site | Use it for | +|---|---|---| +| `> [!NOTE]` | `` | Context that helps but isn't essential | +| `> [!TIP]` | `` | A shortcut or a better way to do something | +| `> [!IMPORTANT]` | `` | Something the reader must know to succeed | +| `> [!WARNING]`, `> [!CAUTION]` | `` | Something that can fail or cause damage | +| `
Title …
` | `` | Background that most readers can skip, such as "Why inline?" | +| A `mermaid` code block | A diagram | Flows between parties; replace ASCII diagrams with these | +| `` … `` | ``, one per heading inside | Alternative ways to do the same thing | +| `` … `` | ``, one per heading inside | A procedure whose steps each need explanation | +| `` around a link list | `` | The Related section | +| `` … `` | Omitted | Content that only makes sense on GitHub | +| A code block that names a file | The full file | See [Code Blocks](#code-blocks) | + +Use these only where they fit the content. A plain section is better than tabs +with one tab. + +### Alerts + +Put the marker on its own line: + +```markdown +> [!WARNING] +> The agent may refuse an obviously dangerous command before the hook runs. +``` + +A plain `>` blockquote stays a blockquote. Use an alert for asides instead. + +### Tabs and Steps + +Put a heading for each tab or step inside the markers. On GitHub they read as +ordinary sections: + +```markdown + + +### Load via API + +… + +### Launch via badge + +… + + +``` + +Leave a blank line before and after each `docs:` comment. + +### Cards + +Each list item is a link, a ` - `, and a short description: + +```markdown +## Related + + + +- [`load-plugin`](../load-plugin/) - Programmatic plugin loading +- [OpenHands Hooks Guide](https://docs.openhands.dev/sdk/guides/hooks) - Full hook documentation + + +``` + +Cards for published examples use that example's icon; other cards get a book or +external-link icon. + +## Links and Images + +- **Other examples**: link relatively, as `../load-plugin/`. The link points to + the example's docs page once it is published, and to GitHub until then. +- **Files in this repository**: link relatively, as `./safety-guardian/`. On the + docs site these go to GitHub at the commit that was rendered. +- **Docs pages**: use the full URL without `.md`, as + `https://docs.openhands.dev/sdk/guides/hooks`. It becomes a link within the docs + site. +- **Images**: use a relative path to a file in the example's directory, with alt + text that describes the image. The docs site loads it from GitHub. + +A relative link to something that doesn't exist fails the build. + +## Code Blocks + +- Always give a language: `bash` for commands, `python`, `json`, `yaml`, and + `text` for program output. +- Write commands to run from the example's directory, and make them + copy-pasteable: no `$` prompts, and placeholders in capitals, as `$OH_API_KEY`. +- Follow the command with what it prints, in a `text` block, trimmed to the lines + that matter. + +### Blocks That Show a Real File + +When a code block shows a file from the example, name the file after the +language. The path is relative to the example's directory: + +````markdown +```json safety-guardian/hooks/hooks.json +{ … } +``` +```` + +The build fails unless the block matches the file exactly, so the README can't +drift from the code. For a long file, add `excerpt` and show only the part that +matters: + +````markdown +```json safety-guardian/hooks/hooks.json excerpt +``` +```` + +The docs page always shows the full file, collapsed if it is long. + +### Not Supported + +Raw HTML other than `
` and `` fails the build, as do unknown or +unclosed `docs:` comments. Errors name the file and line. + +## Example Code + +Scripts should run with nothing but an API key: + +- **Python 3.10 or later**, using the standard library plus `requests`, or + `websockets` for streaming. Say what to install in the README. +- **A complete demo by default.** With no arguments, the script creates what it + needs, shows the result, and deletes what it created. A `--keep` flag leaves it + running for inspection. +- **The standard options.** Read each one from a flag, falling back to an + environment variable: + + | Flag | Environment variable | Default | + |---|---|---| + | `--api-key` | `OH_API_KEY` | Required | + | `--base-url` | `OH_API_BASE` | `https://app.all-hands.dev` | + | `--poll-timeout` | `POLL_TIMEOUT` | `180` seconds | + | `--sandbox-spec-id` | `SANDBOX_SPEC_ID` | The server's default | + | `--sandbox-id` | `SANDBOX_ID` | Create a new sandbox | + | `--message` | `INITIAL_MESSAGE` | A short message for the example | + +- **Never print secrets**, including API keys, tokens, and secret values the + example sets. +- **Pass pre-commit**: ruff, pycodestyle, and pyright, as configured in + [`.pre-commit-config.yaml`](./.pre-commit-config.yaml). + +### Launch Badges + +For an example that loads a plugin, a badge lets readers launch it in OpenHands +Cloud with one click. Generate the badge with +[`launch-plugin-badge/build_launch_url.py`](./launch-plugin-badge/) rather than +editing the URL by hand. The plugin details are base64-encoded in the URL, so +mistakes are hard to spot: + +```bash +python launch-plugin-badge/build_launch_url.py \ + --repo-path command-blacklist/safety-guardian \ + --message "…" \ + --label "Try Safety Guardian" +``` + +The source must be `github:OpenHands/enterprise-cookbook` (the default) and the +ref `main`. + +## example.yaml + +```yaml +title: Command Blacklist +description: Block known-dangerous shell commands with a PreToolUse hook bundled in a plugin. +category: Guardrails +icon: shield-halved +``` + +| Field | Required | Shown as | Guidance | +|---|---|---|---| +| `title` | Yes | Page title, sidebar entry, and overview card | Title Case, two to four words, matching the README's `#` heading | +| `description` | Yes | Subtitle under the page title, and overview card text | One sentence saying what the example does. Start with a verb, not "This example". | +| `category` | Yes | Sidebar group and overview section | One of the categories in [`cookbook.yaml`](./cookbook.yaml) | +| `icon` | No | Sidebar and card icon | A [Font Awesome](https://fontawesome.com/search?ic=free) icon name that no other example in the category uses | + +Other keys fail the build. + +## Upleveling Checklist + +Use this when bringing an existing example up to this guide. One example per pull +request. + +**Required to publish** + +- [ ] `example.yaml` added, following [example.yaml](#exampleyaml). +- [ ] `npm run check` in `tools/docs-render` passes. +- [ ] `#` and `##` headings are in Title Case, and the `#` heading matches `title`. +- [ ] Sections follow [README Structure](#readme-structure), with the standard + names. +- [ ] Links to docs pages use full URLs without `.md`, and links to other + examples are relative. +- [ ] Launch badges are regenerated with `build_launch_url.py`, with the source + `github:OpenHands/enterprise-cookbook` and ref `main`. +- [ ] No leftover references to old branches or the former repository name + (`jpshackelford/oh-examples`). + +**Polish** + +- [ ] Asides are alerts, not plain blockquotes or "Note:" paragraphs. +- [ ] ASCII diagrams are replaced with mermaid diagrams. +- [ ] Alternative ways to run the example are tabs. +- [ ] Optional background is in a `
` block. +- [ ] Code blocks that show a real file name that file. +- [ ] Related links are cards. + +**Code** + +- [ ] The script runs with only an API key, and cleans up unless `--keep` is + given. +- [ ] Flags and environment variables follow [Example Code](#example-code); for + example, `OH_API_BASE` rather than `OH_API_URL`. +- [ ] The docs preview looks right. Check each changed page from the preview + comment. diff --git a/cookbook.yaml b/cookbook.yaml index 2014be4..db463db 100644 --- a/cookbook.yaml +++ b/cookbook.yaml @@ -1,7 +1,7 @@ --- # Settings for the Cookbook tab on docs.openhands.dev. # An example is published there once its directory has an example.yaml. -# See tools/docs-render/README.md. +# See CONTRIBUTING.md and STYLEGUIDE.md. tab: Cookbook after_tab: Enterprise overview_group: Overview diff --git a/tools/docs-render/README.md b/tools/docs-render/README.md index 473e5ae..05f8f53 100644 --- a/tools/docs-render/README.md +++ b/tools/docs-render/README.md @@ -1,95 +1,71 @@ -# Publishing examples to docs.openhands.dev +# docs-render -Examples in this repository can also appear as pages in the **Cookbook** tab of -[docs.openhands.dev](https://docs.openhands.dev). Each page is generated from the -example's `README.md` by the converter in this directory. The conversion is -deterministic and has no manual step, so the docs page always matches the README. +The converter that turns each published example's `README.md` into a page in the +Cookbook tab of docs.openhands.dev. These are notes for maintaining it. -## Publish an example +- To write an example that renders well, see [STYLEGUIDE.md](../../STYLEGUIDE.md). +- For how pull requests get docs previews and how changes are published, see + [CONTRIBUTING.md](../../CONTRIBUTING.md). -Add an `example.yaml` next to the example's `README.md`: +## What It Generates -```yaml -title: Command blacklist -description: Block known-dangerous shell commands with a PreToolUse hook. -category: Guardrails # one of the categories in /cookbook.yaml -icon: shield-halved # optional; any Font Awesome icon name -``` - -Examples without an `example.yaml` are not published. The page title and -description come from this file, so the README's `# ` heading is dropped. - -## What happens to a pull request +Given this repository and a checkout of OpenHands/docs, `render` writes: -1. **Docs render** checks that every published example converts cleanly. -2. **Docs preview** opens a draft PR in OpenHands/docs with the rendered pages - and comments on your PR with a Mintlify preview link to each changed page. The - `docs-preview` status fails if the page doesn't build or the docs checks fail. -3. After merge, **Docs publish** updates a single `cookbook-sync` PR in - OpenHands/docs. Nothing under `cookbook/` in the docs repository is edited by - hand. +- `cookbook/.mdx` for every example with an `example.yaml`. +- `cookbook/index.mdx`, the overview page, built from + [`cookbook.yaml`](../../cookbook.yaml): one section per category, with a card + for each example. +- The Cookbook tab in `docs.json`, placed after `after_tab`, with the overview + group and one group per category. The rest of `docs.json` is rewritten byte for + byte, so the diff touches only the Cookbook tab. +- A redirect to `/cookbook` for each page that is no longer published, so old + links keep working. A redirect is dropped when its page is published again. -## Writing the README +It deletes pages under `cookbook/` for examples that are no longer published, and +refuses to run if `cookbook/` contains a file it didn't generate. The output +depends only on its inputs, so a second run changes nothing. -Write ordinary GitHub Markdown; it should read well on GitHub. Headings, lists, -tables, links, images, and code blocks pass through unchanged. These constructs -also become docs components: +## Commands -| In the README | On the docs site | -|---|---| -| `> [!NOTE]`, `> [!TIP]`, `> [!IMPORTANT]`, `> [!WARNING]`, `> [!CAUTION]` | ``, ``, ``, ``, `` | -| `
Title …
` | `` | -| A `mermaid` code block | A rendered diagram | -| A code block whose info string names a file, such as `json hooks/hooks.json` | The real file, titled with its path, collapsed if long | -| `` … `` | ``, one tab per heading inside | -| `` … `` | ``, one step per heading inside | -| `` around a list of `[link](url) - description` items | `` | -| `` … `` | Omitted from the docs page | - -Write each GitHub alert with the marker on its own line: - -```markdown -> [!WARNING] -> The agent may refuse an obviously dangerous command before the hook runs. +```bash +npm ci +npm test # converter tests +npm run check # render every example, report errors +node cli.mjs render --docs ../../../docs # write pages into a docs checkout ``` -The `docs:` comments are invisible on GitHub, where tabs and steps read as -ordinary sections. Leave a blank line before and after each comment. - -### Links - -- Links to another published example (`../load-plugin/`) become links to its docs - page. Links to an unpublished example go to GitHub. -- Links to files or directories in this repository go to GitHub. -- Links to `https://docs.openhands.dev/...` become links within the docs site. -- Relative links must point to something that exists. - -### Code blocks that name a file +`render` options: -A code block such as ```` ```json safety-guardian/hooks/hooks.json ```` (path -relative to the example directory) must match that file exactly, so the README -can't drift from the code. To show only part of a long file in the README, add -`excerpt`: ```` ```json safety-guardian/hooks/hooks.json excerpt ````. The docs -page always shows the full file. +| Option | Default | Purpose | +|---|---|---| +| `--docs DIR` | Required | The OpenHands/docs checkout to write into | +| `--examples DIR` | This repository | The directory containing the examples and `cookbook.yaml` | +| `--ref REF` | `main` | The commit or branch that links to files on GitHub point at | +| `--summary FILE` | None | Write the pages added, changed, and removed as JSON, used for the preview comment | -### Not supported +## Previewing Locally -Raw HTML other than `
` and `` is rejected, as are unknown -`docs:` comments and unclosed ones. Errors name the file and line. - -## Running locally - -```bash -cd tools/docs-render -npm ci -npm test # converter tests -npm run check # render every published example and report errors -``` - -To see the pages in the docs site, render into a checkout of OpenHands/docs and -run Mintlify's dev server there: +Render into a checkout of OpenHands/docs next to this repository and run +Mintlify's dev server there: ```bash node tools/docs-render/cli.mjs render --docs ../docs cd ../docs && npx mint dev ``` + +## How the Workflows Use It + +| File | Role | +|---|---| +| [`.github/actions/render-docs`](../../.github/actions/render-docs/action.yml) | Runs `render` against a fresh docs checkout and uploads the result. Has no secrets. | +| [`.github/workflows/docs-render.yml`](../../.github/workflows/docs-render.yml) | Runs `npm test` and `npm run check` on every pull request and push. | +| [`.github/workflows/docs-preview.yml`](../../.github/workflows/docs-preview.yml) | Pushes the rendered files to a draft docs pull request per cookbook pull request and reports back. | +| [`.github/workflows/docs-publish.yml`](../../.github/workflows/docs-publish.yml) | Keeps the `cookbook-sync` docs pull request up to date from `main`. | +| [`.github/scripts/docs_sync.py`](../../.github/scripts/docs_sync.py) | Pushes to the docs repository, waits for its checks, and posts the comment and `docs-preview` status. | + +## Changing the Converter + +The conversion rules authors rely on are documented in +[STYLEGUIDE.md](../../STYLEGUIDE.md#docs-components). When you add or change a +rule, update that section in the same pull request and add a test in +`test/render.test.mjs`.