diff --git a/.github/workflows/community-check.yml b/.github/workflows/community-check.yml new file mode 100644 index 0000000..dde7ef0 --- /dev/null +++ b/.github/workflows/community-check.yml @@ -0,0 +1,53 @@ +name: Community Entry Check + +# Lightweight, non-executing gate for community showcase entries. +# It does NOT run contributor code. It validates entry structure, scans for +# committed secrets, and confirms hosted entries use Opik. Secrets-free, so it +# runs on forks. The showcase index (community/README.md) is regenerated +# separately on merge by community-index.yml — contributors run nothing. +# +# Community entries are intentionally exempt from the verified-bucket workflows +# (compliance / pr-test / test-notebooks / scheduled), which are path-scoped to +# examples|integrations|scripts|use-cases|guides and never match community/**. + +on: + pull_request: + paths: + - "community/**" + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v7 + + - uses: astral-sh/setup-uv@v7 + with: + version: "latest" + enable-cache: true + python-version: "3.12" + + - name: Install tooling + run: | + cd community/_ci + uv sync + + - name: Unit-test the check tooling + run: | + cd community/_ci + uv run pytest -q + + - name: Lint the check tooling + run: | + cd community/_ci + uv run ruff check . + uv run ruff format --check . + + - name: Validate all community entries + run: | + cd community/_ci + uv run python check_entry.py diff --git a/.github/workflows/community-index.yml b/.github/workflows/community-index.yml new file mode 100644 index 0000000..bb46f7c --- /dev/null +++ b/.github/workflows/community-index.yml @@ -0,0 +1,56 @@ +name: Community Index + +# Regenerates the community showcase index (community/README.md) from every +# entry's meta.yaml after changes land on main. This keeps the index current +# with zero manual action — contributors never run the generator, and no one +# hand-edits community/README.md. +# +# It runs build_index.py (which only parses YAML — it never executes contributor +# code) and commits the result back to main only if it changed. The path filter +# excludes community/README.md so this workflow's own index commit does not +# re-trigger it. + +on: + push: + branches: [main] + paths: + - "community/**" + - "!community/README.md" + +permissions: + contents: write + +concurrency: + group: community-index + cancel-in-progress: false + +jobs: + regenerate: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v7 + + - uses: astral-sh/setup-uv@v7 + with: + version: "latest" + enable-cache: true + python-version: "3.12" + + - name: Regenerate showcase index + run: | + cd community/_ci + uv sync + uv run python build_index.py + + - name: Commit index if it changed + run: | + if [[ -n "$(git status --porcelain community/README.md)" ]]; then + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add community/README.md + git commit -m "chore: regenerate community showcase index" + git push + else + echo "Showcase index already up to date." + fi diff --git a/.gitignore b/.gitignore index d260914..69140a8 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,6 @@ poetry.lock # Local planning/process artifacts (superpowers brainstorm specs & implementation plans) — kept local, never committed docs/superpowers/ + +# Community proof screenshots are intentionally committed (overrides the global *.png rule above) +!community/**/*.png diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b04c193..70d1f83 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,12 @@ Thank you for contributing. This repo is a reference library for Opik users — the goal is examples that are easy to find, easy to run, and easy to adapt. +> **Contributing community showcase work instead of a verified example?** The +> `community/` folder has its own, lighter-weight contract — see +> [community/CONTRIBUTING.md](community/CONTRIBUTING.md). The rules below apply +> to the verified `integrations/`, `guides/`, `use-cases/`, and `scripts/` +> buckets. + ## Recommended workflow This is the loop we follow for non-trivial contributions. The slash-commands in brackets come from Claude Code plugins (see below) and are optional but recommended. **Start every contribution with `/brainstorming`** to agree on scope, and **finish by reviewing your own PR with `/review`** before asking a human. diff --git a/README.md b/README.md index ce144e3..15dd3e7 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,16 @@ Standalone scripts for automating and managing Opik resources. | [scripts/usage_stats](scripts/usage_stats/) | Fetch trace and span counts per project and visualise trends | | [scripts/leaderboard_dashboard](scripts/leaderboard_dashboard/) | Create an Experiment Leaderboard dashboard via the REST API | +## Community + +Work built with Opik by the open-source community. Unlike the buckets above, +these entries are **community-contributed and not maintainer-verified** — they +showcase what people are building. Standout real-world projects get hosted +in-repo and spotlighted in our community forums. + +Browse them in [community/](community/), or add your own via +[community/CONTRIBUTING.md](community/CONTRIBUTING.md). + ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) for the contribution guide and example template. diff --git a/community/CONTRIBUTING.md b/community/CONTRIBUTING.md new file mode 100644 index 0000000..e593fcb --- /dev/null +++ b/community/CONTRIBUTING.md @@ -0,0 +1,48 @@ +# Contributing to the Community folder + +This folder showcases work the open-source community has built with Opik. It is +deliberately **lighter-weight than the main repo contract** — the strict +`run.sh` / dry-run / litellm / CI rules in the root +[CONTRIBUTING.md](../CONTRIBUTING.md) do **not** apply here. Community entries +are not executed by CI; a maintainer reviews (and, for hosted entries, runs) +them by hand. + +There is one thing we always require: **proof you actually logged with Opik** — +either Comet cloud or the self-hosted open-source platform. + +## Two kinds of entry + +- **Listed** (default): a folder describing your work with links out to your own + repo/blog/notebook. No code needs to live here. +- **Hosted**: standout, real-world projects we promote into this repo with their + code included. You submit as *listed*; a maintainer sets `hosted: true` and + moves your code in when promoting. We also spotlight promoted work in our + community forums. + +## Add your entry + +1. Copy `templates/entry-template/` to `community/_/` + (lowercase, underscores, e.g. `jane_support_agent`). +2. Fill in `meta.yaml` (all fields) and `README.md` (all four sections). +3. Replace `opik-proof.png` with a real screenshot of your Opik traces or + dashboard. +4. If you want it considered for hosting, include your code in the folder — it + must genuinely use Opik (`import opik`, `@opik.track`, ...). +5. Open a PR. A maintainer reviews it. + +That's it — you don't need to run anything. The showcase index +(`community/README.md`) is regenerated automatically when your entry merges, so +your project appears there without any manual step on your part. + +## What the automated check enforces + +`community/_ci/check_entry.py` runs on your PR (a hard gate). It does **not** +run your code. It checks: + +- `meta.yaml` has all required fields, at least one link, and a valid + `opik_platform`. +- `README.md` has all four sections filled in (no leftover `TODO`). +- `opik-proof.png` exists and is referenced from your README. +- No `.env` file or hardcoded API keys are committed. +- Hosted entries contain code that uses Opik. +- The folder name is `lowercase_with_underscores`. diff --git a/community/README.md b/community/README.md new file mode 100644 index 0000000..8aa9b74 --- /dev/null +++ b/community/README.md @@ -0,0 +1,12 @@ +# Community + + + +Work built with Opik by the open-source community. These entries are +**community-contributed and not maintainer-verified** — they are showcased here +to help people discover what others are building. Standout, real-world projects +are hosted in-repo (see the Hosted column). + +To add your own, see [CONTRIBUTING.md](CONTRIBUTING.md). + +_No community contributions yet — be the first!_ diff --git a/community/_ci/build_index.py b/community/_ci/build_index.py new file mode 100644 index 0000000..6ebbc4b --- /dev/null +++ b/community/_ci/build_index.py @@ -0,0 +1,121 @@ +from __future__ import annotations + +import sys +from pathlib import Path + +from check_entry import COMMUNITY_DIR, discover_entries +from entry_rules import load_meta + +_HEADER = """\ +# Community + + + +Work built with Opik by the open-source community. These entries are +**community-contributed and not maintainer-verified** — they are showcased here +to help people discover what others are building. Standout, real-world projects +are hosted in-repo (see the Hosted column). + +To add your own, see [CONTRIBUTING.md](CONTRIBUTING.md). +""" + +_TABLE_HEADER = ( + "| Project | Author | Description | Platform | Links | Hosted | Tags |\n" + "|---|---|---|---|---|---|---|" +) + + +def load_entries(community_dir: Path) -> list[dict]: + entries: list[dict] = [] + for entry in discover_entries(community_dir): + data, errors = load_meta(entry) + if errors: + continue + entries.append( + { + "slug": entry.name, + "title": str(data.get("title", entry.name)), + "description": str(data.get("description", "")), + "author": str(data.get("author", "")), + "links": data.get("links") or {}, + "opik_platform": str(data.get("opik_platform", "")), + "tags": data.get("tags") or [], + "hosted": bool(data.get("hosted", False)), + } + ) + entries.sort(key=lambda e: e["title"].casefold()) + return entries + + +def _escape_pipes(value: str) -> str: + return value.replace("|", "\\|") + + +def _links_cell(links: dict) -> str: + parts = [ + f"[{_escape_pipes(str(label))}]({_escape_pipes(str(url))})" + for label, url in links.items() + if str(url).startswith("http") + ] + return "
".join(parts) if parts else "" + + +def _row(entry: dict) -> str: + project = f"[{_escape_pipes(entry['title'])}]({entry['slug']}/)" + author = ( + f"[@{_escape_pipes(entry['author'])}](https://github.com/{_escape_pipes(entry['author'])})" + if entry["author"] + else "" + ) + tags = _escape_pipes(", ".join(str(t) for t in entry["tags"])) + hosted = "hosted" if entry["hosted"] else "" + cells = [ + project, + author, + _escape_pipes(entry["description"]), + _escape_pipes(entry["opik_platform"]), + _links_cell(entry["links"]), + hosted, + tags, + ] + return "| " + " | ".join(cells) + " |" + + +def render_index(entries: list[dict]) -> str: + lines = [_HEADER] + if not entries: + lines.append("_No community contributions yet — be the first!_\n") + else: + lines.append(_TABLE_HEADER) + lines.extend(_row(e) for e in entries) + lines.append("") + return "\n".join(lines).rstrip("\n") + "\n" + + +def write_index(community_dir: Path) -> None: + (community_dir / "README.md").write_text( + render_index(load_entries(community_dir)), encoding="utf-8" + ) + + +def check_index(community_dir: Path) -> bool: + readme = community_dir / "README.md" + current = readme.read_text(encoding="utf-8") if readme.is_file() else "" + return current == render_index(load_entries(community_dir)) + + +def main(argv: list[str] | None = None) -> int: + args = sys.argv[1:] if argv is None else argv + if "--check" in args: + if check_index(COMMUNITY_DIR): + print("community/README.md is up to date.") + return 0 + print("community/README.md is stale. Run: python build_index.py") + return 1 + write_index(COMMUNITY_DIR) + print("Wrote community/README.md") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/community/_ci/check_entry.py b/community/_ci/check_entry.py new file mode 100644 index 0000000..042960c --- /dev/null +++ b/community/_ci/check_entry.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +import sys +from pathlib import Path + +from entry_rules import ( + validate_code_uses_opik, + validate_folder_name, + validate_meta, + validate_no_secrets, + validate_proof, + validate_readme, +) + +COMMUNITY_DIR = Path(__file__).resolve().parent.parent +RESERVED_DIRS = {"_ci", "templates"} + + +def discover_entries(community_dir: Path) -> list[Path]: + return [ + child + for child in sorted(community_dir.iterdir()) + if child.is_dir() and child.name not in RESERVED_DIRS + ] + + +def check_entry(entry: Path) -> list[str]: + errors: list[str] = [] + errors += validate_folder_name(entry) + errors += validate_meta(entry) + errors += validate_readme(entry) + errors += validate_proof(entry) + errors += validate_no_secrets(entry) + errors += validate_code_uses_opik(entry) + return errors + + +def main(argv: list[str] | None = None) -> int: + args = sys.argv[1:] if argv is None else argv + if args: + entries = [Path(a) for a in args] + else: + entries = discover_entries(COMMUNITY_DIR) + + if not entries: + print("No community entries to check.") + return 0 + + all_errors: list[str] = [] + for entry in entries: + entry_errors = check_entry(entry) + if entry_errors: + all_errors.extend(entry_errors) + else: + print(f"OK {entry.name}") + + if all_errors: + print("\nCommunity entry check failed:") + for error in all_errors: + print(f" - {error}") + return 1 + + print("\nAll community entries passed.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/community/_ci/entry_rules.py b/community/_ci/entry_rules.py new file mode 100644 index 0000000..4930390 --- /dev/null +++ b/community/_ci/entry_rules.py @@ -0,0 +1,185 @@ +from __future__ import annotations + +import re +from pathlib import Path + +import yaml + +REQUIRED_SECTIONS = [ + "What I built", + "Problem it solves", + "What I learned", + "How I used Opik", +] +PLACEHOLDER_TOKENS = ["TODO", "FILL ME IN", "