diff --git a/.claude/hooks/hooks.json b/.claude/hooks/hooks.json new file mode 100644 index 000000000000..2026e339749a --- /dev/null +++ b/.claude/hooks/hooks.json @@ -0,0 +1,16 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Write|Edit", + "hooks": [ + { + "type": "command", + "_comment": "Path traversal is intentional: hook config lives in .claude/, source lives under scripts/validate/ as a repo-level tool.", + "command": "printf '%s' \"$TOOL_INPUT\" | node ${PLUGIN_DIR}/../scripts/validate/front-matter-check/index.mjs" + } + ] + } + ] + } +} diff --git a/.claude/skills/draft-issue/SKILL.md b/.claude/skills/draft-issue/SKILL.md new file mode 100644 index 000000000000..4c71a4a9badd --- /dev/null +++ b/.claude/skills/draft-issue/SKILL.md @@ -0,0 +1,127 @@ +--- +name: draft-issue +description: >- + Draft GitHub issues for opentelemetry.io following the real issue templates + under `.github/ISSUE_TEMPLATE/`, the contributing guide, and the repo's live + label taxonomy. Use when creating issue drafts from investigation findings or + conversation context. +argument-hint: '' +allowed-tools: Read Grep Bash +effort: low +--- + +# Draft Issue + +Drafts ready-to-paste GitHub issues for `open-telemetry/opentelemetry.io`. The +templates under `.github/ISSUE_TEMPLATE/` and the live label set are the sources +of truth — when this skill drifts from them, trust the source. + +## Arguments {#arguments} + +- If `$ARGUMENTS` is empty, infer context from the conversation. If there is + insufficient context, ask the user what issue to draft. +- Otherwise, treat the full `$ARGUMENTS` string as the description. There are no + flags. + +## When to use + +- After investigating a problem in the OTel site codebase. +- When a conversation reveals a bug, docs gap, or feature need. +- When drafting a blog post proposal for OTel. + +## Issue types {#issue-types} + +The repo has five issue templates. Auto-detect type from context, or accept an +explicit type from the user. + +| Template file | Title prefix | Use for | +| --------------------- | ----------------- | ---------------------------------------------------- | +| `DOCS_UPDATE.yml` | `[Docs]: ` | Documentation errors, missing content, outdated info | +| `ISSUE_REPORT.yml` | `bug: ` | Site bugs, broken functionality, CI/workflow issues | +| `FEATURE_REQUEST.yml` | `feat: ` | New site features, tooling improvements | +| `BLOG_POST.yml` | `blog: ` | Blog post proposals (auto-applies `blog` label) | +| `PAGE_FEEDBACK.yml` | `page feedback: ` | Feedback from the "was this page helpful?" widget | + +When drafting, read the chosen template under `.github/ISSUE_TEMPLATE/` and +mirror its section labels verbatim so GitHub maps the body back to template form +fields. Required fields are marked in the template. + +For writing or reviewing the post itself after a `blog:` proposal lands, see the +`review-blog-post` skill. + +## Drafting rules + +From `content/en/docs/contributing/issues.md`: + +- **Be specific.** Describe what is missing, out of date, wrong, or needs + improvement. "Fix the security docs" is too broad; "Add details to the + 'Restricting network access' topic" is actionable. +- **Right-size the scope.** One issue = one reasonable unit of work; break broad + problems into smaller, reviewable issues. +- **Search first.** Check existing issues for duplicates before filing. +- **Reference related issues and PRs** with `#1234` for the same repo, or the + full URL for cross-repo references. +- **Concise, actionable** descriptions; no filler. +- Wrap body prose at 80 characters (convention, not enforced). + +## Labels {#labels} + +The repo's labels evolve; do not maintain a copy in this skill. Discover the +live set with: + +```bash +gh label list --repo open-telemetry/opentelemetry.io --limit 200 +``` + +Path-based auto-labeling (e.g. `blog`, `registry`, `i18n`, `lang:*`, several +`sig:*`) is defined in `.github/component-label-map.yml` and applies on PRs +only. You can still suggest these on issues manually. + +Governance for the `triage:*`, `type:*`, `priority:*`, and `sig:*` families +lives in `content/en/docs/contributing/sig-practices.md`. Skip `type:discussion` +— its own label description says "Do not use, convert discussion issues into +real Discussions." + +## Output format + +Produce a fenced block with title, suggested labels, and body. Labels are +comma-separated so they can be passed directly to +`gh issue create --label ""`. + +``` +**Title**: `bug: pr-approval-labels workflow adds ready-to-be-merged despite requested changes` + +**Labels**: `CI/infra`, `Github actions`, `p2-medium` + +--- + +### What happened? + +The `pr-approval-labels` workflow currently adds the `ready-to-be-merged` +label when a PR receives an approving review, but it does not check whether +other reviewers have requested changes. + +### What did you expect would happen? + +The workflow should skip adding the `ready-to-be-merged` label if any +reviewer has a pending "changes requested" status on the PR. + +### Name + path of the page + +.github/workflows/pr-approval-labels.yml + +### Additional context + +Related to #1234. +``` + +## References {#references} + +- `.github/ISSUE_TEMPLATE/*.yml` — five real templates; mirror their section + labels verbatim. +- `content/en/docs/contributing/issues.md` — user-facing guidance on filing + great issues. +- `content/en/docs/contributing/sig-practices.md` — label and triage governance. +- `.github/component-label-map.yml` — path-based PR auto-labeling. +- `gh label list --repo open-telemetry/opentelemetry.io --limit 200` — live + label set. diff --git a/.claude/skills/review-blog-post/SKILL.md b/.claude/skills/review-blog-post/SKILL.md new file mode 100644 index 000000000000..edd9bc10e856 --- /dev/null +++ b/.claude/skills/review-blog-post/SKILL.md @@ -0,0 +1,201 @@ +--- +name: review-blog-post +description: >- + Review OpenTelemetry blog posts for front matter compliance, content + conventions, GitHub link stability (`gh-url-hash`), spelling, and OTel + terminology. Use when reviewing a PR or draft under `content/en/blog/`. +argument-hint: '' +allowed-tools: Read Grep Glob Bash +model: sonnet +effort: medium +--- + +# Review Blog Post + +Review workflow for OpenTelemetry blog posts. The repo tooling — the +front-matter-check hook, prettier, markdownlint (`gh-url-hash`), cSpell, and the +publish-labels workflow — enforces the mechanical rules; this skill covers the +judgment layer those tools cannot check. + +## Arguments {#arguments} + +- If `$ARGUMENTS` is empty, ask for a file path or PR number. +- If `$ARGUMENTS` contains `/` or ends in `.md`, treat it as a repo-relative + file path. +- If `$ARGUMENTS` is a GitHub URL containing `/pull/`, extract the PR number + after `/pull/`. +- If `$ARGUMENTS` is a bare number or starts with `#`, treat it as a PR number. +- Otherwise, stop and ask for a valid file path or PR number. + +## Location + +Blog posts live under `content/en/blog/YYYY/`. Use a single `short-name.md` when +there are no images, or a `short-name/index.md` directory when there are. +`short-name` is kebab-case — no dates, no special characters. + +Scaffold from the archetype with Hugo: + +```sh +npx hugo new content/en/blog/$(date +%Y)/short-name.md # no images +npx hugo new content/en/blog/$(date +%Y)/short-name/index.md # with images +``` + +## Front matter + +A `PreToolUse` hook on `Write`/`Edit` +([`scripts/validate/front-matter-check/`][fm-check]) blocks any +`content/en/blog/**/*.md` change whose front matter is missing `title`, +`linkTitle`, `date` (must be `YYYY-MM-DD`), or `author` (must be a Markdown +link), or that introduces an H1 in the body. When reviewing an existing PR where +the hook didn't run, double-check those fields against +[`archetypes/blog.md`][archetype]. + +Judgment calls beyond the hook: + +- **`title`** — sentence case is typical; a few proper-noun-heavy posts use + Title Case. Keep it descriptive. +- **`author`** — single-author posts use a single-line Markdown link; multi- + author posts must use the YAML folded form (`>-`) because the list spans + lines. The trailing `(Organization)` suffix is optional but common. + + ```yaml + author: '[Juraci Paixao Krohling](https://github.com/jpkrohling) (OllyGarden)' + ``` + + ```yaml + author: >- + [Johanna Öjeling](https://github.com/johannaojeling) (Grafana Labs), + [Juliano Costa](https://github.com/julianocosta89) (Datadog), [Tristan + Sloughter](https://github.com/tsloughter) (community) + ``` + +- **`draft: true`** — work-in-progress; required for future-dated posts. +- **`canonical_url`** — set when the post is a cross-post; points to the + original. Preferred over the older `crosspost_url`. +- **`body_class: otel-with-contributions-from`** — set when secondary + contributors are credited in the intro paragraph (see + [Authoring rules](#authoring-rules)). +- **`issue`** — optional; only ~15% of recent posts set this. +- **`sig`** — sponsoring SIG (e.g. `Developer Experience SIG`). When present, + the PR should carry a matching `sig:` label. +- **`cSpell:ignore`** — see [Spelling](#spelling). + +## Submission prerequisites + +From [`content/en/docs/contributing/blog.md`][contrib-blog]: + +- Non-commercial, broadly relevant; no vendor product pitches. +- Prefer CNCF projects in examples (Jaeger for traces, Prometheus for metrics). +- A pre-submission issue is required; a SIG sponsor is strongly recommended + (ideally from a different company than the author). +- "Call for Contributors" posts follow the project-management process in + `open-telemetry/community`. + +## Authoring rules {#authoring-rules} + +- Start headings at `##` (no H1; the H1 is auto-generated from `title`) and + don't skip levels. +- Wrap prose at 80 columns (`npm run format`, prettier with + `proseWrap: always`). Don't hand-wrap — run the formatter. Skip URLs, code + blocks, and front matter values. +- Place images beside `index.md`; descriptive kebab-case filenames; always + include meaningful alt text. +- Always tag fenced code blocks with a language. +- Credit secondary contributors who aren't in the `author` field in the intro: + _"With contributions from [Name](https://github.com/username), …"_ and set + `body_class: otel-with-contributions-from`. +- Prefer active voice. Link external tools and OTel concepts on first mention + only — don't over-link. + +## GitHub links (`gh-url-hash`) + +A blog-only markdownlint rule +([`scripts/_md-rules/gh-url-hash/index.mjs`][gh-rule], enabled via +`content/en/blog/.markdownlint.yaml`) blocks default-branch links +(`main`/`master`) and short commit hashes in GitHub `blob`/`tree` URLs. Tags, +release refs, and full 40-character SHAs are allowed. + +Run `npm run fix:markdown` to auto-fix default-branch links by resolving the +current HEAD commit. Auto-fix needs network access; on rate-limit or unreachable +failures, fix manually with a full SHA or a release tag. + +## Spelling {#spelling} + +Spell-checking uses cSpell (`.cspell.yml`). Repo-wide additions go in +`.cspell/en-words.txt`; post-local words go in the `cSpell:ignore` front matter +field. Add `# prettier-ignore` immediately above `cSpell:ignore` only when the +line is long enough that the formatter would wrap it: + +```yaml +# prettier-ignore +cSpell:ignore: jpkrohling Krohling logdedup OllyGarden OTTL Paixao telemetrygen +``` + +## OTel terminology + +- **OpenTelemetry** is one word; **OTel** is acceptable shorthand only after the + first full mention. +- Signal names are lowercase: traces, metrics, logs. +- Component names are cased: SDK, API, Collector. +- Proper nouns: Jaeger, Zipkin, Prometheus, Kubernetes. +- Semantic-convention attribute names should match the current names in + [`docs/specs/semconv/`](https://opentelemetry.io/docs/specs/semconv/). + +## Publish timing + +- The `date` field drives publication. Use `draft: true` while the date is in + the future. +- A daily workflow ([`blog-publish-labels.yml`][publish-workflow], 7 AM UTC) + adds `ready-to-be-merged` only when **all** hold: docs-approver approval, + SIG/component-owner approval, and `date:` is in the past or today. The + workflow only labels — a human still merges. + +## Cross-posting + +Decide which version is canonical (typically the original OpenTelemetry post). +On any external copy, mention the original, link back to it, and set the +platform's canonical-URL tag if available. When the OTel post is the copy, set +`canonical_url` in its front matter. + +## Reviewing a PR + +Walk the post top-to-bottom against the sections above. The mechanical checks +below are what humans most often miss after the hook + linters pass: + +1. Run `npm run format` (wrap), `npm run fix:markdown` (`gh-url-hash`), + `npm run check:spelling`. All must be clean. +2. Author front matter: single-line vs. folded `>-` form correct? + `(Organization)` accurate? +3. Multi-author intro credits + `body_class: otel-with-contributions-from` set + if needed. +4. `gh-url-hash`: no `main`/`master` or short SHAs; tags or full SHAs only. +5. Submission prerequisites: non-commercial, CNCF tools preferred, SIG sponsor + identified. +6. OTel terminology consistent throughout. +7. `date` and `draft` set so the publish workflow gates the merge as intended. + +## References + +- [`archetypes/blog.md`][archetype] — canonical front matter template. +- [`content/en/docs/contributing/blog.md`][contrib-blog] — submission process, + cross-posting, `gh-url-hash` rationale. +- `content/en/blog/.markdownlint.yaml` — enables `gh-url-hash` for blog posts. +- [`scripts/_md-rules/gh-url-hash/index.mjs`][gh-rule] — authoritative rule + behavior. +- [`scripts/validate/front-matter-check/`][fm-check] — write-time hook source + + tests. +- [`.github/workflows/blog-publish-labels.yml`][publish-workflow] — publish date + and approval gating. +- `.cspell.yml`, `.cspell/en-words.txt` — spell-check configuration. +- `package.json` — `prettier.proseWrap: always` drives 80-char wrapping. + +[archetype]: + https://github.com/open-telemetry/opentelemetry.io/blob/main/archetypes/blog.md +[contrib-blog]: + https://github.com/open-telemetry/opentelemetry.io/blob/main/content/en/docs/contributing/blog.md +[fm-check]: + https://github.com/open-telemetry/opentelemetry.io/tree/main/scripts/validate/front-matter-check +[gh-rule]: + https://github.com/open-telemetry/opentelemetry.io/blob/main/scripts/_md-rules/gh-url-hash/index.mjs +[publish-workflow]: + https://github.com/open-telemetry/opentelemetry.io/blob/main/.github/workflows/blog-publish-labels.yml diff --git a/.claude/skills/review-pull-request/SKILL.md b/.claude/skills/review-pull-request/SKILL.md new file mode 100644 index 000000000000..bec54240977b --- /dev/null +++ b/.claude/skills/review-pull-request/SKILL.md @@ -0,0 +1,211 @@ +--- +name: review-pull-request +description: >- + Review pull requests for opentelemetry.io: CI check semantics, CLA and + approval-label workflow, refcache handling, locale rules, and content quality. + Use when reviewing a PR or debugging a CI failure in + open-telemetry/opentelemetry.io. +argument-hint: '' +allowed-tools: Bash Read Grep Glob +model: sonnet +effort: medium +--- + +# Review Pull Request + +Review workflow for pull requests in `open-telemetry/opentelemetry.io`. The +contributing guide and the per-check decoder in [`pr-checks.md`][pr-checks] are +the authoritative sources — when this skill drifts from them, trust them. + +For blog-specific rules (`gh-url-hash`, author front matter, publish-date +gating), defer to the sibling `review-blog-post` skill. For label drafting +guidance, defer to `draft-issue`. + +## Arguments {#arguments} + +- If `$ARGUMENTS` is empty, ask for a PR number or URL. +- If `$ARGUMENTS` is a GitHub URL containing `/pull/`, extract the numeric PR + number after `/pull/`. +- If `$ARGUMENTS` starts with `#`, strip the `#` and use the digits. +- If `$ARGUMENTS` is a bare number, use it. +- Otherwise, stop and ask for a valid PR number or URL. + +## When to use + +- Reviewing a PR in `open-telemetry/opentelemetry.io`. +- Debugging a CI check failure on a PR. +- Preparing your own PR for submission. + +## Workflow + +### 1. Setup + +Pull metadata, diff, and checks; classify changed files: + +```bash +gh pr view --json title,body,files,reviews,labels,author,isDraft,headRepositoryOwner +gh pr diff +gh pr checks +``` + +Group files into `content/en/blog/**`, `content/en/docs/**`, +`content//**`, `data/registry/**`, `.github/**`, `scripts/**`, or config — +classification drives which CI checks matter and which rules apply. + +### 2. Walk CI checks + +For each failing check, match ` / ` against +[`pr-checks.md`][pr-checks] — every check has a section describing what it +validates and the local fix command. Caveats: + +- Link checking is sharded (`en` / `locales-A-to-M` / `locales-N-to-Z`); a + single failing shard does not necessarily block merge — read the failure. +- Fork PRs can hit token-scope limits that look like check failures but are + permissions artifacts. Read the log before concluding. +- `Netlify Deploy Preview` failures: open **Details** for the build log before + reasoning about them. + +### 3. Verify process rules + +**Pre-merge approval** + +- CLA: every commit author email is covered (CNCF EasyCLA) — + [`pr-checks.md#easy-cla`][cla]. +- Linked issue: PR references an issue labeled `triage:accepted`. Exceptions: + auto-update PRs and hotfixes by maintainers/approvers — + [`sig-practices.md#prs`][prs]. +- Co-owned PRs: docs approver + SIG/locale approver — + [`sig-practices.md#co-owned-prs`][co-owned] and + [`#translation-prs`][translation]. + +**Content origin** + +- Submodules: non-maintainer PRs should not touch them; a maintainer fixes + before merge — [`sig-practices.md#general`][general]. +- Locale span: semantic changes are per-locale; editorial cross-locale edits are + OK and append `# patched` to `default_lang_commit` — + [`localization.md#prs-should-not-span-locales`][locale-span] and + [`#patch-locale-links`][patch-locale]. + +**Branch state** + +- Branch freshness: authors should not continuously rebase — maintainers update + before merge — [`sig-practices.md#general`][general]. +- Stale handling: `stale` after 21 days inactivity; never auto-closed — + [`sig-practices.md#prs`][prs]. + +### 4. Review content + +For docs PRs (`content/en/docs/**`): + +- **Front matter.** Valid YAML; appropriate `title`, `linkTitle`, `weight`, + `description`; Hugo-specific fields intact. +- **Terminology.** "OpenTelemetry" one word; "OTel" only after first full + mention; signal names lowercase (`traces`, `metrics`, `logs`); component names + cased (`SDK`, `API`, `Collector`); proper nouns cased. Enforced by `textlint` + via `.textlintrc.yml`. +- **Link references.** Prefer collapsed form `[text][]` over shortcut `[text]`; + enforced by `scripts/_md-rules/no-shortcut-ref-link/`. +- **Markdown extensions.** GitHub alerts and Obsidian callouts are OK. +- **Internal links.** Use Hugo `ref` / `relref` or paths starting with + `/docs/...`; never full `https://opentelemetry.io` URLs. +- **Code blocks** carry a language tag; **images** carry meaningful alt text. + +For blog PRs (`content/en/blog/**`), defer to the `review-blog-post` skill. + +### 5. Final pass and output + +Walk this checklist before writing the review: + +**CI and process** + +- [ ] `Easy CLA` green (or author has a fix path). +- [ ] Netlify preview builds. +- [ ] Each failing `check-*` assessed against [`pr-checks.md#checks`][checks]. +- [ ] Linked issue is `triage:accepted` (or this is an auto/hotfix PR). +- [ ] Does not span locales with semantic changes — or uses `# patched` for + editorial cross-locale edits. +- [ ] First-time-contributor AI checklist in the PR description is filled in and + looks human-written. +- [ ] No unrelated changes bundled. + +**Labels** + +- [ ] Auto-applied labels look correct (sig/lang/blog/registry/i18n); none added + by hand. +- [ ] `ready-to-be-merged` / `missing:*` not touched manually. +- [ ] `sig-approval-missing` added if docs approval landed without SIG approval + on a co-owned PR. + +**Content** + +- [ ] Front matter valid; terminology consistent; code blocks tagged; images + have alt text; internal links use paths or Hugo refs (not + `opentelemetry.io` URLs); no shortcut-form reference links. + +**Refcache and links** + +- [ ] `refcache.json` updates (if any) committed in the PR. +- [ ] No hand-edits to `refcache.json`. +- [ ] Unreachable-but-valid URLs use `?link-check=no` (see + [Refcache](#refcache)). + +Then structure the review as: + +- **CI Status Summary** — one line per check (pass/fail/skip); call out fork-PR + permissions artifacts separately from real failures. +- **Required Changes (Blocking)** — issues that must be fixed before merge. Cite + a file or check name for each. +- **Suggested Improvements (Non-blocking)** — terminology, cross-link + opportunities, phrasing. +- **Positive Feedback** — short but present. + +## Refcache {#refcache} + +`static/refcache.json` is a 1MB+ cache of external-link status codes. +`npm run check:links` updates it as a side effect — authors commit the updated +file themselves ([`pr-checks.md#build-and-check-links`][build-checks]). The +`Links / REFCACHE updates?` job fails if the on-branch cache is stale relative +to what the link check produced. + +Do not hand-edit `refcache.json`. If a URL returns a non-200 for server reasons +(blocked bot, LinkedIn 999, …), append `?link-check=no` (or `&link-check=no`) to +the URL — [`pr-checks.md#handling-valid-external-links`][handling-links]. +Maintainers can validate 4xx entries via +`./scripts/double-check-refcache-4XX.mjs`. + +For resolving merge/rebase conflicts in `refcache.json`, see the +`resolve-refcache-conflicts` skill. + +## References + +Source-of-truth files — read on demand: + +- [`pr-checks.md`][pr-checks] — per-check decoder (what each check validates, + how to fix). +- [`npm-scripts.md`][npm-scripts] — full `npm run` catalog. +- [`pull-requests.md`][pull-requests], [`sig-practices.md`][sig-practices], + [`localization.md`][localization], [`issues.md`][issues] — process rules + deep-linked above. + +[pr-checks]: ../../../content/en/docs/contributing/pr-checks.md +[checks]: ../../../content/en/docs/contributing/pr-checks.md#checks +[cla]: ../../../content/en/docs/contributing/pr-checks.md#easy-cla +[build-checks]: + ../../../content/en/docs/contributing/pr-checks.md#build-and-check-links +[handling-links]: + ../../../content/en/docs/contributing/pr-checks.md#handling-valid-external-links +[npm-scripts]: ../../../content/en/site/build/npm-scripts.md +[pull-requests]: ../../../content/en/docs/contributing/pull-requests.md +[sig-practices]: ../../../content/en/docs/contributing/sig-practices.md +[localization]: ../../../content/en/docs/contributing/localization.md +[issues]: ../../../content/en/docs/contributing/issues.md +[prs]: ../../../content/en/docs/contributing/sig-practices.md#prs +[co-owned]: ../../../content/en/docs/contributing/sig-practices.md#co-owned-prs +[translation]: + ../../../content/en/docs/contributing/sig-practices.md#translation-prs +[general]: ../../../content/en/docs/contributing/sig-practices.md#general +[locale-span]: + ../../../content/en/docs/contributing/localization.md#prs-should-not-span-locales +[patch-locale]: + ../../../content/en/docs/contributing/localization.md#patch-locale-links diff --git a/content/en/site/skills/_index.md b/content/en/site/skills/_index.md index 12c92090dc1a..8a944f6e06b8 100644 --- a/content/en/site/skills/_index.md +++ b/content/en/site/skills/_index.md @@ -20,11 +20,35 @@ procedures are defined in this section. As mentioned above, skills are defined in [`.claude/skills/`][], they are: +- [`/draft-issue `][draft-issue]: draft a GitHub issue in the + `opentelemetry.io` repository following issue templates, contributing + guidelines, and the label taxonomy. - [`/resolve-refcache-conflicts `][resolve-refcache-conflicts]: resolve `static/refcache.json` merge/rebase conflicts. +- [`/review-blog-post `][review-blog-post]: review + an OpenTelemetry blog post for front matter compliance, content conventions, + GitHub link stability (`gh-url-hash`), spelling, and OTel terminology. +- [`/review-pull-request `][review-pull-request]: review a + pull request for CI check semantics, CLA and approval-label workflow, refcache + handling, locale rules, and content quality. Some agent chats let you invoke a skill by typing `/` followed by its name. +## Hooks + +Alongside the agent skills above, [hooks][] run automatically on certain tool +events. Configuration lives in [`.claude/hooks/hooks.json`][hooks-json]; hook +source lives under [`scripts/validate/`][validate]. + +- **Blog front matter check**: a `PreToolUse` hook on `Write` and `Edit` that + blocks changes to `content/en/blog/**/*.md` when the front matter is missing + required fields, uses a bad date format, or introduces an H1 heading. It + applies the same conventions as [`/review-blog-post`](#agent-skills) at + write-time, without waiting for review. Source: + [`scripts/validate/front-matter-check/`][frontmatter-check]. Pure logic lives + in `index.mjs` and is covered by `index.test.mjs` in the same folder + (`npm run test:local-tools` to run it). + ## Maintainer skills See the section index below. @@ -32,5 +56,18 @@ See the section index below. [`.claude/skills/`]: https://github.com/open-telemetry/opentelemetry.io/tree/main/.claude/skills [agentskills.io]: https://agentskills.io +[draft-issue]: + https://github.com/open-telemetry/opentelemetry.io/blob/main/.claude/skills/draft-issue/SKILL.md [resolve-refcache-conflicts]: https://github.com/open-telemetry/opentelemetry.io/blob/main/.claude/skills/resolve-refcache-conflicts/SKILL.md +[review-blog-post]: + https://github.com/open-telemetry/opentelemetry.io/blob/main/.claude/skills/review-blog-post/SKILL.md +[review-pull-request]: + https://github.com/open-telemetry/opentelemetry.io/blob/main/.claude/skills/review-pull-request/SKILL.md +[hooks]: https://docs.claude.com/en/docs/claude-code/hooks +[hooks-json]: + https://github.com/open-telemetry/opentelemetry.io/blob/main/.claude/hooks/hooks.json +[validate]: + https://github.com/open-telemetry/opentelemetry.io/tree/main/scripts/validate +[frontmatter-check]: + https://github.com/open-telemetry/opentelemetry.io/tree/main/scripts/validate/front-matter-check diff --git a/scripts/validate/front-matter-check/index.mjs b/scripts/validate/front-matter-check/index.mjs new file mode 100755 index 000000000000..372a47a848a3 --- /dev/null +++ b/scripts/validate/front-matter-check/index.mjs @@ -0,0 +1,110 @@ +#!/usr/bin/env node +// Frontmatter validation hook for OTel blog posts. +// Fires on Write/Edit tool calls targeting content/en/blog/**/*.md files. +// Reads TOOL_INPUT from stdin (JSON with file_path and content/new_string). + +const BLOG_PATH_RE = /content\/en\/blog\/.*\.md$/; +const DATE_RE = /^\d{4}-\d{2}-\d{2}$/; +const AUTHOR_LINK_RE = /^["']?\[[^\]]+\]\(https?:\/\/[^)]+\)["']?$/; +const AUTHOR_BLOCK_SCALAR_RE = /^[>|][-+]?$/; + +export function validate({ filePath, content }) { + if (!filePath || !BLOG_PATH_RE.test(filePath)) return []; + if (!content || !content.startsWith('---')) return []; + + const lines = content.split('\n'); + if (lines[0] !== '---') return []; + const endIdx = lines.indexOf('---', 1); + if (endIdx === -1) return []; + + const frontmatter = lines.slice(1, endIdx); + const body = lines.slice(endIdx + 1).join('\n'); + const errors = []; + + const find = (key) => frontmatter.find((l) => l.startsWith(`${key}:`)); + const valueOf = (key) => { + const line = find(key); + if (!line) return null; + return line + .slice(key.length + 1) + .trim() + .replace(/^['"]|['"]$/g, ''); + }; + + for (const field of ['title', 'date', 'author']) { + if (!find(field)) + errors.push(`Missing required frontmatter field: ${field}`); + } + + if (!find('linkTitle')) { + errors.push('Missing required frontmatter field: linkTitle'); + } else if (!valueOf('linkTitle')) { + errors.push('Required frontmatter field linkTitle must be non-empty'); + } + + const date = valueOf('date'); + if (date && !DATE_RE.test(date)) { + errors.push(`Date format must be YYYY-MM-DD, got: ${date}`); + } + + const authorLine = find('author'); + if (authorLine) { + const raw = authorLine.slice('author:'.length).trim(); + if (raw && !AUTHOR_BLOCK_SCALAR_RE.test(raw) && !AUTHOR_LINK_RE.test(raw)) { + errors.push( + 'Author should be a Markdown link like [First Last](https://github.com/username), optionally quoted, or use YAML block scalar form for multi-author entries', + ); + } + } + + if (/^# [^#]/m.test(body)) { + errors.push( + 'Blog posts must not use H1 (#) headings. Start with ## (H2) instead', + ); + } + + return errors; +} + +async function readStdin() { + if (process.stdin.isTTY) return ''; + const chunks = []; + for await (const chunk of process.stdin) chunks.push(chunk); + return Buffer.concat(chunks).toString('utf8'); +} + +async function main() { + let input; + try { + input = await readStdin(); + } catch (e) { + console.error(`frontmatter-check: failed to read stdin: ${e.message}`); + process.exit(0); + } + if (!input.trim()) process.exit(0); + + let data; + try { + data = JSON.parse(input); + } catch (e) { + console.error( + `frontmatter-check: failed to parse TOOL_INPUT JSON: ${e.message}`, + ); + process.exit(0); + } + + const filePath = data.file_path || ''; + const content = data.content ?? data.new_string ?? ''; + + const errors = validate({ filePath, content }); + if (errors.length === 0) process.exit(0); + + console.log('OTel Blog Frontmatter Issues:'); + for (const err of errors) console.log(` - ${err}`); + process.exit(1); +} + +// Run only when invoked directly +if (import.meta.url === `file://${process.argv[1]}`) { + main(); +} diff --git a/scripts/validate/front-matter-check/index.test.mjs b/scripts/validate/front-matter-check/index.test.mjs new file mode 100644 index 000000000000..6599f38b8e0d --- /dev/null +++ b/scripts/validate/front-matter-check/index.test.mjs @@ -0,0 +1,124 @@ +// Tests for the pure validate() orchestrator: path filtering, frontmatter +// parsing, and per-rule reporting for blog posts. + +import { describe, test } from 'node:test'; +import assert from 'node:assert/strict'; + +import { validate } from './index.mjs'; + +const BLOG_PATH = 'content/en/blog/2024-01-01/post/index.md'; + +const wrap = (fm, body = '\n## heading\n') => `---\n${fm}\n---\n${body}`; + +const validFrontmatter = [ + 'title: Hello', + 'date: 2024-01-01', + 'author: "[Jane Doe](https://github.com/jane)"', + 'linkTitle: Hello', +].join('\n'); + +describe('frontmatter-check: validate()', () => { + test('passes on a well-formed blog post', () => { + assert.deepEqual( + validate({ filePath: BLOG_PATH, content: wrap(validFrontmatter) }), + [], + ); + }); + + test('skips files outside content/en/blog', () => { + assert.deepEqual( + validate({ + filePath: 'content/en/docs/index.md', + content: wrap('title: x', '\n# allowed here\n'), + }), + [], + ); + }); + + test('skips content without frontmatter', () => { + assert.deepEqual( + validate({ filePath: BLOG_PATH, content: '## just a body\n' }), + [], + ); + }); + + test('reports missing required fields', () => { + const errors = validate({ + filePath: BLOG_PATH, + content: wrap('date: 2024-01-01'), + }); + assert.ok(errors.some((e) => e.includes('title'))); + assert.ok(errors.some((e) => e.includes('author'))); + assert.ok(errors.some((e) => e.includes('linkTitle'))); + }); + + test('reports empty linkTitle', () => { + const errors = validate({ + filePath: BLOG_PATH, + content: wrap( + [ + 'title: x', + 'date: 2024-01-01', + 'author: "[J](https://x/j)"', + 'linkTitle:', + ].join('\n'), + ), + }); + assert.ok(errors.some((e) => e.includes('linkTitle must be non-empty'))); + }); + + test('reports bad date format', () => { + const errors = validate({ + filePath: BLOG_PATH, + content: wrap( + [ + 'title: Hello', + 'date: 2024/01/01', + 'author: "[Jane](https://github.com/jane)"', + 'linkTitle: Hello', + ].join('\n'), + ), + }); + assert.ok(errors.some((e) => e.includes('Date format must be YYYY-MM-DD'))); + }); + + test('reports H1 heading in body', () => { + const errors = validate({ + filePath: BLOG_PATH, + content: wrap(validFrontmatter, '\n# not allowed\n'), + }); + assert.ok(errors.some((e) => e.includes('must not use H1'))); + }); + + test('rejects non-link author', () => { + const errors = validate({ + filePath: BLOG_PATH, + content: wrap( + [ + 'title: x', + 'date: 2024-01-01', + 'author: Jane Doe', + 'linkTitle: x', + ].join('\n'), + ), + }); + assert.ok(errors.some((e) => e.includes('Markdown link'))); + }); + + test('accepts YAML block-scalar author for multi-author entries', () => { + const errors = validate({ + filePath: BLOG_PATH, + content: wrap( + [ + 'title: x', + 'date: 2024-01-01', + 'author: >-', + ' [Jane](https://github.com/jane),', + ' [John](https://github.com/john)', + 'linkTitle: x', + ].join('\n'), + ), + }); + assert.deepEqual(errors, []); + }); +}); diff --git a/static/refcache.json b/static/refcache.json index 60a6a5e912f1..146cf3c1303e 100644 --- a/static/refcache.json +++ b/static/refcache.json @@ -1791,6 +1791,10 @@ "StatusCode": 200, "LastSeen": "2026-04-23T10:11:05.682789825Z" }, + "https://docs.claude.com/en/docs/claude-code/hooks": { + "StatusCode": 200, + "LastSeen": "2026-04-23T19:54:03.404555836Z" + }, "https://docs.cloud.google.com/run/docs/write-functions": { "StatusCode": 200, "LastSeen": "2026-04-24T10:07:59.168756143Z" @@ -15155,10 +15159,26 @@ "StatusCode": 206, "LastSeen": "2026-04-28T12:59:31.515805011Z" }, + "https://github.com/open-telemetry/opentelemetry.io/blob/main/.claude/hooks/hooks.json": { + "StatusCode": 206, + "LastSeen": "2026-04-23T10:14:15.563797015Z" + }, + "https://github.com/open-telemetry/opentelemetry.io/blob/main/.claude/skills/draft-issue/SKILL.md": { + "StatusCode": 206, + "LastSeen": "2026-04-23T10:14:15.563797015Z" + }, "https://github.com/open-telemetry/opentelemetry.io/blob/main/.claude/skills/resolve-refcache-conflicts/SKILL.md": { "StatusCode": 206, "LastSeen": "2026-04-23T10:14:15.563797015Z" }, + "https://github.com/open-telemetry/opentelemetry.io/blob/main/.claude/skills/review-blog-post/SKILL.md": { + "StatusCode": 206, + "LastSeen": "2026-04-23T10:14:15.563797015Z" + }, + "https://github.com/open-telemetry/opentelemetry.io/blob/main/.claude/skills/review-pull-request/SKILL.md": { + "StatusCode": 206, + "LastSeen": "2026-04-23T10:14:15.563797015Z" + }, "https://github.com/open-telemetry/opentelemetry.io/blob/main/.cspell.yml": { "StatusCode": 206, "LastSeen": "2026-04-28T12:56:50.690752624Z" @@ -15451,6 +15471,14 @@ "StatusCode": 206, "LastSeen": "2026-04-23T10:14:19.023581507Z" }, + "https://github.com/open-telemetry/opentelemetry.io/tree/main/scripts/validate": { + "StatusCode": 206, + "LastSeen": "2026-04-23T10:14:15.563797015Z" + }, + "https://github.com/open-telemetry/opentelemetry.io/tree/main/scripts/validate/front-matter-check": { + "StatusCode": 206, + "LastSeen": "2026-04-23T10:14:15.563797015Z" + }, "https://github.com/open-telemetry/opentelemetry.io/tree/main/templates/registry-entry.yml": { "StatusCode": 206, "LastSeen": "2026-04-28T10:25:03.242378274Z"