diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index e4f8884..7100d8d 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -7,6 +7,8 @@ Closes # + - [ ] Linked to its tracking issue (commented to claim it before starting) - [ ] Example is in the right bucket (`integrations` / `guides` / `use-cases` / `scripts`) diff --git a/.github/workflows/community-check.yml b/.github/workflows/community-check.yml index dde7ef0..390a671 100644 --- a/.github/workflows/community-check.yml +++ b/.github/workflows/community-check.yml @@ -1,10 +1,11 @@ -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. +name: Community Projects Check + +# Lightweight, non-executing gate for the community index. Community projects +# are links-only — code lives in the authors' repos — so this only validates +# community/projects.yaml (required fields, valid repo URLs/handles, no +# duplicates). Secrets-free, so it runs on forks. The 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 @@ -47,7 +48,7 @@ jobs: uv run ruff check . uv run ruff format --check . - - name: Validate all community entries + - name: Validate community projects run: | cd community/_ci - uv run python check_entry.py + uv run python check_projects.py diff --git a/.github/workflows/community-index.yml b/.github/workflows/community-index.yml index bb46f7c..ed8d248 100644 --- a/.github/workflows/community-index.yml +++ b/.github/workflows/community-index.yml @@ -1,9 +1,9 @@ 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. +# Regenerates the community index (community/README.md) from +# community/projects.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 @@ -37,7 +37,7 @@ jobs: enable-cache: true python-version: "3.12" - - name: Regenerate showcase index + - name: Regenerate community index run: | cd community/_ci uv sync @@ -49,8 +49,8 @@ jobs: 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 commit -m "chore: regenerate community index" git push else - echo "Showcase index already up to date." + echo "Community index already up to date." fi diff --git a/AGENTS.md b/AGENTS.md index ed1e1e2..fbb6a64 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,9 +10,15 @@ opik-examples/ ├── guides/ # How-to patterns for Opik workflows ├── use-cases/ # End-to-end applications and domain workflows ├── scripts/ # Utility automations and API helpers -└── templates/ # Starter templates (use-case-template, script-template) +├── templates/ # Starter templates (use-case-template, script-template) +└── community/ # Curated links-only index of community-built projects ``` +The `community/` folder is not a code bucket: it is a curated index +(`community/projects.yaml` → generated `community/README.md`) of projects that +live in their authors' repos. It is exempt from the `run.sh` / dry-run / litellm +contract — see [community/CONTRIBUTING.md](community/CONTRIBUTING.md). + **Which bucket does new code belong in?** | If you are… | Put it in | diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..8ae1422 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,77 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances + of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for +moderation decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the maintainers by opening an issue in this repository or through +[Comet support](https://www.comet.com/site/about-us/contact-us/). All +complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[FAQ]: https://www.contributor-covenant.org/faq diff --git a/README.md b/README.md index d60d377..f386f8c 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,8 @@ opik-examples/ ├── guides/ # How-to examples for Opik workflows and patterns ├── use-cases/ # End-to-end applications and domain workflows ├── scripts/ # Utility automations and API helpers -└── templates/ # Starter templates (use-case-template, script-template) +├── templates/ # Starter templates (use-case-template, script-template) +└── community/ # Curated index of community-built projects (links only) ``` ## Integrations @@ -59,12 +60,13 @@ Standalone scripts for automating and managing Opik resources. ## 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. +A curated index of projects the open-source community has built with Opik. +It is links-only — each project lives in its author's repository. Unlike the +buckets above, these entries are **community-contributed and not +maintainer-verified**. -Browse them in [community/](community/), or add your own via +Browse the table in [community/](community/), or add your own project with a +single block in `projects.yaml` — see [community/CONTRIBUTING.md](community/CONTRIBUTING.md). ## Contributing diff --git a/community/CONTRIBUTING.md b/community/CONTRIBUTING.md index 4177027..14be499 100644 --- a/community/CONTRIBUTING.md +++ b/community/CONTRIBUTING.md @@ -1,52 +1,54 @@ -# Contributing to the Community folder +# Contributing to the Community index -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 +This folder is a **curated, links-only index** of projects the open-source +community has built with [Opik](https://www.comet.com/site/products/opik/). +Your code stays in your own repository — the index just points to it. That +means no sync issues when you update your project, and contributing takes a +couple of minutes. + +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. Provide it one of -two ways: commit an `opik-proof.png` screenshot referenced from your `README.md` -(the default for authors), or set an http(s) `proof_url` in `meta.yaml` pointing -at your screenshot (used mainly when a maintainer showcases an external project). - -## 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. +are not executed by CI; a maintainer reviews each submission by hand. + +## Add your project + +1. Add one block to [`projects.yaml`](projects.yaml): + + ```yaml + - title: Your project title + description: One or two sentences on what you built and how it uses Opik. + author: your-github-handle + repo: https://github.com/your-handle/your-project + ``` + +2. Open a PR. That's it — you don't need to run anything. + +All four fields are required. Keep `description` under 250 characters, +`author` a bare GitHub handle (no `@`, no URL), and `repo` an http(s) link. +Don't edit `README.md` — it is generated from `projects.yaml` automatically +after your PR merges. + +## Review bar + +A maintainer checks that the linked project genuinely uses Opik (e.g. +`import opik`, `@opik.track`, or Opik dashboards in the docs) and that the +description is accurate. Entries are **community-contributed and not +maintainer-verified** — we curate the list, we don't maintain the projects. ## 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`). -- Proof of Opik usage exists: either `opik-proof.png` (referenced from your - README) or an http(s) `proof_url` in `meta.yaml`. -- No `.env` file or hardcoded API keys are committed. -- Hosted entries contain code that uses Opik. -- The folder name is `lowercase_with_underscores`. +`community/_ci/check_projects.py` runs on your PR (a hard gate). It only +validates `projects.yaml`: + +- Every entry has `title`, `description`, `author`, and `repo` (and no other + fields). +- `repo` is an http(s) URL and `author` is a valid GitHub handle. +- `description` is at most 250 characters. +- No duplicate titles or repos. + +## Promotion + +Standout projects that meet the standards of the verified buckets +(`integrations/`, `guides/`, `use-cases/`, `scripts/`) may be invited into the +main repo — the root [CONTRIBUTING.md](../CONTRIBUTING.md) contract applies +there. diff --git a/community/README.md b/community/README.md index 1a8b54a..3b38299 100644 --- a/community/README.md +++ b/community/README.md @@ -2,14 +2,15 @@ -Work built with Opik by the open-source community. These entries are +A curated index of projects the open-source community has built with Opik. It is +links-only: each project lives in its author's repository, and 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 help people discover what others are building. -To add your own, see [CONTRIBUTING.md](CONTRIBUTING.md). +To add yours, see [CONTRIBUTING.md](CONTRIBUTING.md) — it's one small block in +[projects.yaml](projects.yaml). -| Project | Author | Description | Platform | Links | Hosted | Tags | -|---|---|---|---|---|---|---| -| [Building a Coding Agent from Scratch (course)](paul_iusztin_coding_agent_course/) | [@decodingai-magazine](https://github.com/decodingai-magazine) | An open-source course that builds a terminal coding agent from scratch, using Opik for tracing and evals. | cloud | [repo](https://github.com/decodingai-magazine/building-a-coding-agent-from-scratch-course) | | agent, course, coding-agent, evaluation, observability | -| [Observable Job Agent](jamwithai_observable_job_agent/) | [@jamwithai](https://github.com/jamwithai) | An observability-first CV-to-job-matching agent, instrumented with Opik from run one. | cloud | [repo](https://github.com/jamwithai/observable-job-agent) | | agent, langgraph, observability, rag | +| Project | Description | Author | +|---|---|---| +| [Building a Coding Agent from Scratch (course)](https://github.com/decodingai-magazine/building-a-coding-agent-from-scratch-course) | An open-source course that builds a terminal coding agent from scratch, using Opik for tracing and evals. | [@decodingai-magazine](https://github.com/decodingai-magazine) | +| [Observable Job Agent](https://github.com/jamwithai/observable-job-agent) | An observability-first CV-to-job-matching agent, instrumented with Opik from run one. | [@jamwithai](https://github.com/jamwithai) | diff --git a/community/_ci/build_index.py b/community/_ci/build_index.py index 6ebbc4b..b9a6163 100644 --- a/community/_ci/build_index.py +++ b/community/_ci/build_index.py @@ -3,46 +3,44 @@ import sys from pathlib import Path -from check_entry import COMMUNITY_DIR, discover_entries -from entry_rules import load_meta +from project_rules import load_projects, validate_projects + +COMMUNITY_DIR = Path(__file__).resolve().parent.parent +PROJECTS_FILE_NAME = "projects.yaml" _HEADER = """\ # Community -Work built with Opik by the open-source community. These entries are +A curated index of projects the open-source community has built with Opik. It is +links-only: each project lives in its author's repository, and 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 help people discover what others are building. -To add your own, see [CONTRIBUTING.md](CONTRIBUTING.md). +To add yours, see [CONTRIBUTING.md](CONTRIBUTING.md) — it's one small block in +[projects.yaml](projects.yaml). """ -_TABLE_HEADER = ( - "| Project | Author | Description | Platform | Links | Hosted | Tags |\n" - "|---|---|---|---|---|---|---|" -) +_TABLE_HEADER = "| Project | Description | Author |\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)), - } - ) + projects, errors = load_projects(community_dir / PROJECTS_FILE_NAME) + if not errors: + errors = validate_projects(projects) + if errors: + raise ValueError("invalid projects.yaml: " + "; ".join(errors)) + + entries = [ + { + "title": str(item["title"]).strip(), + "description": str(item["description"]).strip(), + "author": str(item["author"]).strip(), + "repo": str(item["repo"]).strip(), + } + for item in projects + ] entries.sort(key=lambda e: e["title"].casefold()) return entries @@ -51,33 +49,12 @@ 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']}/)" + project = f"[{_escape_pipes(entry['title'])}]({_escape_pipes(entry['repo'])})" 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, - ] + cells = [project, _escape_pipes(entry["description"]), author] return "| " + " | ".join(cells) + " |" @@ -106,13 +83,17 @@ def check_index(community_dir: Path) -> bool: 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") + try: + 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) + except ValueError as exc: + print(f"Cannot build index: {exc}") return 1 - write_index(COMMUNITY_DIR) print("Wrote community/README.md") return 0 diff --git a/community/_ci/check_entry.py b/community/_ci/check_entry.py deleted file mode 100644 index 042960c..0000000 --- a/community/_ci/check_entry.py +++ /dev/null @@ -1,68 +0,0 @@ -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/check_projects.py b/community/_ci/check_projects.py new file mode 100644 index 0000000..aca6660 --- /dev/null +++ b/community/_ci/check_projects.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +import sys +from pathlib import Path + +from project_rules import load_projects, validate_projects + +COMMUNITY_DIR = Path(__file__).resolve().parent.parent +PROJECTS_FILE = COMMUNITY_DIR / "projects.yaml" + + +def check_projects(path: Path) -> list[str]: + projects, errors = load_projects(path) + if errors: + return errors + return validate_projects(projects) + + +def main(argv: list[str] | None = None) -> int: + args = sys.argv[1:] if argv is None else argv + path = Path(args[0]) if args else PROJECTS_FILE + + errors = check_projects(path) + if errors: + print("Community projects check failed:") + for error in errors: + print(f" - {error}") + return 1 + + projects, _ = load_projects(path) + print(f"OK {len(projects)} community project(s) validated.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/community/_ci/entry_rules.py b/community/_ci/entry_rules.py deleted file mode 100644 index d78cf3e..0000000 --- a/community/_ci/entry_rules.py +++ /dev/null @@ -1,201 +0,0 @@ -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", "