{"owner":"karmada-io","repo":"karmada","hasSkills":true,"totalSkillsCount":6,"totalTokensCount":8155,"categories":["plugin-manifest","anthropic-skill","claude-rule"],"hasMcp":false,"mcpConfig":null,"found":["vendor/github.com/google/gnostic-models/extensions/README.md",".claude/skills/e2e-root-cause-analysis/SKILL.md",".claude/skills/generating-release-notes/SKILL.md",".claude/skills/generating-release-notes/scripts/fetch_pr_info.py",".claude/skills/generating-release-notes/references/karmada-release-note-format.md",".claude/skills/generating-release-notes/scripts/fetch_contributors.sh"],"skills":{"vendor/github.com/google/gnostic-models/extensions/README.md":"# Extensions\n\n**Extension Support is experimental.**\n\nThis directory contains support code for building Gnostic extensio handlers and\nassociated examples.\n\nExtension handlers can be used to compile vendor or specification extensions\ninto protocol buffer structures.\n\nLike plugins, extension handlers are built as separate executables. Extension\nbodies are written to extension handlers as serialized\nExtensionHandlerRequests.\n",".claude/skills/e2e-root-cause-analysis/SKILL.md":"---\nname: e2e-root-cause-analysis\ndescription: Find the root cause of failing or flaky Karmada E2E tests from CI runs. Use when a user reports an e2e test failure, a flaky CI job, or asks to find the root cause of an e2e timeout, providing a GitHub Actions run/job URL, a PR number/URL, or logs.\n---\n\n# E2E Root Cause Analysis\n\nFind the root cause of a Karmada E2E failure by working **backwards** from the error you see to the reason behind it. Never stop at the first error message. For each finding, answer: \"what does this error mean, and what caused it?\" Repeat until you reach a root cause that is fully backed by log evidence.\n\n## Step 0: Find the failing job (if only a PR is given)\n\nIf the user gives a PR number or PR URL instead of a job URL, find the failing job first:\n\n```bash\n# List all checks of the PR; failing ones come with their job URLs\ngh pr checks <pr-number> --repo karmada-io/karmada\n\n# If the failure was on an older commit, list runs for that commit\ngh run list --repo karmada-io/karmada --commit <sha> --json databaseId,name,conclusion,url\n```\n\nPick the failing e2e job (e.g. `e2e test (v1.34.0)`) and take its `.../actions/runs/<run_id>/job/<job_id>` URL. If several jobs failed, ask the user which one to look at, or start with the e2e one.\n\n## Step 1: Collect the evidence\n\nGiven a GitHub Actions run/job URL (e.g. `.../actions/runs/<run_id>/job/<job_id>`):\n\n1. Download the job log (the ginkgo output):\n\n   ```bash\n   gh api repos/karmada-io/karmada/actions/jobs/<job_id>/logs > job.log\n   ```\n\n2. List and download the component-log artifact for the failing job's Kubernetes version. This is the most valuable evidence — it holds the logs of every Karmada component and member cluster:\n\n   ```bash\n   gh api repos/karmada-io/karmada/actions/runs/<run_id>/artifacts \\\n     --jq '.artifacts[] | {id, name}'\n   gh run download <run_id> --repo karmada-io/karmada -n karmada_e2e_log_<k8s_version>\n   ```\n\n   Artifact layout:\n   - `karmada-host/karmada-host-control-plane/containers/` — control-plane components: `karmada-scheduler-*`, `karmada-controller-manager-*`, `karmada-webhook-*`, `karmada-aggregated-apiserver-*`, `karmada-scheduler-estimator-<member>-*`, etc. There are usually two pods per component (leader election); grep both, only the active one has the useful lines.\n   - `member3/` — member cluster logs, including `karmada-agent-*` for pull-mode cluster.\n\n## Step 2: Find the failure in the ginkgo log\n\n```bash\ngrep -n \"\\[FAILED\\]\\|Summarizing\" job.log\n```\n\nFor the failing spec, extract:\n\n- The **assertion** that failed (framework helper file:line) and the timeout duration.\n- The **Timeline** section: every `STEP:` with its timestamp. Timestamps are the key — they let you match the test's actions against component logs.\n- The Timeline of the **previous test case** that shares resources (same CRD, namespace, cluster labels...). Flaky tests are often caused by the previous case's cleanup, which runs in the background and races with the current case's setup.\n\nWarning signs to look for in the timeline:\n\n- A readiness wait (a `framework.Wait*` helper) that passes much faster than normal (milliseconds instead of seconds). This usually means the check read **stale state** left over from the previous case.\n- Cleanup steps of the previous case that only wait for the control plane, while changes on the member clusters are still in flight.\n\n## Step 3: Trace backwards through component logs\n\nPresent the analysis in reverse order — from the error you see down to the root cause. At each level, state: *what was seen → what it means → what caused it*. A typical chain:\n\n1. **The test error.** e.g. a timeout while waiting for some object to reach the expected state. Meaning: what the test expected never happened. Question: why?\n\n2. **The decision of the component that owns the object.** Grep that component's log for the object name, e.g.:\n\n   ```bash\n   grep -h \"<object-name>\" karmada-<component>-*.log\n   ```\n\n   Depending on the failure, the owning component may be the scheduler (binding never scheduled, `Warning` events explaining why), the controller-manager (work not dispatched, resource not synced to members), the webhook (request rejected), or the agent (pull-mode cluster out of sync). Count how many times the object shows up: if it appears once and never again, the component did **not retry** — see the note below.\n\n3. **Why the component made that decision.** The decision is usually based on some state (cluster status, cache, quota, ...). Grep the logs of the controller that writes that state around the failure window, and build a timeline of those writes with exact timestamps. Compare it with the test's Timeline from Step 2: look for a window in which the state the component read was stale or briefly wrong.\n\n4. **The root cause.** Often a race between test cases: the previous case's cleanup returned after waiting only for the control plane, while changes on the member clusters and status collection were still in flight. State it with the exact timestamps and log lines that prove the window.\n\nIf the failing object was expected to recover but never did (e.g. it appears only once in the owning component's log), do not assume the runtime retries on its own — check how retry/requeue actually works in the component's code before claiming \"it should self-heal\".\n\n## Step 4: Report\n\nWrite the conclusion as an evidence chain, from the error backwards:\n\n1. The error seen (assertion, timeout, file:line).\n2. What it means (which expected state was never reached).\n3. Direct cause with log lines + timestamps (the owning component's decision).\n4. Deeper cause (the stale or briefly wrong state, race window with timestamps).\n5. Root cause (e.g. cleanup returned too early and did not wait for member clusters to catch up).\n6. Why it did not self-heal, if relevant (checked against component code).\n\nQuote exact log lines with timestamps and file names. Check every claim about runtime behavior against the code in the repository (e.g. `pkg/scheduler/`, `pkg/controllers/`) before stating it. Clean up downloaded logs afterwards.\n\n## Step 5: Improve this skill\n\nAfter the diagnosis is done, review what you learned. If you found something that would help future diagnoses — a new failure pattern, a better command, a log location not listed here, or a wrong assumption in this document — propose an update to this skill file:\n\n- Keep the addition **generic**: describe the pattern, not the specific test, object names, or timestamps of this one case.\n- Show the user the proposed change and let them decide whether to apply it.\n- Do not commit the change yourself.\n",".claude/skills/generating-release-notes/SKILL.md":"---\nname: generating-release-notes\ndescription: \"Use when preparing changelogs or release notes from merged pull requests for a patch, minor, or preview release.\"\nargument-hint: \"Target release version, for example v1.14.5 or v1.15.0-rc.0\"\n---\n\n# Generating Release Notes\n\n## Overview\n\nGenerate release notes from merged pull requests. Complete each gate in order. Do not start writing until the target version, comparison range, and format plugin are known.\n\n## Inputs\n\nA target version is required. If it is missing or unclear, ask for it and stop. Do not guess \"the next release.\"\n\nWork out these values only after the target is known:\n\n- Release type: patch, minor, or preview.\n- Previous version or preview tag.\n- Comparison branch.\n- Output changelog path.\n\nThe selected format plugin defines how to work out these values for its repository.\n\n## Select a Format Plugin\n\n1. Determine the Git repository root directory name with `basename \"$(git rev-parse --show-toplevel)\"`.\n2. Look at the Markdown files under [references](./references/). Select a plugin whose `Repository Match` includes that directory name.\n3. If exactly one plugin matches, load it and follow it exactly.\n4. If multiple plugins match, ask the user to choose.\n5. If no plugin matches, stop and report the directory name. Ask for a new repository format plugin. Never silently use another repository's format.\n\nYou can add a format plugin for a new repository. Add `<repository>-release-note-format.md` under `references/` with these sections:\n\n- `Repository Match`\n- `Upstream Data Source`\n- `Version Resolution`\n- `Output Location`\n- `Document Structure`\n- `Entry Format and Classification`\n- `Language and Style`\n- `Validation`\n\n## Required Workflow\n\n### Gate 1: Resolve the Release Range\n\n1. Classify the target as patch, minor, or preview using the selected plugin.\n2. Resolve the previous tag and head branch using the plugin's rules.\n3. Check the target, previous tag, branch, and output path against what the user asked for.\n4. Stop if anything is unclear. Do not switch to a different tag or branch without confirmation.\n\nDo not continue until Gate 1 passes.\n\n### Gate 2: Collect Merged Pull Requests\n\n1. Check that `python3` and the included [fetch_pr_info.py](./scripts/fetch_pr_info.py) exist.\n2. Check that `GITHUB_TOKEN` is set, but do not check its scopes. If it is not set but `gh auth status` reports a logged-in account, reuse that credential with `export GITHUB_TOKEN=$(gh auth token)` in the same shell as the scripts. Otherwise, ask the user to set a token with the `public_repo` scope in their environment. Never ask the user to send a token through chat, and never print the token value.\n3. From the repository root, run the included script with the base tag and head branch from Gate 1 and the `owner/repository` value listed under the plugin's `Upstream Data Source`:\n\n\t```bash\n\tpython3 .claude/skills/generating-release-notes/scripts/fetch_pr_info.py <base-tag> <head-branch> --repo <owner/repository>\n\t```\n\n4. Use only the `SUMMARY OF PRS WITH USER-FACING CHANGES` section. Ignore debug output and PRs without user-facing changes.\n5. Keep the PR number, title, author, kind, and every user-facing change. If a PR has more than one user-facing change, split them into separate entries.\n\nDo not continue if collection fails or credentials are invalid.\n\n### Gate 3: Collect Contributors\n\nRun this gate only when the selected format plugin requires a Contributors section.\n\n1. Reuse the same base tag and head branch from Gate 1. Contributor collection and PR collection must use the same comparison range.\n2. From the repository root, run the included [fetch_contributors.sh](./scripts/fetch_contributors.sh) with the owner and repository listed under `Upstream Data Source`:\n\n\t```bash\n\t.claude/skills/generating-release-notes/scripts/fetch_contributors.sh <owner> <repository> <base-tag> <head-branch>\n\t```\n\n3. The output is the list of GitHub authors whose commits were merged in that comparison range.\n4. Keep unique GitHub handles sorted alphabetically.\n\nDo not continue if contributor collection fails.\n\n### Gate 4: Classify and Draft\n\n1. Use the PR kind returned by the collection script as the main signal for choosing a category.\n2. Apply the plugin's rules for which category wins when more than one applies.\n3. Rewrite each entry in the tense and entry syntax the plugin requires, without changing its technical meaning. Make the leading verb and wording match the final category. If you move an entry to another category, re-read its wording and rewrite it if needed.\n4. Group entries as required by the plugin.\n5. Build the complete release section using the structure for the target release type.\n6. Add the commit authors collected in Gate 3 when the release requires a Contributors section.\n\nDo not make up user-facing changes, and do not guess at behavior from a PR title alone.\n\n### Gate 5: Update the Changelog\n\n1. Read the existing changelog before editing.\n2. Insert the new release in the order the plugin requires.\n3. Keep existing release sections and unrelated user changes as they are.\n4. Run the plugin's TOC or formatting command after content is final.\n\n### Gate 6: Validate\n\nCheck the requirements that apply to every repository:\n\n- No user-facing change is duplicated or missing.\n- Existing changelog content and unrelated user changes are unchanged.\n\nRun every check in the selected plugin's `Validation` section. Fix all failures before finishing.\n\n### Gate 7: Improve this skill\n\nAfter the release notes are done, think about what you learned. If you found something that would help future runs — a new failure pattern, a better command, or a wrong assumption in this document — propose an update to this skill file:\n\n- Show the user the proposed change and let them decide whether to apply it.\n- Do not commit the change yourself.\n\n## Common Failures\n\n| Failure | Required response |\n|---|---|\n| Target version is missing | Ask for it; do not guess. |\n| No format plugin matches | Stop and request a plugin. |\n| PR or contributor collection fails | Report the error and stop; do not make up or replace data. |\n",".claude/skills/generating-release-notes/scripts/fetch_pr_info.py":"#!/usr/bin/env python3\n# Copyright 2026 The Karmada Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"Fetch merged PR metadata and user-facing changes for release notes.\"\"\"\n\nimport argparse\nimport json\nimport os\nimport re\nimport sys\nimport urllib.error\nimport urllib.request\n\n\nGITHUB_API_BASE = \"https://api.github.com\"\nNO_RELEASE_NOTE_VALUES = {\n    \"\",\n    \"n/a\",\n    \"na\",\n    \"no\",\n    \"none\",\n    \"noop\",\n    \"nope\",\n    \"nothing\",\n}\n\n\ndef github_request(url, token, payload=None):\n    \"\"\"Send an authenticated GitHub API request and decode its JSON body.\"\"\"\n    headers = {\n        \"Accept\": \"application/vnd.github.v3+json\",\n        \"User-Agent\": \"Release-Note-Generator\",\n    }\n    if token:\n        headers[\"Authorization\"] = f\"Bearer {token}\"\n\n    data = None\n    if payload is not None:\n        headers[\"Content-Type\"] = \"application/json\"\n        data = json.dumps(payload).encode(\"utf-8\")\n\n    request = urllib.request.Request(url, data=data, headers=headers)\n    try:\n        with urllib.request.urlopen(request) as response:\n            return json.load(response)\n    except urllib.error.HTTPError as error:\n        detail = error.read().decode(\"utf-8\", errors=\"replace\")\n        print(f\"GitHub request failed ({error.code}): {detail}\", file=sys.stderr)\n    except urllib.error.URLError as error:\n        print(f\"GitHub request failed: {error.reason}\", file=sys.stderr)\n    return None\n\n\ndef get_commit_comparison(repo_owner, repo_name, base_tag, head_branch, token):\n    \"\"\"Get the GitHub comparison between the base tag and head branch.\"\"\"\n    base_url = (\n        f\"{GITHUB_API_BASE}/repos/{repo_owner}/{repo_name}/compare/\"\n        f\"{base_tag}...{head_branch}\"\n    )\n    comparison = None\n    commits = []\n    page = 1\n\n    while True:\n        response = github_request(f\"{base_url}?per_page=100&page={page}\", token)\n        if not response:\n            break\n        if comparison is None:\n            comparison = response.copy()\n\n        page_commits = response.get(\"commits\", [])\n        commits.extend(page_commits)\n        total_commits = comparison.get(\"total_commits\", len(commits))\n        if len(commits) >= total_commits or len(page_commits) < 100:\n            break\n        page += 1\n\n    if comparison is not None:\n        comparison[\"commits\"] = commits\n    return comparison\n\n\ndef get_pr_details_batch(repo_owner, repo_name, pr_numbers, token):\n    \"\"\"Fetch pull request title, body, and author in one GraphQL request.\"\"\"\n    if not pr_numbers:\n        return {}\n\n    query_parts = []\n    for index, number in enumerate(pr_numbers):\n        query_parts.append(\n            f\"pr{index}: pullRequest(number: {number}) {{ \"\n            \"number title body author { login } \"\n            \"labels(first: 100) { nodes { name } } }\"\n        )\n    query = (\n        \"{ repository(owner: \\\"\"\n        + repo_owner\n        + \"\\\", name: \\\"\"\n        + repo_name\n        + \"\\\") { \"\n        + \" \".join(query_parts)\n        + \" } }\"\n    )\n    response = github_request(\n        \"https://api.github.com/graphql\", token, {\"query\": query}\n    )\n    if not response:\n        return None\n    if response.get(\"errors\"):\n        print(f\"GraphQL errors: {response['errors']}\", file=sys.stderr)\n        return None\n\n    repository = response.get(\"data\", {}).get(\"repository\", {})\n    result = {}\n    for index, number in enumerate(pr_numbers):\n        pull_request = repository.get(f\"pr{index}\")\n        if pull_request:\n            result[number] = pull_request\n    return result\n\n\ndef normalize_release_note(content):\n    \"\"\"Normalize a candidate release note and reject standard empty values.\"\"\"\n    normalized = re.sub(r\"\\s+\", \" \", content.strip())\n    if normalized.lower() in NO_RELEASE_NOTE_VALUES or len(normalized) <= 3:\n        return None\n    return normalized\n\n\ndef extract_user_facing_change(pr_body):\n    \"\"\"Extract a user-facing change from supported PR template formats.\"\"\"\n    if not pr_body:\n        return None\n\n    release_note_patterns = [\n        r\"```release-note[^\\r\\n]*[\\r\\n]+(.*?)^[ \\t]*```[ \\t]*$\",\n        r\"```release-note\\s*[\\r\\n]+([^\\r\\n]+(?:[\\r\\n]+[^\\r\\n`]+)*)\",\n    ]\n    for pattern in release_note_patterns:\n        match = re.search(pattern, pr_body, re.MULTILINE | re.DOTALL)\n        if match:\n            return normalize_release_note(match.group(1))\n\n    fallback = re.search(\n        r\"\\*\\*Does this PR introduce a user-facing change\\?\\*\\*:\\s*\"\n        r\"```[^\\r\\n]*[\\r\\n]+([\\s\\S]*?)```\",\n        pr_body,\n        re.IGNORECASE,\n    )\n    if fallback:\n        return normalize_release_note(fallback.group(1))\n    return None\n\n\ndef extract_pr_kind_from_labels(labels):\n    \"\"\"Extract PR kinds from kind-prefixed labels.\"\"\"\n    kinds = []\n    for label in (labels or {}).get(\"nodes\", []):\n        name = (label or {}).get(\"name\", \"\")\n        if name.startswith(\"kind/\") and len(name) > len(\"kind/\"):\n            kind = name[len(\"kind/\") :]\n            if kind not in kinds:\n                kinds.append(kind)\n    return kinds or None\n\n\ndef extract_pr_kind_from_body(pr_body):\n    \"\"\"Extract PR kinds from the Karmada pull request template body.\"\"\"\n    if not pr_body:\n        return None\n\n    section = re.search(\n        r\"\\*\\*What type of PR is this\\?\\*\\*(.*?)\"\n        r\"\\*\\*What this PR does / why we need it\\*\\*:\",\n        pr_body,\n        re.DOTALL | re.IGNORECASE,\n    )\n    if not section:\n        return None\n\n    without_comments = re.sub(r\"<!--.*?-->\", \"\", section.group(1), flags=re.DOTALL)\n    kinds = re.findall(\n        r\"^\\s*/kind\\s+([a-zA-Z0-9-]+)\\s*$\", without_comments, re.MULTILINE\n    )\n    return kinds or None\n\n\ndef extract_pr_kind(labels, pr_body):\n    \"\"\"Extract PR kinds from labels, falling back to the PR body.\"\"\"\n    return extract_pr_kind_from_labels(labels) or extract_pr_kind_from_body(pr_body)\n\n\ndef extract_pr_number(commit_message):\n    \"\"\"Extract a PR number from a GitHub merge or squash commit title.\"\"\"\n    title = commit_message.splitlines()[0] if commit_message else \"\"\n    merge_match = re.match(r\"Merge pull request #(\\d+)\\b\", title)\n    if merge_match:\n        return int(merge_match.group(1))\n\n    squash_match = re.search(r\"\\(#(\\d+)\\)$\", title)\n    if squash_match:\n        return int(squash_match.group(1))\n    return None\n\n\ndef comparison_is_truncated(comparison):\n    \"\"\"Report whether the compare response omitted commits from its list.\"\"\"\n    commits = comparison.get(\"commits\", [])\n    return comparison.get(\"total_commits\", len(commits)) > len(commits)\n\n\ndef parse_arguments():\n    \"\"\"Parse command-line arguments.\"\"\"\n    parser = argparse.ArgumentParser(\n        description=\"Fetch merged PR information for release note generation.\"\n    )\n    parser.add_argument(\"base_tag\", help=\"Base tag or branch to compare from\")\n    parser.add_argument(\"head_branch\", help=\"Head tag or branch to compare to\")\n    parser.add_argument(\n        \"--repo\",\n        required=True,\n        help=\"GitHub repository in owner/name format\",\n    )\n    return parser.parse_args()\n\n\ndef main():\n    \"\"\"Fetch and print PRs that contain user-facing changes.\"\"\"\n    args = parse_arguments()\n    if args.repo.count(\"/\") != 1:\n        print(\"Error: --repo must use owner/name format\", file=sys.stderr)\n        return 2\n\n    token = os.getenv(\"GITHUB_TOKEN\")\n    if not token:\n        print(\"Error: GITHUB_TOKEN is not set\", file=sys.stderr)\n        return 2\n\n    repo_owner, repo_name = args.repo.split(\"/\", 1)\n    print(f\"Fetching PR information for {args.repo}...\")\n    print(f\"Comparing {args.base_tag}...{args.head_branch}\")\n    print(\"=\" * 80)\n\n    comparison = get_commit_comparison(\n        repo_owner, repo_name, args.base_tag, args.head_branch, token\n    )\n    if not comparison:\n        print(\"Failed to get commit comparison\", file=sys.stderr)\n        return 1\n\n    commits = comparison.get(\"commits\", [])\n    if comparison_is_truncated(comparison):\n        print(\n            \"Error: GitHub returned only \"\n            f\"{len(commits)} of {comparison['total_commits']} commits; \"\n            \"refusing to generate incomplete release notes\",\n            file=sys.stderr,\n        )\n        return 1\n\n    pr_numbers = set()\n    for commit in commits:\n        message = commit.get(\"commit\", {}).get(\"message\", \"\")\n        pr_number = extract_pr_number(message)\n        if pr_number is not None:\n            pr_numbers.add(pr_number)\n\n    sorted_pr_numbers = sorted(pr_numbers)\n    print(f\"Found {len(commits)} commits\")\n    print(f\"Found {len(sorted_pr_numbers)} merged PRs\")\n    print(\"=\" * 80)\n\n    pr_details = get_pr_details_batch(\n        repo_owner, repo_name, sorted_pr_numbers, token\n    )\n    if pr_details is None:\n        print(\"Failed to get pull request details\", file=sys.stderr)\n        return 1\n\n    release_notes = []\n    for pr_number in sorted_pr_numbers:\n        pull_request = pr_details.get(pr_number)\n        if not pull_request:\n            continue\n        change = extract_user_facing_change(pull_request.get(\"body\") or \"\")\n        if not change:\n            continue\n        author = pull_request.get(\"author\") or {}\n        release_notes.append(\n            {\n                \"number\": pr_number,\n                \"title\": pull_request.get(\"title\", \"\"),\n                \"author\": author.get(\"login\", \"unknown\"),\n                \"kind\": extract_pr_kind(\n                    pull_request.get(\"labels\"), pull_request.get(\"body\") or \"\"\n                ),\n                \"change\": change,\n            }\n        )\n\n    print(\"\\n\" + \"=\" * 80)\n    print(\"SUMMARY OF PRS WITH USER-FACING CHANGES\")\n    print(\"=\" * 80)\n    if release_notes:\n        for release_note in release_notes:\n            print(f\"\\nPR #{release_note['number']} by @{release_note['author']}\")\n            print(f\"Title: {release_note['title']}\")\n            print(f\"Kind: {release_note['kind']}\")\n            print(f\"Change: {release_note['change']}\")\n    else:\n        print(\"No PRs with user-facing changes found.\")\n    print(f\"\\nTotal PRs with user-facing changes: {len(release_notes)}\")\n    return 0\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n",".claude/skills/generating-release-notes/references/karmada-release-note-format.md":"# Karmada Release Note Format\n\n## Repository Match\n\n- Git repository root directory name: `karmada`\n\nSelect this format whenever the Git repository root directory is named `karmada`.\n\n## Upstream Data Source\n\n- GitHub owner: `karmada-io`\n- GitHub repository: `karmada`\n- Script `--repo` value: `karmada-io/karmada`\n\nAlways collect comparison and pull request data from `karmada-io/karmada`, regardless of the local fork owner. Do not derive the data source owner from the local `origin` URL.\n\n## Version Resolution\n\nClassify versions as follows:\n\n- Patch: `vX.Y.Z` where `Z > 0`. The base is `vX.Y.(Z-1)`, and the head is `release-X.Y`.\n- Minor: `vX.Y.0`. Use `vX.Y.0-alpha.0` as the base and `master` as the head.\n- Preview: `vX.Y.0-alpha.1`, `vX.Y.0-alpha.2`, `vX.Y.0-beta.0`, or `vX.Y.0-rc.0`. Use the previous preview tag and `master` as the head.\n\nThe expected preview sequence is:\n\n1. `vX.Y.0-alpha.1`, based on `vX.Y.0-alpha.0`\n2. `vX.Y.0-alpha.2`, based on `vX.Y.0-alpha.1`\n3. `vX.Y.0-beta.0`, based on `vX.Y.0-alpha.2`\n4. `vX.Y.0-rc.0`, based on `vX.Y.0-beta.0`\n\nConfirm unusual or skipped preview sequences with the user.\n\n## Output Location\n\nUpdate `docs/CHANGELOG/CHANGELOG-X.Y.md`. This file contains the minor, preview, and all patch release notes for release line `X.Y`.\n\nOrder patch sections by version descending. Preserve the established placement of minor and preview sections in the existing file.\n\n## Document Structure\n\n### Patch Release\n\n```markdown\n# vX.Y.Z\n## Downloads for vX.Y.Z\n## Changelog since vX.Y.(Z-1)\n### Changes by Kind\n#### Bug Fixes\n#### Others\n```\n\nPlace fixes under `Bug Fixes`. Place other changes, including dependency upgrades, under `Others`. Preserve the exact capitalization already used by the target changelog when it differs from this schematic.\n\n### Minor Release\n\n```markdown\n# vX.Y.0\n## Downloads for vX.Y.0\n## Urgent Update Notes\n## What's New\n## Other Notable Changes\n### API Changes\n### Features & Enhancements\n### Deprecation\n### Bug Fixes\n### Security\n## Other\n### Dependencies\n### Helm Charts\n### Instrumentation\n### Performance\n## Contributors\n\nThank you to everyone who contributed to this release!\n\nUsers whose commits are in this release (alphabetically by username)\n\n- @xxxA\n- @xxxB\n```\n\n`Urgent Update Notes` and `What's New` remain empty and are reserved for manual completion. Build `Contributors` from the commit authors collected with the same comparison range used for pull requests, excluding accounts whose usernames end in `[bot]`, such as `dependabot[bot]` and `github-actions[bot]`.\n\n### Preview Release\n\n```markdown\n# vX.Y.0-<preview>\n## Downloads for vX.Y.0-<preview>\n## Changelog since <previous-preview-version>\n## Urgent Update Notes\n## Changes by Kind\n### API Changes\n### Features & Enhancements\n### Deprecation\n### Bug Fixes\n### Security\n## Other\n### Dependencies\n### Helm Charts\n### Instrumentation\n### Performance\n```\n\n## Entry Format and Classification\n\nEvery entry uses this syntax:\n\n```markdown\n- `<component>`: <change> ([#<PR>](https://github.com/karmada-io/karmada/pull/<PR>), @<author>)\n```\n\nRules:\n\n- Mark component names with backticks, for example `karmada-controller-manager`.\n- Keep entries for the same component together within a category.\n- If one PR contains multiple user-facing changes, write one entry for each change and retain the same PR attribution.\n- Use the PR's kind as the primary classification signal. For example, `feature` maps to `Features & Enhancements` and `bug` maps to `Bug Fixes`.\n- Only use the title, description, and user-facing change as the primary signal when the kind is empty.\n- Specialized categories may override the kind-based category when the user-facing change clearly belongs to them. Classify API changes in `API Changes`, dependency version changes in `Dependencies`, Helm packaging changes in `Helm Charts`, metrics or observability changes in `Instrumentation`, performance work in `Performance`, deprecations in `Deprecation`, and security corrections in `Security`.\n- Outside those specialized categories, wording such as \"fix\" or \"feature\" in the title, description, or user-facing change must not override an available kind.\n- When one PR contains changes belonging to different categories, classify each split entry independently.\n\n## Language and Style\n\n- Write all release-note content in English.\n- Match the leading verb to the final category, even when the original user-facing change uses different framing.\n- `Features & Enhancements` entries describe the capability that was introduced or enabled and normally begin with verbs such as \"Added,\" \"Introduced,\" \"Enabled,\" or \"Supported.\" Do not leave a feature entry framed as \"Fixed\" merely because the original PR description used that word.\n- `Bug Fixes` entries describe the corrected defect and normally begin with \"Fixed\" or \"Corrected.\"\n- `API Changes` entries describe the API operation directly, using verbs such as \"Added,\" \"Introduced,\" \"Updated,\" \"Removed,\" or \"Deprecated\" as appropriate.\n- Deprecation entries use present perfect tense, for example, \"has been deprecated.\"\n- Dependency entries use present perfect or simple past tense, for example, \"has been upgraded to\" or \"Upgraded to.\"\n- Features, fixes, and all other categories use simple past tense.\n- Present-tense constructions such as \"now supports\" or \"no longer relies on\" are allowed only for a newly introduced capability or behavioral change.\n- Correct spelling and grammar while preserving technical meaning and exact identifiers.\n\n## Validation\n\n1. Confirm category, component grouping, tense, PR URL, and author syntax for every entry.\n2. For minor releases, confirm contributors exclude usernames ending in `[bot]`, are unique, and are alphabetically sorted.\n3. Run `doctoc docs/CHANGELOG/CHANGELOG-X.Y.md` after the release section is complete.\n4. The final changelog should have correct release ordering, an updated TOC, and no unrelated changes.\n",".claude/skills/generating-release-notes/scripts/fetch_contributors.sh":"#!/usr/bin/env bash\n# Copyright 2026 The Karmada Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nset -o errexit\nset -o nounset\nset -o pipefail\n\nfunction usage() {\n  echo \"Usage: $0 <owner> <repository> <base-ref> <head-ref>\" >&2\n}\n\nif [[ $# -ne 4 ]]; then\n  usage\n  exit 2\nfi\n\nif [[ -z \"${GITHUB_TOKEN:-}\" ]]; then\n  echo \"Error: GITHUB_TOKEN is not set\" >&2\n  exit 2\nfi\n\nfor command in curl jq; do\n  if ! command -v \"${command}\" > /dev/null 2>&1; then\n    echo \"Error: required command '${command}' was not found\" >&2\n    exit 2\n  fi\ndone\n\nrepo_owner=$1\nrepo_name=$2\nbase_ref=$3\nhead_ref=$4\nencoded_base_ref=$(printf '%s' \"${base_ref}\" | jq -sRr @uri)\nencoded_head_ref=$(printf '%s' \"${head_ref}\" | jq -sRr @uri)\n\ntemp_dir=$(mktemp -d)\ntrap 'rm -rf \"${temp_dir}\"' EXIT\nauthors_file=\"${temp_dir}/authors\"\ntouch \"${authors_file}\"\nunresolved_commits_file=\"${temp_dir}/unresolved-commits\"\ntouch \"${unresolved_commits_file}\"\n\npage=1\nfetched_commits=0\ntotal_commits=1\n\nwhile (( fetched_commits < total_commits )); do\n  url=\"https://api.github.com/repos/${repo_owner}/${repo_name}/compare/${encoded_base_ref}...${encoded_head_ref}?per_page=100&page=${page}\"\n  response_file=\"${temp_dir}/response-${page}.json\"\n\n  curl --fail --silent --show-error --location \\\n    -H \"Accept: application/vnd.github+json\" \\\n    -H \"Authorization: Bearer ${GITHUB_TOKEN}\" \\\n    -H \"X-GitHub-Api-Version: 2022-11-28\" \\\n    \"${url}\" > \"${response_file}\"\n\n  total_commits=$(jq -er '.total_commits | numbers' \"${response_file}\")\n  page_commits=$(jq -er '.commits | length' \"${response_file}\")\n  jq -r '.commits[].author.login // empty' \"${response_file}\" >> \"${authors_file}\"\n  jq -r '\n    .commits[]\n    | select(.author.login == null)\n    | (.commit.message | split(\"\\n\")[0]) as $title\n    | [\n        .sha,\n        if ($title | test(\"^Merge pull request #[0-9]+\\\\b\")) then\n          ($title | capture(\"^Merge pull request #(?<number>[0-9]+)\\\\b\").number)\n        elif ($title | test(\"\\\\(#[0-9]+\\\\)$\")) then\n          ($title | capture(\"\\\\(#(?<number>[0-9]+)\\\\)$\").number)\n        else\n          \"\"\n        end\n      ]\n    | @tsv\n  ' \\\n    \"${response_file}\" >> \"${unresolved_commits_file}\"\n\n  if (( page_commits == 0 )); then\n    break\n  fi\n\n  fetched_commits=$((fetched_commits + page_commits))\n  page=$((page + 1))\ndone\n\nfailed_commits_file=\"${temp_dir}/failed-commits\"\ntouch \"${failed_commits_file}\"\nwhile IFS=$'\\t' read -r commit_sha pr_number; do\n  [[ -n \"${commit_sha}\" ]] || continue\n  if [[ -z \"${pr_number}\" ]]; then\n    printf '%s\\n' \"${commit_sha}\" >> \"${failed_commits_file}\"\n    continue\n  fi\n  url=\"https://api.github.com/repos/${repo_owner}/${repo_name}/pulls/${pr_number}\"\n  response_file=\"${temp_dir}/pull-${pr_number}.json\"\n\n  if ! curl --fail --silent --show-error --location \\\n    -H \"Accept: application/vnd.github+json\" \\\n    -H \"Authorization: Bearer ${GITHUB_TOKEN}\" \\\n    -H \"X-GitHub-Api-Version: 2022-11-28\" \\\n    \"${url}\" > \"${response_file}\"; then\n    echo \"Error: failed to fetch PR #${pr_number} for commit ${commit_sha}\" >&2\n    exit 1\n  fi\n\n  if login=$(jq -er --arg sha \"${commit_sha}\" '\n    if .merged_at != null\n      and ((.head.sha // \"\") == $sha or (.merge_commit_sha // \"\") == $sha)\n      and (.user.login // \"\") != \"\"\n      then .user.login\n      else empty\n    end\n  ' \"${response_file}\"); then\n    printf '%s\\n' \"${login}\" >> \"${authors_file}\"\n  else\n    printf '%s\\n' \"${commit_sha}\" >> \"${failed_commits_file}\"\n  fi\ndone < \"${unresolved_commits_file}\"\n\nif [[ -s \"${failed_commits_file}\" ]]; then\n  echo \"Error: unable to resolve contributors for these commits:\" >&2\n  while IFS= read -r commit_sha; do\n    echo \"  ${commit_sha}\" >&2\n  done < \"${failed_commits_file}\"\n  exit 1\nfi\n\nLC_ALL=C sort -fu \"${authors_file}\"\n"},"files":{"vendor/github.com/google/gnostic-models/extensions/README.md":"# Extensions\n\n**Extension Support is experimental.**\n\nThis directory contains support code for building Gnostic extensio handlers and\nassociated examples.\n\nExtension handlers can be used to compile vendor or specification extensions\ninto protocol buffer structures.\n\nLike plugins, extension handlers are built as separate executables. Extension\nbodies are written to extension handlers as serialized\nExtensionHandlerRequests.\n",".claude/skills/e2e-root-cause-analysis/SKILL.md":"---\nname: e2e-root-cause-analysis\ndescription: Find the root cause of failing or flaky Karmada E2E tests from CI runs. Use when a user reports an e2e test failure, a flaky CI job, or asks to find the root cause of an e2e timeout, providing a GitHub Actions run/job URL, a PR number/URL, or logs.\n---\n\n# E2E Root Cause Analysis\n\nFind the root cause of a Karmada E2E failure by working **backwards** from the error you see to the reason behind it. Never stop at the first error message. For each finding, answer: \"what does this error mean, and what caused it?\" Repeat until you reach a root cause that is fully backed by log evidence.\n\n## Step 0: Find the failing job (if only a PR is given)\n\nIf the user gives a PR number or PR URL instead of a job URL, find the failing job first:\n\n```bash\n# List all checks of the PR; failing ones come with their job URLs\ngh pr checks <pr-number> --repo karmada-io/karmada\n\n# If the failure was on an older commit, list runs for that commit\ngh run list --repo karmada-io/karmada --commit <sha> --json databaseId,name,conclusion,url\n```\n\nPick the failing e2e job (e.g. `e2e test (v1.34.0)`) and take its `.../actions/runs/<run_id>/job/<job_id>` URL. If several jobs failed, ask the user which one to look at, or start with the e2e one.\n\n## Step 1: Collect the evidence\n\nGiven a GitHub Actions run/job URL (e.g. `.../actions/runs/<run_id>/job/<job_id>`):\n\n1. Download the job log (the ginkgo output):\n\n   ```bash\n   gh api repos/karmada-io/karmada/actions/jobs/<job_id>/logs > job.log\n   ```\n\n2. List and download the component-log artifact for the failing job's Kubernetes version. This is the most valuable evidence — it holds the logs of every Karmada component and member cluster:\n\n   ```bash\n   gh api repos/karmada-io/karmada/actions/runs/<run_id>/artifacts \\\n     --jq '.artifacts[] | {id, name}'\n   gh run download <run_id> --repo karmada-io/karmada -n karmada_e2e_log_<k8s_version>\n   ```\n\n   Artifact layout:\n   - `karmada-host/karmada-host-control-plane/containers/` — control-plane components: `karmada-scheduler-*`, `karmada-controller-manager-*`, `karmada-webhook-*`, `karmada-aggregated-apiserver-*`, `karmada-scheduler-estimator-<member>-*`, etc. There are usually two pods per component (leader election); grep both, only the active one has the useful lines.\n   - `member3/` — member cluster logs, including `karmada-agent-*` for pull-mode cluster.\n\n## Step 2: Find the failure in the ginkgo log\n\n```bash\ngrep -n \"\\[FAILED\\]\\|Summarizing\" job.log\n```\n\nFor the failing spec, extract:\n\n- The **assertion** that failed (framework helper file:line) and the timeout duration.\n- The **Timeline** section: every `STEP:` with its timestamp. Timestamps are the key — they let you match the test's actions against component logs.\n- The Timeline of the **previous test case** that shares resources (same CRD, namespace, cluster labels...). Flaky tests are often caused by the previous case's cleanup, which runs in the background and races with the current case's setup.\n\nWarning signs to look for in the timeline:\n\n- A readiness wait (a `framework.Wait*` helper) that passes much faster than normal (milliseconds instead of seconds). This usually means the check read **stale state** left over from the previous case.\n- Cleanup steps of the previous case that only wait for the control plane, while changes on the member clusters are still in flight.\n\n## Step 3: Trace backwards through component logs\n\nPresent the analysis in reverse order — from the error you see down to the root cause. At each level, state: *what was seen → what it means → what caused it*. A typical chain:\n\n1. **The test error.** e.g. a timeout while waiting for some object to reach the expected state. Meaning: what the test expected never happened. Question: why?\n\n2. **The decision of the component that owns the object.** Grep that component's log for the object name, e.g.:\n\n   ```bash\n   grep -h \"<object-name>\" karmada-<component>-*.log\n   ```\n\n   Depending on the failure, the owning component may be the scheduler (binding never scheduled, `Warning` events explaining why), the controller-manager (work not dispatched, resource not synced to members), the webhook (request rejected), or the agent (pull-mode cluster out of sync). Count how many times the object shows up: if it appears once and never again, the component did **not retry** — see the note below.\n\n3. **Why the component made that decision.** The decision is usually based on some state (cluster status, cache, quota, ...). Grep the logs of the controller that writes that state around the failure window, and build a timeline of those writes with exact timestamps. Compare it with the test's Timeline from Step 2: look for a window in which the state the component read was stale or briefly wrong.\n\n4. **The root cause.** Often a race between test cases: the previous case's cleanup returned after waiting only for the control plane, while changes on the member clusters and status collection were still in flight. State it with the exact timestamps and log lines that prove the window.\n\nIf the failing object was expected to recover but never did (e.g. it appears only once in the owning component's log), do not assume the runtime retries on its own — check how retry/requeue actually works in the component's code before claiming \"it should self-heal\".\n\n## Step 4: Report\n\nWrite the conclusion as an evidence chain, from the error backwards:\n\n1. The error seen (assertion, timeout, file:line).\n2. What it means (which expected state was never reached).\n3. Direct cause with log lines + timestamps (the owning component's decision).\n4. Deeper cause (the stale or briefly wrong state, race window with timestamps).\n5. Root cause (e.g. cleanup returned too early and did not wait for member clusters to catch up).\n6. Why it did not self-heal, if relevant (checked against component code).\n\nQuote exact log lines with timestamps and file names. Check every claim about runtime behavior against the code in the repository (e.g. `pkg/scheduler/`, `pkg/controllers/`) before stating it. Clean up downloaded logs afterwards.\n\n## Step 5: Improve this skill\n\nAfter the diagnosis is done, review what you learned. If you found something that would help future diagnoses — a new failure pattern, a better command, a log location not listed here, or a wrong assumption in this document — propose an update to this skill file:\n\n- Keep the addition **generic**: describe the pattern, not the specific test, object names, or timestamps of this one case.\n- Show the user the proposed change and let them decide whether to apply it.\n- Do not commit the change yourself.\n",".claude/skills/generating-release-notes/SKILL.md":"---\nname: generating-release-notes\ndescription: \"Use when preparing changelogs or release notes from merged pull requests for a patch, minor, or preview release.\"\nargument-hint: \"Target release version, for example v1.14.5 or v1.15.0-rc.0\"\n---\n\n# Generating Release Notes\n\n## Overview\n\nGenerate release notes from merged pull requests. Complete each gate in order. Do not start writing until the target version, comparison range, and format plugin are known.\n\n## Inputs\n\nA target version is required. If it is missing or unclear, ask for it and stop. Do not guess \"the next release.\"\n\nWork out these values only after the target is known:\n\n- Release type: patch, minor, or preview.\n- Previous version or preview tag.\n- Comparison branch.\n- Output changelog path.\n\nThe selected format plugin defines how to work out these values for its repository.\n\n## Select a Format Plugin\n\n1. Determine the Git repository root directory name with `basename \"$(git rev-parse --show-toplevel)\"`.\n2. Look at the Markdown files under [references](./references/). Select a plugin whose `Repository Match` includes that directory name.\n3. If exactly one plugin matches, load it and follow it exactly.\n4. If multiple plugins match, ask the user to choose.\n5. If no plugin matches, stop and report the directory name. Ask for a new repository format plugin. Never silently use another repository's format.\n\nYou can add a format plugin for a new repository. Add `<repository>-release-note-format.md` under `references/` with these sections:\n\n- `Repository Match`\n- `Upstream Data Source`\n- `Version Resolution`\n- `Output Location`\n- `Document Structure`\n- `Entry Format and Classification`\n- `Language and Style`\n- `Validation`\n\n## Required Workflow\n\n### Gate 1: Resolve the Release Range\n\n1. Classify the target as patch, minor, or preview using the selected plugin.\n2. Resolve the previous tag and head branch using the plugin's rules.\n3. Check the target, previous tag, branch, and output path against what the user asked for.\n4. Stop if anything is unclear. Do not switch to a different tag or branch without confirmation.\n\nDo not continue until Gate 1 passes.\n\n### Gate 2: Collect Merged Pull Requests\n\n1. Check that `python3` and the included [fetch_pr_info.py](./scripts/fetch_pr_info.py) exist.\n2. Check that `GITHUB_TOKEN` is set, but do not check its scopes. If it is not set but `gh auth status` reports a logged-in account, reuse that credential with `export GITHUB_TOKEN=$(gh auth token)` in the same shell as the scripts. Otherwise, ask the user to set a token with the `public_repo` scope in their environment. Never ask the user to send a token through chat, and never print the token value.\n3. From the repository root, run the included script with the base tag and head branch from Gate 1 and the `owner/repository` value listed under the plugin's `Upstream Data Source`:\n\n\t```bash\n\tpython3 .claude/skills/generating-release-notes/scripts/fetch_pr_info.py <base-tag> <head-branch> --repo <owner/repository>\n\t```\n\n4. Use only the `SUMMARY OF PRS WITH USER-FACING CHANGES` section. Ignore debug output and PRs without user-facing changes.\n5. Keep the PR number, title, author, kind, and every user-facing change. If a PR has more than one user-facing change, split them into separate entries.\n\nDo not continue if collection fails or credentials are invalid.\n\n### Gate 3: Collect Contributors\n\nRun this gate only when the selected format plugin requires a Contributors section.\n\n1. Reuse the same base tag and head branch from Gate 1. Contributor collection and PR collection must use the same comparison range.\n2. From the repository root, run the included [fetch_contributors.sh](./scripts/fetch_contributors.sh) with the owner and repository listed under `Upstream Data Source`:\n\n\t```bash\n\t.claude/skills/generating-release-notes/scripts/fetch_contributors.sh <owner> <repository> <base-tag> <head-branch>\n\t```\n\n3. The output is the list of GitHub authors whose commits were merged in that comparison range.\n4. Keep unique GitHub handles sorted alphabetically.\n\nDo not continue if contributor collection fails.\n\n### Gate 4: Classify and Draft\n\n1. Use the PR kind returned by the collection script as the main signal for choosing a category.\n2. Apply the plugin's rules for which category wins when more than one applies.\n3. Rewrite each entry in the tense and entry syntax the plugin requires, without changing its technical meaning. Make the leading verb and wording match the final category. If you move an entry to another category, re-read its wording and rewrite it if needed.\n4. Group entries as required by the plugin.\n5. Build the complete release section using the structure for the target release type.\n6. Add the commit authors collected in Gate 3 when the release requires a Contributors section.\n\nDo not make up user-facing changes, and do not guess at behavior from a PR title alone.\n\n### Gate 5: Update the Changelog\n\n1. Read the existing changelog before editing.\n2. Insert the new release in the order the plugin requires.\n3. Keep existing release sections and unrelated user changes as they are.\n4. Run the plugin's TOC or formatting command after content is final.\n\n### Gate 6: Validate\n\nCheck the requirements that apply to every repository:\n\n- No user-facing change is duplicated or missing.\n- Existing changelog content and unrelated user changes are unchanged.\n\nRun every check in the selected plugin's `Validation` section. Fix all failures before finishing.\n\n### Gate 7: Improve this skill\n\nAfter the release notes are done, think about what you learned. If you found something that would help future runs — a new failure pattern, a better command, or a wrong assumption in this document — propose an update to this skill file:\n\n- Show the user the proposed change and let them decide whether to apply it.\n- Do not commit the change yourself.\n\n## Common Failures\n\n| Failure | Required response |\n|---|---|\n| Target version is missing | Ask for it; do not guess. |\n| No format plugin matches | Stop and request a plugin. |\n| PR or contributor collection fails | Report the error and stop; do not make up or replace data. |\n",".claude/skills/generating-release-notes/scripts/fetch_pr_info.py":"#!/usr/bin/env python3\n# Copyright 2026 The Karmada Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"Fetch merged PR metadata and user-facing changes for release notes.\"\"\"\n\nimport argparse\nimport json\nimport os\nimport re\nimport sys\nimport urllib.error\nimport urllib.request\n\n\nGITHUB_API_BASE = \"https://api.github.com\"\nNO_RELEASE_NOTE_VALUES = {\n    \"\",\n    \"n/a\",\n    \"na\",\n    \"no\",\n    \"none\",\n    \"noop\",\n    \"nope\",\n    \"nothing\",\n}\n\n\ndef github_request(url, token, payload=None):\n    \"\"\"Send an authenticated GitHub API request and decode its JSON body.\"\"\"\n    headers = {\n        \"Accept\": \"application/vnd.github.v3+json\",\n        \"User-Agent\": \"Release-Note-Generator\",\n    }\n    if token:\n        headers[\"Authorization\"] = f\"Bearer {token}\"\n\n    data = None\n    if payload is not None:\n        headers[\"Content-Type\"] = \"application/json\"\n        data = json.dumps(payload).encode(\"utf-8\")\n\n    request = urllib.request.Request(url, data=data, headers=headers)\n    try:\n        with urllib.request.urlopen(request) as response:\n            return json.load(response)\n    except urllib.error.HTTPError as error:\n        detail = error.read().decode(\"utf-8\", errors=\"replace\")\n        print(f\"GitHub request failed ({error.code}): {detail}\", file=sys.stderr)\n    except urllib.error.URLError as error:\n        print(f\"GitHub request failed: {error.reason}\", file=sys.stderr)\n    return None\n\n\ndef get_commit_comparison(repo_owner, repo_name, base_tag, head_branch, token):\n    \"\"\"Get the GitHub comparison between the base tag and head branch.\"\"\"\n    base_url = (\n        f\"{GITHUB_API_BASE}/repos/{repo_owner}/{repo_name}/compare/\"\n        f\"{base_tag}...{head_branch}\"\n    )\n    comparison = None\n    commits = []\n    page = 1\n\n    while True:\n        response = github_request(f\"{base_url}?per_page=100&page={page}\", token)\n        if not response:\n            break\n        if comparison is None:\n            comparison = response.copy()\n\n        page_commits = response.get(\"commits\", [])\n        commits.extend(page_commits)\n        total_commits = comparison.get(\"total_commits\", len(commits))\n        if len(commits) >= total_commits or len(page_commits) < 100:\n            break\n        page += 1\n\n    if comparison is not None:\n        comparison[\"commits\"] = commits\n    return comparison\n\n\ndef get_pr_details_batch(repo_owner, repo_name, pr_numbers, token):\n    \"\"\"Fetch pull request title, body, and author in one GraphQL request.\"\"\"\n    if not pr_numbers:\n        return {}\n\n    query_parts = []\n    for index, number in enumerate(pr_numbers):\n        query_parts.append(\n            f\"pr{index}: pullRequest(number: {number}) {{ \"\n            \"number title body author { login } \"\n            \"labels(first: 100) { nodes { name } } }\"\n        )\n    query = (\n        \"{ repository(owner: \\\"\"\n        + repo_owner\n        + \"\\\", name: \\\"\"\n        + repo_name\n        + \"\\\") { \"\n        + \" \".join(query_parts)\n        + \" } }\"\n    )\n    response = github_request(\n        \"https://api.github.com/graphql\", token, {\"query\": query}\n    )\n    if not response:\n        return None\n    if response.get(\"errors\"):\n        print(f\"GraphQL errors: {response['errors']}\", file=sys.stderr)\n        return None\n\n    repository = response.get(\"data\", {}).get(\"repository\", {})\n    result = {}\n    for index, number in enumerate(pr_numbers):\n        pull_request = repository.get(f\"pr{index}\")\n        if pull_request:\n            result[number] = pull_request\n    return result\n\n\ndef normalize_release_note(content):\n    \"\"\"Normalize a candidate release note and reject standard empty values.\"\"\"\n    normalized = re.sub(r\"\\s+\", \" \", content.strip())\n    if normalized.lower() in NO_RELEASE_NOTE_VALUES or len(normalized) <= 3:\n        return None\n    return normalized\n\n\ndef extract_user_facing_change(pr_body):\n    \"\"\"Extract a user-facing change from supported PR template formats.\"\"\"\n    if not pr_body:\n        return None\n\n    release_note_patterns = [\n        r\"```release-note[^\\r\\n]*[\\r\\n]+(.*?)^[ \\t]*```[ \\t]*$\",\n        r\"```release-note\\s*[\\r\\n]+([^\\r\\n]+(?:[\\r\\n]+[^\\r\\n`]+)*)\",\n    ]\n    for pattern in release_note_patterns:\n        match = re.search(pattern, pr_body, re.MULTILINE | re.DOTALL)\n        if match:\n            return normalize_release_note(match.group(1))\n\n    fallback = re.search(\n        r\"\\*\\*Does this PR introduce a user-facing change\\?\\*\\*:\\s*\"\n        r\"```[^\\r\\n]*[\\r\\n]+([\\s\\S]*?)```\",\n        pr_body,\n        re.IGNORECASE,\n    )\n    if fallback:\n        return normalize_release_note(fallback.group(1))\n    return None\n\n\ndef extract_pr_kind_from_labels(labels):\n    \"\"\"Extract PR kinds from kind-prefixed labels.\"\"\"\n    kinds = []\n    for label in (labels or {}).get(\"nodes\", []):\n        name = (label or {}).get(\"name\", \"\")\n        if name.startswith(\"kind/\") and len(name) > len(\"kind/\"):\n            kind = name[len(\"kind/\") :]\n            if kind not in kinds:\n                kinds.append(kind)\n    return kinds or None\n\n\ndef extract_pr_kind_from_body(pr_body):\n    \"\"\"Extract PR kinds from the Karmada pull request template body.\"\"\"\n    if not pr_body:\n        return None\n\n    section = re.search(\n        r\"\\*\\*What type of PR is this\\?\\*\\*(.*?)\"\n        r\"\\*\\*What this PR does / why we need it\\*\\*:\",\n        pr_body,\n        re.DOTALL | re.IGNORECASE,\n    )\n    if not section:\n        return None\n\n    without_comments = re.sub(r\"<!--.*?-->\", \"\", section.group(1), flags=re.DOTALL)\n    kinds = re.findall(\n        r\"^\\s*/kind\\s+([a-zA-Z0-9-]+)\\s*$\", without_comments, re.MULTILINE\n    )\n    return kinds or None\n\n\ndef extract_pr_kind(labels, pr_body):\n    \"\"\"Extract PR kinds from labels, falling back to the PR body.\"\"\"\n    return extract_pr_kind_from_labels(labels) or extract_pr_kind_from_body(pr_body)\n\n\ndef extract_pr_number(commit_message):\n    \"\"\"Extract a PR number from a GitHub merge or squash commit title.\"\"\"\n    title = commit_message.splitlines()[0] if commit_message else \"\"\n    merge_match = re.match(r\"Merge pull request #(\\d+)\\b\", title)\n    if merge_match:\n        return int(merge_match.group(1))\n\n    squash_match = re.search(r\"\\(#(\\d+)\\)$\", title)\n    if squash_match:\n        return int(squash_match.group(1))\n    return None\n\n\ndef comparison_is_truncated(comparison):\n    \"\"\"Report whether the compare response omitted commits from its list.\"\"\"\n    commits = comparison.get(\"commits\", [])\n    return comparison.get(\"total_commits\", len(commits)) > len(commits)\n\n\ndef parse_arguments():\n    \"\"\"Parse command-line arguments.\"\"\"\n    parser = argparse.ArgumentParser(\n        description=\"Fetch merged PR information for release note generation.\"\n    )\n    parser.add_argument(\"base_tag\", help=\"Base tag or branch to compare from\")\n    parser.add_argument(\"head_branch\", help=\"Head tag or branch to compare to\")\n    parser.add_argument(\n        \"--repo\",\n        required=True,\n        help=\"GitHub repository in owner/name format\",\n    )\n    return parser.parse_args()\n\n\ndef main():\n    \"\"\"Fetch and print PRs that contain user-facing changes.\"\"\"\n    args = parse_arguments()\n    if args.repo.count(\"/\") != 1:\n        print(\"Error: --repo must use owner/name format\", file=sys.stderr)\n        return 2\n\n    token = os.getenv(\"GITHUB_TOKEN\")\n    if not token:\n        print(\"Error: GITHUB_TOKEN is not set\", file=sys.stderr)\n        return 2\n\n    repo_owner, repo_name = args.repo.split(\"/\", 1)\n    print(f\"Fetching PR information for {args.repo}...\")\n    print(f\"Comparing {args.base_tag}...{args.head_branch}\")\n    print(\"=\" * 80)\n\n    comparison = get_commit_comparison(\n        repo_owner, repo_name, args.base_tag, args.head_branch, token\n    )\n    if not comparison:\n        print(\"Failed to get commit comparison\", file=sys.stderr)\n        return 1\n\n    commits = comparison.get(\"commits\", [])\n    if comparison_is_truncated(comparison):\n        print(\n            \"Error: GitHub returned only \"\n            f\"{len(commits)} of {comparison['total_commits']} commits; \"\n            \"refusing to generate incomplete release notes\",\n            file=sys.stderr,\n        )\n        return 1\n\n    pr_numbers = set()\n    for commit in commits:\n        message = commit.get(\"commit\", {}).get(\"message\", \"\")\n        pr_number = extract_pr_number(message)\n        if pr_number is not None:\n            pr_numbers.add(pr_number)\n\n    sorted_pr_numbers = sorted(pr_numbers)\n    print(f\"Found {len(commits)} commits\")\n    print(f\"Found {len(sorted_pr_numbers)} merged PRs\")\n    print(\"=\" * 80)\n\n    pr_details = get_pr_details_batch(\n        repo_owner, repo_name, sorted_pr_numbers, token\n    )\n    if pr_details is None:\n        print(\"Failed to get pull request details\", file=sys.stderr)\n        return 1\n\n    release_notes = []\n    for pr_number in sorted_pr_numbers:\n        pull_request = pr_details.get(pr_number)\n        if not pull_request:\n            continue\n        change = extract_user_facing_change(pull_request.get(\"body\") or \"\")\n        if not change:\n            continue\n        author = pull_request.get(\"author\") or {}\n        release_notes.append(\n            {\n                \"number\": pr_number,\n                \"title\": pull_request.get(\"title\", \"\"),\n                \"author\": author.get(\"login\", \"unknown\"),\n                \"kind\": extract_pr_kind(\n                    pull_request.get(\"labels\"), pull_request.get(\"body\") or \"\"\n                ),\n                \"change\": change,\n            }\n        )\n\n    print(\"\\n\" + \"=\" * 80)\n    print(\"SUMMARY OF PRS WITH USER-FACING CHANGES\")\n    print(\"=\" * 80)\n    if release_notes:\n        for release_note in release_notes:\n            print(f\"\\nPR #{release_note['number']} by @{release_note['author']}\")\n            print(f\"Title: {release_note['title']}\")\n            print(f\"Kind: {release_note['kind']}\")\n            print(f\"Change: {release_note['change']}\")\n    else:\n        print(\"No PRs with user-facing changes found.\")\n    print(f\"\\nTotal PRs with user-facing changes: {len(release_notes)}\")\n    return 0\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n",".claude/skills/generating-release-notes/references/karmada-release-note-format.md":"# Karmada Release Note Format\n\n## Repository Match\n\n- Git repository root directory name: `karmada`\n\nSelect this format whenever the Git repository root directory is named `karmada`.\n\n## Upstream Data Source\n\n- GitHub owner: `karmada-io`\n- GitHub repository: `karmada`\n- Script `--repo` value: `karmada-io/karmada`\n\nAlways collect comparison and pull request data from `karmada-io/karmada`, regardless of the local fork owner. Do not derive the data source owner from the local `origin` URL.\n\n## Version Resolution\n\nClassify versions as follows:\n\n- Patch: `vX.Y.Z` where `Z > 0`. The base is `vX.Y.(Z-1)`, and the head is `release-X.Y`.\n- Minor: `vX.Y.0`. Use `vX.Y.0-alpha.0` as the base and `master` as the head.\n- Preview: `vX.Y.0-alpha.1`, `vX.Y.0-alpha.2`, `vX.Y.0-beta.0`, or `vX.Y.0-rc.0`. Use the previous preview tag and `master` as the head.\n\nThe expected preview sequence is:\n\n1. `vX.Y.0-alpha.1`, based on `vX.Y.0-alpha.0`\n2. `vX.Y.0-alpha.2`, based on `vX.Y.0-alpha.1`\n3. `vX.Y.0-beta.0`, based on `vX.Y.0-alpha.2`\n4. `vX.Y.0-rc.0`, based on `vX.Y.0-beta.0`\n\nConfirm unusual or skipped preview sequences with the user.\n\n## Output Location\n\nUpdate `docs/CHANGELOG/CHANGELOG-X.Y.md`. This file contains the minor, preview, and all patch release notes for release line `X.Y`.\n\nOrder patch sections by version descending. Preserve the established placement of minor and preview sections in the existing file.\n\n## Document Structure\n\n### Patch Release\n\n```markdown\n# vX.Y.Z\n## Downloads for vX.Y.Z\n## Changelog since vX.Y.(Z-1)\n### Changes by Kind\n#### Bug Fixes\n#### Others\n```\n\nPlace fixes under `Bug Fixes`. Place other changes, including dependency upgrades, under `Others`. Preserve the exact capitalization already used by the target changelog when it differs from this schematic.\n\n### Minor Release\n\n```markdown\n# vX.Y.0\n## Downloads for vX.Y.0\n## Urgent Update Notes\n## What's New\n## Other Notable Changes\n### API Changes\n### Features & Enhancements\n### Deprecation\n### Bug Fixes\n### Security\n## Other\n### Dependencies\n### Helm Charts\n### Instrumentation\n### Performance\n## Contributors\n\nThank you to everyone who contributed to this release!\n\nUsers whose commits are in this release (alphabetically by username)\n\n- @xxxA\n- @xxxB\n```\n\n`Urgent Update Notes` and `What's New` remain empty and are reserved for manual completion. Build `Contributors` from the commit authors collected with the same comparison range used for pull requests, excluding accounts whose usernames end in `[bot]`, such as `dependabot[bot]` and `github-actions[bot]`.\n\n### Preview Release\n\n```markdown\n# vX.Y.0-<preview>\n## Downloads for vX.Y.0-<preview>\n## Changelog since <previous-preview-version>\n## Urgent Update Notes\n## Changes by Kind\n### API Changes\n### Features & Enhancements\n### Deprecation\n### Bug Fixes\n### Security\n## Other\n### Dependencies\n### Helm Charts\n### Instrumentation\n### Performance\n```\n\n## Entry Format and Classification\n\nEvery entry uses this syntax:\n\n```markdown\n- `<component>`: <change> ([#<PR>](https://github.com/karmada-io/karmada/pull/<PR>), @<author>)\n```\n\nRules:\n\n- Mark component names with backticks, for example `karmada-controller-manager`.\n- Keep entries for the same component together within a category.\n- If one PR contains multiple user-facing changes, write one entry for each change and retain the same PR attribution.\n- Use the PR's kind as the primary classification signal. For example, `feature` maps to `Features & Enhancements` and `bug` maps to `Bug Fixes`.\n- Only use the title, description, and user-facing change as the primary signal when the kind is empty.\n- Specialized categories may override the kind-based category when the user-facing change clearly belongs to them. Classify API changes in `API Changes`, dependency version changes in `Dependencies`, Helm packaging changes in `Helm Charts`, metrics or observability changes in `Instrumentation`, performance work in `Performance`, deprecations in `Deprecation`, and security corrections in `Security`.\n- Outside those specialized categories, wording such as \"fix\" or \"feature\" in the title, description, or user-facing change must not override an available kind.\n- When one PR contains changes belonging to different categories, classify each split entry independently.\n\n## Language and Style\n\n- Write all release-note content in English.\n- Match the leading verb to the final category, even when the original user-facing change uses different framing.\n- `Features & Enhancements` entries describe the capability that was introduced or enabled and normally begin with verbs such as \"Added,\" \"Introduced,\" \"Enabled,\" or \"Supported.\" Do not leave a feature entry framed as \"Fixed\" merely because the original PR description used that word.\n- `Bug Fixes` entries describe the corrected defect and normally begin with \"Fixed\" or \"Corrected.\"\n- `API Changes` entries describe the API operation directly, using verbs such as \"Added,\" \"Introduced,\" \"Updated,\" \"Removed,\" or \"Deprecated\" as appropriate.\n- Deprecation entries use present perfect tense, for example, \"has been deprecated.\"\n- Dependency entries use present perfect or simple past tense, for example, \"has been upgraded to\" or \"Upgraded to.\"\n- Features, fixes, and all other categories use simple past tense.\n- Present-tense constructions such as \"now supports\" or \"no longer relies on\" are allowed only for a newly introduced capability or behavioral change.\n- Correct spelling and grammar while preserving technical meaning and exact identifiers.\n\n## Validation\n\n1. Confirm category, component grouping, tense, PR URL, and author syntax for every entry.\n2. For minor releases, confirm contributors exclude usernames ending in `[bot]`, are unique, and are alphabetically sorted.\n3. Run `doctoc docs/CHANGELOG/CHANGELOG-X.Y.md` after the release section is complete.\n4. The final changelog should have correct release ordering, an updated TOC, and no unrelated changes.\n",".claude/skills/generating-release-notes/scripts/fetch_contributors.sh":"#!/usr/bin/env bash\n# Copyright 2026 The Karmada Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nset -o errexit\nset -o nounset\nset -o pipefail\n\nfunction usage() {\n  echo \"Usage: $0 <owner> <repository> <base-ref> <head-ref>\" >&2\n}\n\nif [[ $# -ne 4 ]]; then\n  usage\n  exit 2\nfi\n\nif [[ -z \"${GITHUB_TOKEN:-}\" ]]; then\n  echo \"Error: GITHUB_TOKEN is not set\" >&2\n  exit 2\nfi\n\nfor command in curl jq; do\n  if ! command -v \"${command}\" > /dev/null 2>&1; then\n    echo \"Error: required command '${command}' was not found\" >&2\n    exit 2\n  fi\ndone\n\nrepo_owner=$1\nrepo_name=$2\nbase_ref=$3\nhead_ref=$4\nencoded_base_ref=$(printf '%s' \"${base_ref}\" | jq -sRr @uri)\nencoded_head_ref=$(printf '%s' \"${head_ref}\" | jq -sRr @uri)\n\ntemp_dir=$(mktemp -d)\ntrap 'rm -rf \"${temp_dir}\"' EXIT\nauthors_file=\"${temp_dir}/authors\"\ntouch \"${authors_file}\"\nunresolved_commits_file=\"${temp_dir}/unresolved-commits\"\ntouch \"${unresolved_commits_file}\"\n\npage=1\nfetched_commits=0\ntotal_commits=1\n\nwhile (( fetched_commits < total_commits )); do\n  url=\"https://api.github.com/repos/${repo_owner}/${repo_name}/compare/${encoded_base_ref}...${encoded_head_ref}?per_page=100&page=${page}\"\n  response_file=\"${temp_dir}/response-${page}.json\"\n\n  curl --fail --silent --show-error --location \\\n    -H \"Accept: application/vnd.github+json\" \\\n    -H \"Authorization: Bearer ${GITHUB_TOKEN}\" \\\n    -H \"X-GitHub-Api-Version: 2022-11-28\" \\\n    \"${url}\" > \"${response_file}\"\n\n  total_commits=$(jq -er '.total_commits | numbers' \"${response_file}\")\n  page_commits=$(jq -er '.commits | length' \"${response_file}\")\n  jq -r '.commits[].author.login // empty' \"${response_file}\" >> \"${authors_file}\"\n  jq -r '\n    .commits[]\n    | select(.author.login == null)\n    | (.commit.message | split(\"\\n\")[0]) as $title\n    | [\n        .sha,\n        if ($title | test(\"^Merge pull request #[0-9]+\\\\b\")) then\n          ($title | capture(\"^Merge pull request #(?<number>[0-9]+)\\\\b\").number)\n        elif ($title | test(\"\\\\(#[0-9]+\\\\)$\")) then\n          ($title | capture(\"\\\\(#(?<number>[0-9]+)\\\\)$\").number)\n        else\n          \"\"\n        end\n      ]\n    | @tsv\n  ' \\\n    \"${response_file}\" >> \"${unresolved_commits_file}\"\n\n  if (( page_commits == 0 )); then\n    break\n  fi\n\n  fetched_commits=$((fetched_commits + page_commits))\n  page=$((page + 1))\ndone\n\nfailed_commits_file=\"${temp_dir}/failed-commits\"\ntouch \"${failed_commits_file}\"\nwhile IFS=$'\\t' read -r commit_sha pr_number; do\n  [[ -n \"${commit_sha}\" ]] || continue\n  if [[ -z \"${pr_number}\" ]]; then\n    printf '%s\\n' \"${commit_sha}\" >> \"${failed_commits_file}\"\n    continue\n  fi\n  url=\"https://api.github.com/repos/${repo_owner}/${repo_name}/pulls/${pr_number}\"\n  response_file=\"${temp_dir}/pull-${pr_number}.json\"\n\n  if ! curl --fail --silent --show-error --location \\\n    -H \"Accept: application/vnd.github+json\" \\\n    -H \"Authorization: Bearer ${GITHUB_TOKEN}\" \\\n    -H \"X-GitHub-Api-Version: 2022-11-28\" \\\n    \"${url}\" > \"${response_file}\"; then\n    echo \"Error: failed to fetch PR #${pr_number} for commit ${commit_sha}\" >&2\n    exit 1\n  fi\n\n  if login=$(jq -er --arg sha \"${commit_sha}\" '\n    if .merged_at != null\n      and ((.head.sha // \"\") == $sha or (.merge_commit_sha // \"\") == $sha)\n      and (.user.login // \"\") != \"\"\n      then .user.login\n      else empty\n    end\n  ' \"${response_file}\"); then\n    printf '%s\\n' \"${login}\" >> \"${authors_file}\"\n  else\n    printf '%s\\n' \"${commit_sha}\" >> \"${failed_commits_file}\"\n  fi\ndone < \"${unresolved_commits_file}\"\n\nif [[ -s \"${failed_commits_file}\" ]]; then\n  echo \"Error: unable to resolve contributors for these commits:\" >&2\n  while IFS= read -r commit_sha; do\n    echo \"  ${commit_sha}\" >&2\n  done < \"${failed_commits_file}\"\n  exit 1\nfi\n\nLC_ALL=C sort -fu \"${authors_file}\"\n"},"items":[{"name":"SKILL.md","path":".claude/skills/e2e-root-cause-analysis/SKILL.md","rawUrl":"https://raw.githubusercontent.com/karmada-io/karmada/HEAD/.claude/skills/e2e-root-cause-analysis/SKILL.md","title":"e2e-root-cause-analysis","category":"anthropic-skill","format":"markdown","content":"---\nname: e2e-root-cause-analysis\ndescription: Find the root cause of failing or flaky Karmada E2E tests from CI runs. Use when a user reports an e2e test failure, a flaky CI job, or asks to find the root cause of an e2e timeout, providing a GitHub Actions run/job URL, a PR number/URL, or logs.\n---\n\n# E2E Root Cause Analysis\n\nFind the root cause of a Karmada E2E failure by working **backwards** from the error you see to the reason behind it. Never stop at the first error message. For each finding, answer: \"what does this error mean, and what caused it?\" Repeat until you reach a root cause that is fully backed by log evidence.\n\n## Step 0: Find the failing job (if only a PR is given)\n\nIf the user gives a PR number or PR URL instead of a job URL, find the failing job first:\n\n```bash\n# List all checks of the PR; failing ones come with their job URLs\ngh pr checks <pr-number> --repo karmada-io/karmada\n\n# If the failure was on an older commit, list runs for that commit\ngh run list --repo karmada-io/karmada --commit <sha> --json databaseId,name,conclusion,url\n```\n\nPick the failing e2e job (e.g. `e2e test (v1.34.0)`) and take its `.../actions/runs/<run_id>/job/<job_id>` URL. If several jobs failed, ask the user which one to look at, or start with the e2e one.\n\n## Step 1: Collect the evidence\n\nGiven a GitHub Actions run/job URL (e.g. `.../actions/runs/<run_id>/job/<job_id>`):\n\n1. Download the job log (the ginkgo output):\n\n   ```bash\n   gh api repos/karmada-io/karmada/actions/jobs/<job_id>/logs > job.log\n   ```\n\n2. List and download the component-log artifact for the failing job's Kubernetes version. This is the most valuable evidence — it holds the logs of every Karmada component and member cluster:\n\n   ```bash\n   gh api repos/karmada-io/karmada/actions/runs/<run_id>/artifacts \\\n     --jq '.artifacts[] | {id, name}'\n   gh run download <run_id> --repo karmada-io/karmada -n karmada_e2e_log_<k8s_version>\n   ```\n\n   Artifact layout:\n   - `karmada-host/karmada-host-control-plane/containers/` — control-plane components: `karmada-scheduler-*`, `karmada-controller-manager-*`, `karmada-webhook-*`, `karmada-aggregated-apiserver-*`, `karmada-scheduler-estimator-<member>-*`, etc. There are usually two pods per component (leader election); grep both, only the active one has the useful lines.\n   - `member3/` — member cluster logs, including `karmada-agent-*` for pull-mode cluster.\n\n## Step 2: Find the failure in the ginkgo log\n\n```bash\ngrep -n \"\\[FAILED\\]\\|Summarizing\" job.log\n```\n\nFor the failing spec, extract:\n\n- The **assertion** that failed (framework helper file:line) and the timeout duration.\n- The **Timeline** section: every `STEP:` with its timestamp. Timestamps are the key — they let you match the test's actions against component logs.\n- The Timeline of the **previous test case** that shares resources (same CRD, namespace, cluster labels...). Flaky tests are often caused by the previous case's cleanup, which runs in the background and races with the current case's setup.\n\nWarning signs to look for in the timeline:\n\n- A readiness wait (a `framework.Wait*` helper) that passes much faster than normal (milliseconds instead of seconds). This usually means the check read **stale state** left over from the previous case.\n- Cleanup steps of the previous case that only wait for the control plane, while changes on the member clusters are still in flight.\n\n## Step 3: Trace backwards through component logs\n\nPresent the analysis in reverse order — from the error you see down to the root cause. At each level, state: *what was seen → what it means → what caused it*. A typical chain:\n\n1. **The test error.** e.g. a timeout while waiting for some object to reach the expected state. Meaning: what the test expected never happened. Question: why?\n\n2. **The decision of the component that owns the object.** Grep that component's log for the object name, e.g.:\n\n   ```bash\n   grep -h \"<object-name>\" karmada-<component>-*.log\n   ```\n\n   Depending on the failure, the owning component may be the scheduler (binding never scheduled, `Warning` events explaining why), the controller-manager (work not dispatched, resource not synced to members), the webhook (request rejected), or the agent (pull-mode cluster out of sync). Count how many times the object shows up: if it appears once and never again, the component did **not retry** — see the note below.\n\n3. **Why the component made that decision.** The decision is usually based on some state (cluster status, cache, quota, ...). Grep the logs of the controller that writes that state around the failure window, and build a timeline of those writes with exact timestamps. Compare it with the test's Timeline from Step 2: look for a window in which the state the component read was stale or briefly wrong.\n\n4. **The root cause.** Often a race between test cases: the previous case's cleanup returned after waiting only for the control plane, while changes on the member clusters and status collection were still in flight. State it with the exact timestamps and log lines that prove the window.\n\nIf the failing object was expected to recover but never did (e.g. it appears only once in the owning component's log), do not assume the runtime retries on its own — check how retry/requeue actually works in the component's code before claiming \"it should self-heal\".\n\n## Step 4: Report\n\nWrite the conclusion as an evidence chain, from the error backwards:\n\n1. The error seen (assertion, timeout, file:line).\n2. What it means (which expected state was never reached).\n3. Direct cause with log lines + timestamps (the owning component's decision).\n4. Deeper cause (the stale or briefly wrong state, race window with timestamps).\n5. Root cause (e.g. cleanup returned too early and did not wait for member clusters to catch up).\n6. Why it did not self-heal, if relevant (checked against component code).\n\nQuote exact log lines with timestamps and file names. Check every claim about runtime behavior against the code in the repository (e.g. `pkg/scheduler/`, `pkg/controllers/`) before stating it. Clean up downloaded logs afterwards.\n\n## Step 5: Improve this skill\n\nAfter the diagnosis is done, review what you learned. If you found something that would help future diagnoses — a new failure pattern, a better command, a log location not listed here, or a wrong assumption in this document — propose an update to this skill file:\n\n- Keep the addition **generic**: describe the pattern, not the specific test, object names, or timestamps of this one case.\n- Show the user the proposed change and let them decide whether to apply it.\n- Do not commit the change yourself.\n","frontmatter":{"name":"e2e-root-cause-analysis","description":"Find the root cause of failing or flaky Karmada E2E tests from CI runs. Use when a user reports an e2e test failure, a flaky CI job, or asks to find the root cause of an e2e timeout, providing a GitHub Actions run/job URL, a PR number/URL, or logs."},"isInternal":false,"tokens":1601,"sizeBytes":6667},{"name":"karmada-release-note-format.md","path":".claude/skills/generating-release-notes/references/karmada-release-note-format.md","rawUrl":"https://raw.githubusercontent.com/karmada-io/karmada/HEAD/.claude/skills/generating-release-notes/references/karmada-release-note-format.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Karmada Release Note Format\n\n## Repository Match\n\n- Git repository root directory name: `karmada`\n\nSelect this format whenever the Git repository root directory is named `karmada`.\n\n## Upstream Data Source\n\n- GitHub owner: `karmada-io`\n- GitHub repository: `karmada`\n- Script `--repo` value: `karmada-io/karmada`\n\nAlways collect comparison and pull request data from `karmada-io/karmada`, regardless of the local fork owner. Do not derive the data source owner from the local `origin` URL.\n\n## Version Resolution\n\nClassify versions as follows:\n\n- Patch: `vX.Y.Z` where `Z > 0`. The base is `vX.Y.(Z-1)`, and the head is `release-X.Y`.\n- Minor: `vX.Y.0`. Use `vX.Y.0-alpha.0` as the base and `master` as the head.\n- Preview: `vX.Y.0-alpha.1`, `vX.Y.0-alpha.2`, `vX.Y.0-beta.0`, or `vX.Y.0-rc.0`. Use the previous preview tag and `master` as the head.\n\nThe expected preview sequence is:\n\n1. `vX.Y.0-alpha.1`, based on `vX.Y.0-alpha.0`\n2. `vX.Y.0-alpha.2`, based on `vX.Y.0-alpha.1`\n3. `vX.Y.0-beta.0`, based on `vX.Y.0-alpha.2`\n4. `vX.Y.0-rc.0`, based on `vX.Y.0-beta.0`\n\nConfirm unusual or skipped preview sequences with the user.\n\n## Output Location\n\nUpdate `docs/CHANGELOG/CHANGELOG-X.Y.md`. This file contains the minor, preview, and all patch release notes for release line `X.Y`.\n\nOrder patch sections by version descending. Preserve the established placement of minor and preview sections in the existing file.\n\n## Document Structure\n\n### Patch Release\n\n```markdown\n# vX.Y.Z\n## Downloads for vX.Y.Z\n## Changelog since vX.Y.(Z-1)\n### Changes by Kind\n#### Bug Fixes\n#### Others\n```\n\nPlace fixes under `Bug Fixes`. Place other changes, including dependency upgrades, under `Others`. Preserve the exact capitalization already used by the target changelog when it differs from this schematic.\n\n### Minor Release\n\n```markdown\n# vX.Y.0\n## Downloads for vX.Y.0\n## Urgent Update Notes\n## What's New\n## Other Notable Changes\n### API Changes\n### Features & Enhancements\n### Deprecation\n### Bug Fixes\n### Security\n## Other\n### Dependencies\n### Helm Charts\n### Instrumentation\n### Performance\n## Contributors\n\nThank you to everyone who contributed to this release!\n\nUsers whose commits are in this release (alphabetically by username)\n\n- @xxxA\n- @xxxB\n```\n\n`Urgent Update Notes` and `What's New` remain empty and are reserved for manual completion. Build `Contributors` from the commit authors collected with the same comparison range used for pull requests, excluding accounts whose usernames end in `[bot]`, such as `dependabot[bot]` and `github-actions[bot]`.\n\n### Preview Release\n\n```markdown\n# vX.Y.0-<preview>\n## Downloads for vX.Y.0-<preview>\n## Changelog since <previous-preview-version>\n## Urgent Update Notes\n## Changes by Kind\n### API Changes\n### Features & Enhancements\n### Deprecation\n### Bug Fixes\n### Security\n## Other\n### Dependencies\n### Helm Charts\n### Instrumentation\n### Performance\n```\n\n## Entry Format and Classification\n\nEvery entry uses this syntax:\n\n```markdown\n- `<component>`: <change> ([#<PR>](https://github.com/karmada-io/karmada/pull/<PR>), @<author>)\n```\n\nRules:\n\n- Mark component names with backticks, for example `karmada-controller-manager`.\n- Keep entries for the same component together within a category.\n- If one PR contains multiple user-facing changes, write one entry for each change and retain the same PR attribution.\n- Use the PR's kind as the primary classification signal. For example, `feature` maps to `Features & Enhancements` and `bug` maps to `Bug Fixes`.\n- Only use the title, description, and user-facing change as the primary signal when the kind is empty.\n- Specialized categories may override the kind-based category when the user-facing change clearly belongs to them. Classify API changes in `API Changes`, dependency version changes in `Dependencies`, Helm packaging changes in `Helm Charts`, metrics or observability changes in `Instrumentation`, performance work in `Performance`, deprecations in `Deprecation`, and security corrections in `Security`.\n- Outside those specialized categories, wording such as \"fix\" or \"feature\" in the title, description, or user-facing change must not override an available kind.\n- When one PR contains changes belonging to different categories, classify each split entry independently.\n\n## Language and Style\n\n- Write all release-note content in English.\n- Match the leading verb to the final category, even when the original user-facing change uses different framing.\n- `Features & Enhancements` entries describe the capability that was introduced or enabled and normally begin with verbs such as \"Added,\" \"Introduced,\" \"Enabled,\" or \"Supported.\" Do not leave a feature entry framed as \"Fixed\" merely because the original PR description used that word.\n- `Bug Fixes` entries describe the corrected defect and normally begin with \"Fixed\" or \"Corrected.\"\n- `API Changes` entries describe the API operation directly, using verbs such as \"Added,\" \"Introduced,\" \"Updated,\" \"Removed,\" or \"Deprecated\" as appropriate.\n- Deprecation entries use present perfect tense, for example, \"has been deprecated.\"\n- Dependency entries use present perfect or simple past tense, for example, \"has been upgraded to\" or \"Upgraded to.\"\n- Features, fixes, and all other categories use simple past tense.\n- Present-tense constructions such as \"now supports\" or \"no longer relies on\" are allowed only for a newly introduced capability or behavioral change.\n- Correct spelling and grammar while preserving technical meaning and exact identifiers.\n\n## Validation\n\n1. Confirm category, component grouping, tense, PR URL, and author syntax for every entry.\n2. For minor releases, confirm contributors exclude usernames ending in `[bot]`, are unique, and are alphabetically sorted.\n3. Run `doctoc docs/CHANGELOG/CHANGELOG-X.Y.md` after the release section is complete.\n4. The final changelog should have correct release ordering, an updated TOC, and no unrelated changes.\n","isInternal":false,"tokens":1425,"sizeBytes":5932},{"name":"SKILL.md","path":".claude/skills/generating-release-notes/SKILL.md","rawUrl":"https://raw.githubusercontent.com/karmada-io/karmada/HEAD/.claude/skills/generating-release-notes/SKILL.md","title":"generating-release-notes","category":"anthropic-skill","format":"markdown","content":"---\nname: generating-release-notes\ndescription: \"Use when preparing changelogs or release notes from merged pull requests for a patch, minor, or preview release.\"\nargument-hint: \"Target release version, for example v1.14.5 or v1.15.0-rc.0\"\n---\n\n# Generating Release Notes\n\n## Overview\n\nGenerate release notes from merged pull requests. Complete each gate in order. Do not start writing until the target version, comparison range, and format plugin are known.\n\n## Inputs\n\nA target version is required. If it is missing or unclear, ask for it and stop. Do not guess \"the next release.\"\n\nWork out these values only after the target is known:\n\n- Release type: patch, minor, or preview.\n- Previous version or preview tag.\n- Comparison branch.\n- Output changelog path.\n\nThe selected format plugin defines how to work out these values for its repository.\n\n## Select a Format Plugin\n\n1. Determine the Git repository root directory name with `basename \"$(git rev-parse --show-toplevel)\"`.\n2. Look at the Markdown files under [references](./references/). Select a plugin whose `Repository Match` includes that directory name.\n3. If exactly one plugin matches, load it and follow it exactly.\n4. If multiple plugins match, ask the user to choose.\n5. If no plugin matches, stop and report the directory name. Ask for a new repository format plugin. Never silently use another repository's format.\n\nYou can add a format plugin for a new repository. Add `<repository>-release-note-format.md` under `references/` with these sections:\n\n- `Repository Match`\n- `Upstream Data Source`\n- `Version Resolution`\n- `Output Location`\n- `Document Structure`\n- `Entry Format and Classification`\n- `Language and Style`\n- `Validation`\n\n## Required Workflow\n\n### Gate 1: Resolve the Release Range\n\n1. Classify the target as patch, minor, or preview using the selected plugin.\n2. Resolve the previous tag and head branch using the plugin's rules.\n3. Check the target, previous tag, branch, and output path against what the user asked for.\n4. Stop if anything is unclear. Do not switch to a different tag or branch without confirmation.\n\nDo not continue until Gate 1 passes.\n\n### Gate 2: Collect Merged Pull Requests\n\n1. Check that `python3` and the included [fetch_pr_info.py](./scripts/fetch_pr_info.py) exist.\n2. Check that `GITHUB_TOKEN` is set, but do not check its scopes. If it is not set but `gh auth status` reports a logged-in account, reuse that credential with `export GITHUB_TOKEN=$(gh auth token)` in the same shell as the scripts. Otherwise, ask the user to set a token with the `public_repo` scope in their environment. Never ask the user to send a token through chat, and never print the token value.\n3. From the repository root, run the included script with the base tag and head branch from Gate 1 and the `owner/repository` value listed under the plugin's `Upstream Data Source`:\n\n\t```bash\n\tpython3 .claude/skills/generating-release-notes/scripts/fetch_pr_info.py <base-tag> <head-branch> --repo <owner/repository>\n\t```\n\n4. Use only the `SUMMARY OF PRS WITH USER-FACING CHANGES` section. Ignore debug output and PRs without user-facing changes.\n5. Keep the PR number, title, author, kind, and every user-facing change. If a PR has more than one user-facing change, split them into separate entries.\n\nDo not continue if collection fails or credentials are invalid.\n\n### Gate 3: Collect Contributors\n\nRun this gate only when the selected format plugin requires a Contributors section.\n\n1. Reuse the same base tag and head branch from Gate 1. Contributor collection and PR collection must use the same comparison range.\n2. From the repository root, run the included [fetch_contributors.sh](./scripts/fetch_contributors.sh) with the owner and repository listed under `Upstream Data Source`:\n\n\t```bash\n\t.claude/skills/generating-release-notes/scripts/fetch_contributors.sh <owner> <repository> <base-tag> <head-branch>\n\t```\n\n3. The output is the list of GitHub authors whose commits were merged in that comparison range.\n4. Keep unique GitHub handles sorted alphabetically.\n\nDo not continue if contributor collection fails.\n\n### Gate 4: Classify and Draft\n\n1. Use the PR kind returned by the collection script as the main signal for choosing a category.\n2. Apply the plugin's rules for which category wins when more than one applies.\n3. Rewrite each entry in the tense and entry syntax the plugin requires, without changing its technical meaning. Make the leading verb and wording match the final category. If you move an entry to another category, re-read its wording and rewrite it if needed.\n4. Group entries as required by the plugin.\n5. Build the complete release section using the structure for the target release type.\n6. Add the commit authors collected in Gate 3 when the release requires a Contributors section.\n\nDo not make up user-facing changes, and do not guess at behavior from a PR title alone.\n\n### Gate 5: Update the Changelog\n\n1. Read the existing changelog before editing.\n2. Insert the new release in the order the plugin requires.\n3. Keep existing release sections and unrelated user changes as they are.\n4. Run the plugin's TOC or formatting command after content is final.\n\n### Gate 6: Validate\n\nCheck the requirements that apply to every repository:\n\n- No user-facing change is duplicated or missing.\n- Existing changelog content and unrelated user changes are unchanged.\n\nRun every check in the selected plugin's `Validation` section. Fix all failures before finishing.\n\n### Gate 7: Improve this skill\n\nAfter the release notes are done, think about what you learned. If you found something that would help future runs — a new failure pattern, a better command, or a wrong assumption in this document — propose an update to this skill file:\n\n- Show the user the proposed change and let them decide whether to apply it.\n- Do not commit the change yourself.\n\n## Common Failures\n\n| Failure | Required response |\n|---|---|\n| Target version is missing | Ask for it; do not guess. |\n| No format plugin matches | Stop and request a plugin. |\n| PR or contributor collection fails | Report the error and stop; do not make up or replace data. |\n","frontmatter":{"name":"generating-release-notes","description":"Use when preparing changelogs or release notes from merged pull requests for a patch, minor, or preview release.","argument-hint":"Target release version, for example v1.14.5 or v1.15.0-rc.0"},"isInternal":false,"tokens":1357,"sizeBytes":6146},{"name":"fetch_contributors.sh","path":".claude/skills/generating-release-notes/scripts/fetch_contributors.sh","rawUrl":"https://raw.githubusercontent.com/karmada-io/karmada/HEAD/.claude/skills/generating-release-notes/scripts/fetch_contributors.sh","title":"Claude Directive: fetch_contributors.sh","category":"claude-rule","format":"markdown","content":"#!/usr/bin/env bash\n# Copyright 2026 The Karmada Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nset -o errexit\nset -o nounset\nset -o pipefail\n\nfunction usage() {\n  echo \"Usage: $0 <owner> <repository> <base-ref> <head-ref>\" >&2\n}\n\nif [[ $# -ne 4 ]]; then\n  usage\n  exit 2\nfi\n\nif [[ -z \"${GITHUB_TOKEN:-}\" ]]; then\n  echo \"Error: GITHUB_TOKEN is not set\" >&2\n  exit 2\nfi\n\nfor command in curl jq; do\n  if ! command -v \"${command}\" > /dev/null 2>&1; then\n    echo \"Error: required command '${command}' was not found\" >&2\n    exit 2\n  fi\ndone\n\nrepo_owner=$1\nrepo_name=$2\nbase_ref=$3\nhead_ref=$4\nencoded_base_ref=$(printf '%s' \"${base_ref}\" | jq -sRr @uri)\nencoded_head_ref=$(printf '%s' \"${head_ref}\" | jq -sRr @uri)\n\ntemp_dir=$(mktemp -d)\ntrap 'rm -rf \"${temp_dir}\"' EXIT\nauthors_file=\"${temp_dir}/authors\"\ntouch \"${authors_file}\"\nunresolved_commits_file=\"${temp_dir}/unresolved-commits\"\ntouch \"${unresolved_commits_file}\"\n\npage=1\nfetched_commits=0\ntotal_commits=1\n\nwhile (( fetched_commits < total_commits )); do\n  url=\"https://api.github.com/repos/${repo_owner}/${repo_name}/compare/${encoded_base_ref}...${encoded_head_ref}?per_page=100&page=${page}\"\n  response_file=\"${temp_dir}/response-${page}.json\"\n\n  curl --fail --silent --show-error --location \\\n    -H \"Accept: application/vnd.github+json\" \\\n    -H \"Authorization: Bearer ${GITHUB_TOKEN}\" \\\n    -H \"X-GitHub-Api-Version: 2022-11-28\" \\\n    \"${url}\" > \"${response_file}\"\n\n  total_commits=$(jq -er '.total_commits | numbers' \"${response_file}\")\n  page_commits=$(jq -er '.commits | length' \"${response_file}\")\n  jq -r '.commits[].author.login // empty' \"${response_file}\" >> \"${authors_file}\"\n  jq -r '\n    .commits[]\n    | select(.author.login == null)\n    | (.commit.message | split(\"\\n\")[0]) as $title\n    | [\n        .sha,\n        if ($title | test(\"^Merge pull request #[0-9]+\\\\b\")) then\n          ($title | capture(\"^Merge pull request #(?<number>[0-9]+)\\\\b\").number)\n        elif ($title | test(\"\\\\(#[0-9]+\\\\)$\")) then\n          ($title | capture(\"\\\\(#(?<number>[0-9]+)\\\\)$\").number)\n        else\n          \"\"\n        end\n      ]\n    | @tsv\n  ' \\\n    \"${response_file}\" >> \"${unresolved_commits_file}\"\n\n  if (( page_commits == 0 )); then\n    break\n  fi\n\n  fetched_commits=$((fetched_commits + page_commits))\n  page=$((page + 1))\ndone\n\nfailed_commits_file=\"${temp_dir}/failed-commits\"\ntouch \"${failed_commits_file}\"\nwhile IFS=$'\\t' read -r commit_sha pr_number; do\n  [[ -n \"${commit_sha}\" ]] || continue\n  if [[ -z \"${pr_number}\" ]]; then\n    printf '%s\\n' \"${commit_sha}\" >> \"${failed_commits_file}\"\n    continue\n  fi\n  url=\"https://api.github.com/repos/${repo_owner}/${repo_name}/pulls/${pr_number}\"\n  response_file=\"${temp_dir}/pull-${pr_number}.json\"\n\n  if ! curl --fail --silent --show-error --location \\\n    -H \"Accept: application/vnd.github+json\" \\\n    -H \"Authorization: Bearer ${GITHUB_TOKEN}\" \\\n    -H \"X-GitHub-Api-Version: 2022-11-28\" \\\n    \"${url}\" > \"${response_file}\"; then\n    echo \"Error: failed to fetch PR #${pr_number} for commit ${commit_sha}\" >&2\n    exit 1\n  fi\n\n  if login=$(jq -er --arg sha \"${commit_sha}\" '\n    if .merged_at != null\n      and ((.head.sha // \"\") == $sha or (.merge_commit_sha // \"\") == $sha)\n      and (.user.login // \"\") != \"\"\n      then .user.login\n      else empty\n    end\n  ' \"${response_file}\"); then\n    printf '%s\\n' \"${login}\" >> \"${authors_file}\"\n  else\n    printf '%s\\n' \"${commit_sha}\" >> \"${failed_commits_file}\"\n  fi\ndone < \"${unresolved_commits_file}\"\n\nif [[ -s \"${failed_commits_file}\" ]]; then\n  echo \"Error: unable to resolve contributors for these commits:\" >&2\n  while IFS= read -r commit_sha; do\n    echo \"  ${commit_sha}\" >&2\n  done < \"${failed_commits_file}\"\n  exit 1\nfi\n\nLC_ALL=C sort -fu \"${authors_file}\"\n","isInternal":false,"tokens":1241,"sizeBytes":4243},{"name":"fetch_pr_info.py","path":".claude/skills/generating-release-notes/scripts/fetch_pr_info.py","rawUrl":"https://raw.githubusercontent.com/karmada-io/karmada/HEAD/.claude/skills/generating-release-notes/scripts/fetch_pr_info.py","title":"Claude Directive: fetch_pr_info.py","category":"claude-rule","format":"markdown","content":"#!/usr/bin/env python3\n# Copyright 2026 The Karmada Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"Fetch merged PR metadata and user-facing changes for release notes.\"\"\"\n\nimport argparse\nimport json\nimport os\nimport re\nimport sys\nimport urllib.error\nimport urllib.request\n\n\nGITHUB_API_BASE = \"https://api.github.com\"\nNO_RELEASE_NOTE_VALUES = {\n    \"\",\n    \"n/a\",\n    \"na\",\n    \"no\",\n    \"none\",\n    \"noop\",\n    \"nope\",\n    \"nothing\",\n}\n\n\ndef github_request(url, token, payload=None):\n    \"\"\"Send an authenticated GitHub API request and decode its JSON body.\"\"\"\n    headers = {\n        \"Accept\": \"application/vnd.github.v3+json\",\n        \"User-Agent\": \"Release-Note-Generator\",\n    }\n    if token:\n        headers[\"Authorization\"] = f\"Bearer {token}\"\n\n    data = None\n    if payload is not None:\n        headers[\"Content-Type\"] = \"application/json\"\n        data = json.dumps(payload).encode(\"utf-8\")\n\n    request = urllib.request.Request(url, data=data, headers=headers)\n    try:\n        with urllib.request.urlopen(request) as response:\n            return json.load(response)\n    except urllib.error.HTTPError as error:\n        detail = error.read().decode(\"utf-8\", errors=\"replace\")\n        print(f\"GitHub request failed ({error.code}): {detail}\", file=sys.stderr)\n    except urllib.error.URLError as error:\n        print(f\"GitHub request failed: {error.reason}\", file=sys.stderr)\n    return None\n\n\ndef get_commit_comparison(repo_owner, repo_name, base_tag, head_branch, token):\n    \"\"\"Get the GitHub comparison between the base tag and head branch.\"\"\"\n    base_url = (\n        f\"{GITHUB_API_BASE}/repos/{repo_owner}/{repo_name}/compare/\"\n        f\"{base_tag}...{head_branch}\"\n    )\n    comparison = None\n    commits = []\n    page = 1\n\n    while True:\n        response = github_request(f\"{base_url}?per_page=100&page={page}\", token)\n        if not response:\n            break\n        if comparison is None:\n            comparison = response.copy()\n\n        page_commits = response.get(\"commits\", [])\n        commits.extend(page_commits)\n        total_commits = comparison.get(\"total_commits\", len(commits))\n        if len(commits) >= total_commits or len(page_commits) < 100:\n            break\n        page += 1\n\n    if comparison is not None:\n        comparison[\"commits\"] = commits\n    return comparison\n\n\ndef get_pr_details_batch(repo_owner, repo_name, pr_numbers, token):\n    \"\"\"Fetch pull request title, body, and author in one GraphQL request.\"\"\"\n    if not pr_numbers:\n        return {}\n\n    query_parts = []\n    for index, number in enumerate(pr_numbers):\n        query_parts.append(\n            f\"pr{index}: pullRequest(number: {number}) {{ \"\n            \"number title body author { login } \"\n            \"labels(first: 100) { nodes { name } } }\"\n        )\n    query = (\n        \"{ repository(owner: \\\"\"\n        + repo_owner\n        + \"\\\", name: \\\"\"\n        + repo_name\n        + \"\\\") { \"\n        + \" \".join(query_parts)\n        + \" } }\"\n    )\n    response = github_request(\n        \"https://api.github.com/graphql\", token, {\"query\": query}\n    )\n    if not response:\n        return None\n    if response.get(\"errors\"):\n        print(f\"GraphQL errors: {response['errors']}\", file=sys.stderr)\n        return None\n\n    repository = response.get(\"data\", {}).get(\"repository\", {})\n    result = {}\n    for index, number in enumerate(pr_numbers):\n        pull_request = repository.get(f\"pr{index}\")\n        if pull_request:\n            result[number] = pull_request\n    return result\n\n\ndef normalize_release_note(content):\n    \"\"\"Normalize a candidate release note and reject standard empty values.\"\"\"\n    normalized = re.sub(r\"\\s+\", \" \", content.strip())\n    if normalized.lower() in NO_RELEASE_NOTE_VALUES or len(normalized) <= 3:\n        return None\n    return normalized\n\n\ndef extract_user_facing_change(pr_body):\n    \"\"\"Extract a user-facing change from supported PR template formats.\"\"\"\n    if not pr_body:\n        return None\n\n    release_note_patterns = [\n        r\"```release-note[^\\r\\n]*[\\r\\n]+(.*?)^[ \\t]*```[ \\t]*$\",\n        r\"```release-note\\s*[\\r\\n]+([^\\r\\n]+(?:[\\r\\n]+[^\\r\\n`]+)*)\",\n    ]\n    for pattern in release_note_patterns:\n        match = re.search(pattern, pr_body, re.MULTILINE | re.DOTALL)\n        if match:\n            return normalize_release_note(match.group(1))\n\n    fallback = re.search(\n        r\"\\*\\*Does this PR introduce a user-facing change\\?\\*\\*:\\s*\"\n        r\"```[^\\r\\n]*[\\r\\n]+([\\s\\S]*?)```\",\n        pr_body,\n        re.IGNORECASE,\n    )\n    if fallback:\n        return normalize_release_note(fallback.group(1))\n    return None\n\n\ndef extract_pr_kind_from_labels(labels):\n    \"\"\"Extract PR kinds from kind-prefixed labels.\"\"\"\n    kinds = []\n    for label in (labels or {}).get(\"nodes\", []):\n        name = (label or {}).get(\"name\", \"\")\n        if name.startswith(\"kind/\") and len(name) > len(\"kind/\"):\n            kind = name[len(\"kind/\") :]\n            if kind not in kinds:\n                kinds.append(kind)\n    return kinds or None\n\n\ndef extract_pr_kind_from_body(pr_body):\n    \"\"\"Extract PR kinds from the Karmada pull request template body.\"\"\"\n    if not pr_body:\n        return None\n\n    section = re.search(\n        r\"\\*\\*What type of PR is this\\?\\*\\*(.*?)\"\n        r\"\\*\\*What this PR does / why we need it\\*\\*:\",\n        pr_body,\n        re.DOTALL | re.IGNORECASE,\n    )\n    if not section:\n        return None\n\n    without_comments = re.sub(r\"<!--.*?-->\", \"\", section.group(1), flags=re.DOTALL)\n    kinds = re.findall(\n        r\"^\\s*/kind\\s+([a-zA-Z0-9-]+)\\s*$\", without_comments, re.MULTILINE\n    )\n    return kinds or None\n\n\ndef extract_pr_kind(labels, pr_body):\n    \"\"\"Extract PR kinds from labels, falling back to the PR body.\"\"\"\n    return extract_pr_kind_from_labels(labels) or extract_pr_kind_from_body(pr_body)\n\n\ndef extract_pr_number(commit_message):\n    \"\"\"Extract a PR number from a GitHub merge or squash commit title.\"\"\"\n    title = commit_message.splitlines()[0] if commit_message else \"\"\n    merge_match = re.match(r\"Merge pull request #(\\d+)\\b\", title)\n    if merge_match:\n        return int(merge_match.group(1))\n\n    squash_match = re.search(r\"\\(#(\\d+)\\)$\", title)\n    if squash_match:\n        return int(squash_match.group(1))\n    return None\n\n\ndef comparison_is_truncated(comparison):\n    \"\"\"Report whether the compare response omitted commits from its list.\"\"\"\n    commits = comparison.get(\"commits\", [])\n    return comparison.get(\"total_commits\", len(commits)) > len(commits)\n\n\ndef parse_arguments():\n    \"\"\"Parse command-line arguments.\"\"\"\n    parser = argparse.ArgumentParser(\n        description=\"Fetch merged PR information for release note generation.\"\n    )\n    parser.add_argument(\"base_tag\", help=\"Base tag or branch to compare from\")\n    parser.add_argument(\"head_branch\", help=\"Head tag or branch to compare to\")\n    parser.add_argument(\n        \"--repo\",\n        required=True,\n        help=\"GitHub repository in owner/name format\",\n    )\n    return parser.parse_args()\n\n\ndef main():\n    \"\"\"Fetch and print PRs that contain user-facing changes.\"\"\"\n    args = parse_arguments()\n    if args.repo.count(\"/\") != 1:\n        print(\"Error: --repo must use owner/name format\", file=sys.stderr)\n        return 2\n\n    token = os.getenv(\"GITHUB_TOKEN\")\n    if not token:\n        print(\"Error: GITHUB_TOKEN is not set\", file=sys.stderr)\n        return 2\n\n    repo_owner, repo_name = args.repo.split(\"/\", 1)\n    print(f\"Fetching PR information for {args.repo}...\")\n    print(f\"Comparing {args.base_tag}...{args.head_branch}\")\n    print(\"=\" * 80)\n\n    comparison = get_commit_comparison(\n        repo_owner, repo_name, args.base_tag, args.head_branch, token\n    )\n    if not comparison:\n        print(\"Failed to get commit comparison\", file=sys.stderr)\n        return 1\n\n    commits = comparison.get(\"commits\", [])\n    if comparison_is_truncated(comparison):\n        print(\n            \"Error: GitHub returned only \"\n            f\"{len(commits)} of {comparison['total_commits']} commits; \"\n            \"refusing to generate incomplete release notes\",\n            file=sys.stderr,\n        )\n        return 1\n\n    pr_numbers = set()\n    for commit in commits:\n        message = commit.get(\"commit\", {}).get(\"message\", \"\")\n        pr_number = extract_pr_number(message)\n        if pr_number is not None:\n            pr_numbers.add(pr_number)\n\n    sorted_pr_numbers = sorted(pr_numbers)\n    print(f\"Found {len(commits)} commits\")\n    print(f\"Found {len(sorted_pr_numbers)} merged PRs\")\n    print(\"=\" * 80)\n\n    pr_details = get_pr_details_batch(\n        repo_owner, repo_name, sorted_pr_numbers, token\n    )\n    if pr_details is None:\n        print(\"Failed to get pull request details\", file=sys.stderr)\n        return 1\n\n    release_notes = []\n    for pr_number in sorted_pr_numbers:\n        pull_request = pr_details.get(pr_number)\n        if not pull_request:\n            continue\n        change = extract_user_facing_change(pull_request.get(\"body\") or \"\")\n        if not change:\n            continue\n        author = pull_request.get(\"author\") or {}\n        release_notes.append(\n            {\n                \"number\": pr_number,\n                \"title\": pull_request.get(\"title\", \"\"),\n                \"author\": author.get(\"login\", \"unknown\"),\n                \"kind\": extract_pr_kind(\n                    pull_request.get(\"labels\"), pull_request.get(\"body\") or \"\"\n                ),\n                \"change\": change,\n            }\n        )\n\n    print(\"\\n\" + \"=\" * 80)\n    print(\"SUMMARY OF PRS WITH USER-FACING CHANGES\")\n    print(\"=\" * 80)\n    if release_notes:\n        for release_note in release_notes:\n            print(f\"\\nPR #{release_note['number']} by @{release_note['author']}\")\n            print(f\"Title: {release_note['title']}\")\n            print(f\"Kind: {release_note['kind']}\")\n            print(f\"Change: {release_note['change']}\")\n    else:\n        print(\"No PRs with user-facing changes found.\")\n    print(f\"\\nTotal PRs with user-facing changes: {len(release_notes)}\")\n    return 0\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n","isInternal":false,"tokens":2458,"sizeBytes":10534},{"name":"README.md","path":"vendor/github.com/google/gnostic-models/extensions/README.md","rawUrl":"https://raw.githubusercontent.com/karmada-io/karmada/HEAD/vendor/github.com/google/gnostic-models/extensions/README.md","title":"extensions Documentation","category":"plugin-manifest","format":"markdown","content":"# Extensions\n\n**Extension Support is experimental.**\n\nThis directory contains support code for building Gnostic extensio handlers and\nassociated examples.\n\nExtension handlers can be used to compile vendor or specification extensions\ninto protocol buffer structures.\n\nLike plugins, extension handlers are built as separate executables. Extension\nbodies are written to extension handlers as serialized\nExtensionHandlerRequests.\n","isInternal":false,"tokens":73,"sizeBytes":426}],"systemPromptSnippet":"<yakaai_skills repo=\"karmada-io/karmada\">\n<!-- File: .claude/skills/e2e-root-cause-analysis/SKILL.md (Tokens: ~1601 | Category: anthropic-skill) -->\n---\nname: e2e-root-cause-analysis\ndescription: Find the root cause of failing or flaky Karmada E2E tests from CI runs. Use when a user reports an e2e test failure, a flaky CI job, or asks to find the root cause of an e2e timeout, providing a GitHub Actions run/job URL, a PR number/URL, or logs.\n---\n\n# E2E Root Cause Analysis\n\nFind the root cause of a Karmada E2E failure by working **backwards** from the error you see to the reason behind it. Never stop at the first error message. For each finding, answer: \"what does this error mean, and what caused it?\" Repeat until you reach a root cause that is fully backed by log evidence.\n\n## Step 0: Find the failing job (if only a PR is given)\n\nIf the user gives a PR number or PR URL instead of a job URL, find the failing job first:\n\n```bash\n# List all checks of the PR; failing ones come with their job URLs\ngh pr checks <pr-number> --repo karmada-io/karmada\n\n# If the failure was on an older commit, list runs for that commit\ngh run list --repo karmada-io/karmada --commit <sha> --json databaseId,name,conclusion,url\n```\n\nPick the failing e2e job (e.g. `e2e test (v1.34.0)`) and take its `.../actions/runs/<run_id>/job/<job_id>` URL. If several jobs failed, ask the user which one to look at, or start with the e2e one.\n\n## Step 1: Collect the evidence\n\nGiven a GitHub Actions run/job URL (e.g. `.../actions/runs/<run_id>/job/<job_id>`):\n\n1. Download the job log (the ginkgo output):\n\n   ```bash\n   gh api repos/karmada-io/karmada/actions/jobs/<job_id>/logs > job.log\n   ```\n\n2. List and download the component-log artifact for the failing job's Kubernetes version. This is the most valuable evidence — it holds the logs of every Karmada component and member cluster:\n\n   ```bash\n   gh api repos/karmada-io/karmada/actions/runs/<run_id>/artifacts \\\n     --jq '.artifacts[] | {id, name}'\n   gh run download <run_id> --repo karmada-io/karmada -n karmada_e2e_log_<k8s_version>\n   ```\n\n   Artifact layout:\n   - `karmada-host/karmada-host-control-plane/containers/` — control-plane components: `karmada-scheduler-*`, `karmada-controller-manager-*`, `karmada-webhook-*`, `karmada-aggregated-apiserver-*`, `karmada-scheduler-estimator-<member>-*`, etc. There are usually two pods per component (leader election); grep both, only the active one has the useful lines.\n   - `member3/` — member cluster logs, including `karmada-agent-*` for pull-mode cluster.\n\n## Step 2: Find the failure in the ginkgo log\n\n```bash\ngrep -n \"\\[FAILED\\]\\|Summarizing\" job.log\n```\n\nFor the failing spec, extract:\n\n- The **assertion** that failed (framework helper file:line) and the timeout duration.\n- The **Timeline** section: every `STEP:` with its timestamp. Timestamps are the key — they let you match the test's actions against component logs.\n- The Timeline of the **previous test case** that shares resources (same CRD, namespace, cluster labels...). Flaky tests are often caused by the previous case's cleanup, which runs in the background and races with the current case's setup.\n\nWarning signs to look for in the timeline:\n\n- A readiness wait (a `framework.Wait*` helper) that passes much faster than normal (milliseconds instead of seconds). This usually means the check read **stale state** left over from the previous case.\n- Cleanup steps of the previous case that only wait for the control plane, while changes on the member clusters are still in flight.\n\n## Step 3: Trace backwards through component logs\n\nPresent the analysis in reverse order — from the error you see down to the root cause. At each level, state: *what was seen → what it means → what caused it*. A typical chain:\n\n1. **The test error.** e.g. a timeout while waiting for some object to reach the expected state. Meaning: what the test expected never happened. Question: why?\n\n2. **The decision of the component that owns the object.** Grep that component's log for the object name, e.g.:\n\n   ```bash\n   grep -h \"<object-name>\" karmada-<component>-*.log\n   ```\n\n   Depending on the failure, the owning component may be the scheduler (binding never scheduled, `Warning` events explaining why), the controller-manager (work not dispatched, resource not synced to members), the webhook (request rejected), or the agent (pull-mode cluster out of sync). Count how many times the object shows up: if it appears once and never again, the component did **not retry** — see the note below.\n\n3. **Why the component made that decision.** The decision is usually based on some state (cluster status, cache, quota, ...). Grep the logs of the controller that writes that state around the failure window, and build a timeline of those writes with exact timestamps. Compare it with the test's Timeline from Step 2: look for a window in which the state the component read was stale or briefly wrong.\n\n4. **The root cause.** Often a race between test cases: the previous case's cleanup returned after waiting only for the control plane, while changes on the member clusters and status collection were still in flight. State it with the exact timestamps and log lines that prove the window.\n\nIf the failing object was expected to recover but never did (e.g. it appears only once in the owning component's log), do not assume the runtime retries on its own — check how retry/requeue actually works in the component's code before claiming \"it should self-heal\".\n\n## Step 4: Report\n\nWrite the conclusion as an evidence chain, from the error backwards:\n\n1. The error seen (assertion, timeout, file:line).\n2. What it means (which expected state was never reached).\n3. Direct cause with log lines + timestamps (the owning component's decision).\n4. Deeper cause (the stale or briefly wrong state, race window with timestamps).\n5. Root cause (e.g. cleanup returned too early and did not wait for member clusters to catch up).\n6. Why it did not self-heal, if relevant (checked against component code).\n\nQuote exact log lines with timestamps and file names. Check every claim about runtime behavior against the code in the repository (e.g. `pkg/scheduler/`, `pkg/controllers/`) before stating it. Clean up downloaded logs afterwards.\n\n## Step 5: Improve this skill\n\nAfter the diagnosis is done, review what you learned. If you found something that would help future diagnoses — a new failure pattern, a better command, a log location not listed here, or a wrong assumption in this document — propose an update to this skill file:\n\n- Keep the addition **generic**: describe the pattern, not the specific test, object names, or timestamps of this one case.\n- Show the user the proposed change and let them decide whether to apply it.\n- Do not commit the change yourself.\n\n<!-- File: .claude/skills/generating-release-notes/references/karmada-release-note-format.md (Tokens: ~1425 | Category: anthropic-skill) -->\n# Karmada Release Note Format\n\n## Repository Match\n\n- Git repository root directory name: `karmada`\n\nSelect this format whenever the Git repository root directory is named `karmada`.\n\n## Upstream Data Source\n\n- GitHub owner: `karmada-io`\n- GitHub repository: `karmada`\n- Script `--repo` value: `karmada-io/karmada`\n\nAlways collect comparison and pull request data from `karmada-io/karmada`, regardless of the local fork owner. Do not derive the data source owner from the local `origin` URL.\n\n## Version Resolution\n\nClassify versions as follows:\n\n- Patch: `vX.Y.Z` where `Z > 0`. The base is `vX.Y.(Z-1)`, and the head is `release-X.Y`.\n- Minor: `vX.Y.0`. Use `vX.Y.0-alpha.0` as the base and `master` as the head.\n- Preview: `vX.Y.0-alpha.1`, `vX.Y.0-alpha.2`, `vX.Y.0-beta.0`, or `vX.Y.0-rc.0`. Use the previous preview tag and `master` as the head.\n\nThe expected preview sequence is:\n\n1. `vX.Y.0-alpha.1`, based on `vX.Y.0-alpha.0`\n2. `vX.Y.0-alpha.2`, based on `vX.Y.0-alpha.1`\n3. `vX.Y.0-beta.0`, based on `vX.Y.0-alpha.2`\n4. `vX.Y.0-rc.0`, based on `vX.Y.0-beta.0`\n\nConfirm unusual or skipped preview sequences with the user.\n\n## Output Location\n\nUpdate `docs/CHANGELOG/CHANGELOG-X.Y.md`. This file contains the minor, preview, and all patch release notes for release line `X.Y`.\n\nOrder patch sections by version descending. Preserve the established placement of minor and preview sections in the existing file.\n\n## Document Structure\n\n### Patch Release\n\n```markdown\n# vX.Y.Z\n## Downloads for vX.Y.Z\n## Changelog since vX.Y.(Z-1)\n### Changes by Kind\n#### Bug Fixes\n#### Others\n```\n\nPlace fixes under `Bug Fixes`. Place other changes, including dependency upgrades, under `Others`. Preserve the exact capitalization already used by the target changelog when it differs from this schematic.\n\n### Minor Release\n\n```markdown\n# vX.Y.0\n## Downloads for vX.Y.0\n## Urgent Update Notes\n## What's New\n## Other Notable Changes\n### API Changes\n### Features & Enhancements\n### Deprecation\n### Bug Fixes\n### Security\n## Other\n### Dependencies\n### Helm Charts\n### Instrumentation\n### Performance\n## Contributors\n\nThank you to everyone who contributed to this release!\n\nUsers whose commits are in this release (alphabetically by username)\n\n- @xxxA\n- @xxxB\n```\n\n`Urgent Update Notes` and `What's New` remain empty and are reserved for manual completion. Build `Contributors` from the commit authors collected with the same comparison range used for pull requests, excluding accounts whose usernames end in `[bot]`, such as `dependabot[bot]` and `github-actions[bot]`.\n\n### Preview Release\n\n```markdown\n# vX.Y.0-<preview>\n## Downloads for vX.Y.0-<preview>\n## Changelog since <previous-preview-version>\n## Urgent Update Notes\n## Changes by Kind\n### API Changes\n### Features & Enhancements\n### Deprecation\n### Bug Fixes\n### Security\n## Other\n### Dependencies\n### Helm Charts\n### Instrumentation\n### Performance\n```\n\n## Entry Format and Classification\n\nEvery entry uses this syntax:\n\n```markdown\n- `<component>`: <change> ([#<PR>](https://github.com/karmada-io/karmada/pull/<PR>), @<author>)\n```\n\nRules:\n\n- Mark component names with backticks, for example `karmada-controller-manager`.\n- Keep entries for the same component together within a category.\n- If one PR contains multiple user-facing changes, write one entry for each change and retain the same PR attribution.\n- Use the PR's kind as the primary classification signal. For example, `feature` maps to `Features & Enhancements` and `bug` maps to `Bug Fixes`.\n- Only use the title, description, and user-facing change as the primary signal when the kind is empty.\n- Specialized categories may override the kind-based category when the user-facing change clearly belongs to them. Classify API changes in `API Changes`, dependency version changes in `Dependencies`, Helm packaging changes in `Helm Charts`, metrics or observability changes in `Instrumentation`, performance work in `Performance`, deprecations in `Deprecation`, and security corrections in `Security`.\n- Outside those specialized categories, wording such as \"fix\" or \"feature\" in the title, description, or user-facing change must not override an available kind.\n- When one PR contains changes belonging to different categories, classify each split entry independently.\n\n## Language and Style\n\n- Write all release-note content in English.\n- Match the leading verb to the final category, even when the original user-facing change uses different framing.\n- `Features & Enhancements` entries describe the capability that was introduced or enabled and normally begin with verbs such as \"Added,\" \"Introduced,\" \"Enabled,\" or \"Supported.\" Do not leave a feature entry framed as \"Fixed\" merely because the original PR description used that word.\n- `Bug Fixes` entries describe the corrected defect and normally begin with \"Fixed\" or \"Corrected.\"\n- `API Changes` entries describe the API operation directly, using verbs such as \"Added,\" \"Introduced,\" \"Updated,\" \"Removed,\" or \"Deprecated\" as appropriate.\n- Deprecation entries use present perfect tense, for example, \"has been deprecated.\"\n- Dependency entries use present perfect or simple past tense, for example, \"has been upgraded to\" or \"Upgraded to.\"\n- Features, fixes, and all other categories use simple past tense.\n- Present-tense constructions such as \"now supports\" or \"no longer relies on\" are allowed only for a newly introduced capability or behavioral change.\n- Correct spelling and grammar while preserving technical meaning and exact identifiers.\n\n## Validation\n\n1. Confirm category, component grouping, tense, PR URL, and author syntax for every entry.\n2. For minor releases, confirm contributors exclude usernames ending in `[bot]`, are unique, and are alphabetically sorted.\n3. Run `doctoc docs/CHANGELOG/CHANGELOG-X.Y.md` after the release section is complete.\n4. The final changelog should have correct release ordering, an updated TOC, and no unrelated changes.\n\n<!-- File: .claude/skills/generating-release-notes/SKILL.md (Tokens: ~1357 | Category: anthropic-skill) -->\n---\nname: generating-release-notes\ndescription: \"Use when preparing changelogs or release notes from merged pull requests for a patch, minor, or preview release.\"\nargument-hint: \"Target release version, for example v1.14.5 or v1.15.0-rc.0\"\n---\n\n# Generating Release Notes\n\n## Overview\n\nGenerate release notes from merged pull requests. Complete each gate in order. Do not start writing until the target version, comparison range, and format plugin are known.\n\n## Inputs\n\nA target version is required. If it is missing or unclear, ask for it and stop. Do not guess \"the next release.\"\n\nWork out these values only after the target is known:\n\n- Release type: patch, minor, or preview.\n- Previous version or preview tag.\n- Comparison branch.\n- Output changelog path.\n\nThe selected format plugin defines how to work out these values for its repository.\n\n## Select a Format Plugin\n\n1. Determine the Git repository root directory name with `basename \"$(git rev-parse --show-toplevel)\"`.\n2. Look at the Markdown files under [references](./references/). Select a plugin whose `Repository Match` includes that directory name.\n3. If exactly one plugin matches, load it and follow it exactly.\n4. If multiple plugins match, ask the user to choose.\n5. If no plugin matches, stop and report the directory name. Ask for a new repository format plugin. Never silently use another repository's format.\n\nYou can add a format plugin for a new repository. Add `<repository>-release-note-format.md` under `references/` with these sections:\n\n- `Repository Match`\n- `Upstream Data Source`\n- `Version Resolution`\n- `Output Location`\n- `Document Structure`\n- `Entry Format and Classification`\n- `Language and Style`\n- `Validation`\n\n## Required Workflow\n\n### Gate 1: Resolve the Release Range\n\n1. Classify the target as patch, minor, or preview using the selected plugin.\n2. Resolve the previous tag and head branch using the plugin's rules.\n3. Check the target, previous tag, branch, and output path against what the user asked for.\n4. Stop if anything is unclear. Do not switch to a different tag or branch without confirmation.\n\nDo not continue until Gate 1 passes.\n\n### Gate 2: Collect Merged Pull Requests\n\n1. Check that `python3` and the included [fetch_pr_info.py](./scripts/fetch_pr_info.py) exist.\n2. Check that `GITHUB_TOKEN` is set, but do not check its scopes. If it is not set but `gh auth status` reports a logged-in account, reuse that credential with `export GITHUB_TOKEN=$(gh auth token)` in the same shell as the scripts. Otherwise, ask the user to set a token with the `public_repo` scope in their environment. Never ask the user to send a token through chat, and never print the token value.\n3. From the repository root, run the included script with the base tag and head branch from Gate 1 and the `owner/repository` value listed under the plugin's `Upstream Data Source`:\n\n\t```bash\n\tpython3 .claude/skills/generating-release-notes/scripts/fetch_pr_info.py <base-tag> <head-branch> --repo <owner/repository>\n\t```\n\n4. Use only the `SUMMARY OF PRS WITH USER-FACING CHANGES` section. Ignore debug output and PRs without user-facing changes.\n5. Keep the PR number, title, author, kind, and every user-facing change. If a PR has more than one user-facing change, split them into separate entries.\n\nDo not continue if collection fails or credentials are invalid.\n\n### Gate 3: Collect Contributors\n\nRun this gate only when the selected format plugin requires a Contributors section.\n\n1. Reuse the same base tag and head branch from Gate 1. Contributor collection and PR collection must use the same comparison range.\n2. From the repository root, run the included [fetch_contributors.sh](./scripts/fetch_contributors.sh) with the owner and repository listed under `Upstream Data Source`:\n\n\t```bash\n\t.claude/skills/generating-release-notes/scripts/fetch_contributors.sh <owner> <repository> <base-tag> <head-branch>\n\t```\n\n3. The output is the list of GitHub authors whose commits were merged in that comparison range.\n4. Keep unique GitHub handles sorted alphabetically.\n\nDo not continue if contributor collection fails.\n\n### Gate 4: Classify and Draft\n\n1. Use the PR kind returned by the collection script as the main signal for choosing a category.\n2. Apply the plugin's rules for which category wins when more than one applies.\n3. Rewrite each entry in the tense and entry syntax the plugin requires, without changing its technical meaning. Make the leading verb and wording match the final category. If you move an entry to another category, re-read its wording and rewrite it if needed.\n4. Group entries as required by the plugin.\n5. Build the complete release section using the structure for the target release type.\n6. Add the commit authors collected in Gate 3 when the release requires a Contributors section.\n\nDo not make up user-facing changes, and do not guess at behavior from a PR title alone.\n\n### Gate 5: Update the Changelog\n\n1. Read the existing changelog before editing.\n2. Insert the new release in the order the plugin requires.\n3. Keep existing release sections and unrelated user changes as they are.\n4. Run the plugin's TOC or formatting command after content is final.\n\n### Gate 6: Validate\n\nCheck the requirements that apply to every repository:\n\n- No user-facing change is duplicated or missing.\n- Existing changelog content and unrelated user changes are unchanged.\n\nRun every check in the selected plugin's `Validation` section. Fix all failures before finishing.\n\n### Gate 7: Improve this skill\n\nAfter the release notes are done, think about what you learned. If you found something that would help future runs — a new failure pattern, a better command, or a wrong assumption in this document — propose an update to this skill file:\n\n- Show the user the proposed change and let them decide whether to apply it.\n- Do not commit the change yourself.\n\n## Common Failures\n\n| Failure | Required response |\n|---|---|\n| Target version is missing | Ask for it; do not guess. |\n| No format plugin matches | Stop and request a plugin. |\n| PR or contributor collection fails | Report the error and stop; do not make up or replace data. |\n\n<!-- File: .claude/skills/generating-release-notes/scripts/fetch_contributors.sh (Tokens: ~1241 | Category: claude-rule) -->\n#!/usr/bin/env bash\n# Copyright 2026 The Karmada Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nset -o errexit\nset -o nounset\nset -o pipefail\n\nfunction usage() {\n  echo \"Usage: $0 <owner> <repository> <base-ref> <head-ref>\" >&2\n}\n\nif [[ $# -ne 4 ]]; then\n  usage\n  exit 2\nfi\n\nif [[ -z \"${GITHUB_TOKEN:-}\" ]]; then\n  echo \"Error: GITHUB_TOKEN is not set\" >&2\n  exit 2\nfi\n\nfor command in curl jq; do\n  if ! command -v \"${command}\" > /dev/null 2>&1; then\n    echo \"Error: required command '${command}' was not found\" >&2\n    exit 2\n  fi\ndone\n\nrepo_owner=$1\nrepo_name=$2\nbase_ref=$3\nhead_ref=$4\nencoded_base_ref=$(printf '%s' \"${base_ref}\" | jq -sRr @uri)\nencoded_head_ref=$(printf '%s' \"${head_ref}\" | jq -sRr @uri)\n\ntemp_dir=$(mktemp -d)\ntrap 'rm -rf \"${temp_dir}\"' EXIT\nauthors_file=\"${temp_dir}/authors\"\ntouch \"${authors_file}\"\nunresolved_commits_file=\"${temp_dir}/unresolved-commits\"\ntouch \"${unresolved_commits_file}\"\n\npage=1\nfetched_commits=0\ntotal_commits=1\n\nwhile (( fetched_commits < total_commits )); do\n  url=\"https://api.github.com/repos/${repo_owner}/${repo_name}/compare/${encoded_base_ref}...${encoded_head_ref}?per_page=100&page=${page}\"\n  response_file=\"${temp_dir}/response-${page}.json\"\n\n  curl --fail --silent --show-error --location \\\n    -H \"Accept: application/vnd.github+json\" \\\n    -H \"Authorization: Bearer ${GITHUB_TOKEN}\" \\\n    -H \"X-GitHub-Api-Version: 2022-11-28\" \\\n    \"${url}\" > \"${response_file}\"\n\n  total_commits=$(jq -er '.total_commits | numbers' \"${response_file}\")\n  page_commits=$(jq -er '.commits | length' \"${response_file}\")\n  jq -r '.commits[].author.login // empty' \"${response_file}\" >> \"${authors_file}\"\n  jq -r '\n    .commits[]\n    | select(.author.login == null)\n    | (.commit.message | split(\"\\n\")[0]) as $title\n    | [\n        .sha,\n        if ($title | test(\"^Merge pull request #[0-9]+\\\\b\")) then\n          ($title | capture(\"^Merge pull request #(?<number>[0-9]+)\\\\b\").number)\n        elif ($title | test(\"\\\\(#[0-9]+\\\\)$\")) then\n          ($title | capture(\"\\\\(#(?<number>[0-9]+)\\\\)$\").number)\n        else\n          \"\"\n        end\n      ]\n    | @tsv\n  ' \\\n    \"${response_file}\" >> \"${unresolved_commits_file}\"\n\n  if (( page_commits == 0 )); then\n    break\n  fi\n\n  fetched_commits=$((fetched_commits + page_commits))\n  page=$((page + 1))\ndone\n\nfailed_commits_file=\"${temp_dir}/failed-commits\"\ntouch \"${failed_commits_file}\"\nwhile IFS=$'\\t' read -r commit_sha pr_number; do\n  [[ -n \"${commit_sha}\" ]] || continue\n  if [[ -z \"${pr_number}\" ]]; then\n    printf '%s\\n' \"${commit_sha}\" >> \"${failed_commits_file}\"\n    continue\n  fi\n  url=\"https://api.github.com/repos/${repo_owner}/${repo_name}/pulls/${pr_number}\"\n  response_file=\"${temp_dir}/pull-${pr_number}.json\"\n\n  if ! curl --fail --silent --show-error --location \\\n    -H \"Accept: application/vnd.github+json\" \\\n    -H \"Authorization: Bearer ${GITHUB_TOKEN}\" \\\n    -H \"X-GitHub-Api-Version: 2022-11-28\" \\\n    \"${url}\" > \"${response_file}\"; then\n    echo \"Error: failed to fetch PR #${pr_number} for commit ${commit_sha}\" >&2\n    exit 1\n  fi\n\n  if login=$(jq -er --arg sha \"${commit_sha}\" '\n    if .merged_at != null\n      and ((.head.sha // \"\") == $sha or (.merge_commit_sha // \"\") == $sha)\n      and (.user.login // \"\") != \"\"\n      then .user.login\n      else empty\n    end\n  ' \"${response_file}\"); then\n    printf '%s\\n' \"${login}\" >> \"${authors_file}\"\n  else\n    printf '%s\\n' \"${commit_sha}\" >> \"${failed_commits_file}\"\n  fi\ndone < \"${unresolved_commits_file}\"\n\nif [[ -s \"${failed_commits_file}\" ]]; then\n  echo \"Error: unable to resolve contributors for these commits:\" >&2\n  while IFS= read -r commit_sha; do\n    echo \"  ${commit_sha}\" >&2\n  done < \"${failed_commits_file}\"\n  exit 1\nfi\n\nLC_ALL=C sort -fu \"${authors_file}\"\n\n<!-- File: .claude/skills/generating-release-notes/scripts/fetch_pr_info.py (Tokens: ~2458 | Category: claude-rule) -->\n#!/usr/bin/env python3\n# Copyright 2026 The Karmada Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"Fetch merged PR metadata and user-facing changes for release notes.\"\"\"\n\nimport argparse\nimport json\nimport os\nimport re\nimport sys\nimport urllib.error\nimport urllib.request\n\n\nGITHUB_API_BASE = \"https://api.github.com\"\nNO_RELEASE_NOTE_VALUES = {\n    \"\",\n    \"n/a\",\n    \"na\",\n    \"no\",\n    \"none\",\n    \"noop\",\n    \"nope\",\n    \"nothing\",\n}\n\n\ndef github_request(url, token, payload=None):\n    \"\"\"Send an authenticated GitHub API request and decode its JSON body.\"\"\"\n    headers = {\n        \"Accept\": \"application/vnd.github.v3+json\",\n        \"User-Agent\": \"Release-Note-Generator\",\n    }\n    if token:\n        headers[\"Authorization\"] = f\"Bearer {token}\"\n\n    data = None\n    if payload is not None:\n        headers[\"Content-Type\"] = \"application/json\"\n        data = json.dumps(payload).encode(\"utf-8\")\n\n    request = urllib.request.Request(url, data=data, headers=headers)\n    try:\n        with urllib.request.urlopen(request) as response:\n            return json.load(response)\n    except urllib.error.HTTPError as error:\n        detail = error.read().decode(\"utf-8\", errors=\"replace\")\n        print(f\"GitHub request failed ({error.code}): {detail}\", file=sys.stderr)\n    except urllib.error.URLError as error:\n        print(f\"GitHub request failed: {error.reason}\", file=sys.stderr)\n    return None\n\n\ndef get_commit_comparison(repo_owner, repo_name, base_tag, head_branch, token):\n    \"\"\"Get the GitHub comparison between the base tag and head branch.\"\"\"\n    base_url = (\n        f\"{GITHUB_API_BASE}/repos/{repo_owner}/{repo_name}/compare/\"\n        f\"{base_tag}...{head_branch}\"\n    )\n    comparison = None\n    commits = []\n    page = 1\n\n    while True:\n        response = github_request(f\"{base_url}?per_page=100&page={page}\", token)\n        if not response:\n            break\n        if comparison is None:\n            comparison = response.copy()\n\n        page_commits = response.get(\"commits\", [])\n        commits.extend(page_commits)\n        total_commits = comparison.get(\"total_commits\", len(commits))\n        if len(commits) >= total_commits or len(page_commits) < 100:\n            break\n        page += 1\n\n    if comparison is not None:\n        comparison[\"commits\"] = commits\n    return comparison\n\n\ndef get_pr_details_batch(repo_owner, repo_name, pr_numbers, token):\n    \"\"\"Fetch pull request title, body, and author in one GraphQL request.\"\"\"\n    if not pr_numbers:\n        return {}\n\n    query_parts = []\n    for index, number in enumerate(pr_numbers):\n        query_parts.append(\n            f\"pr{index}: pullRequest(number: {number}) {{ \"\n            \"number title body author { login } \"\n            \"labels(first: 100) { nodes { name } } }\"\n        )\n    query = (\n        \"{ repository(owner: \\\"\"\n        + repo_owner\n        + \"\\\", name: \\\"\"\n        + repo_name\n        + \"\\\") { \"\n        + \" \".join(query_parts)\n        + \" } }\"\n    )\n    response = github_request(\n        \"https://api.github.com/graphql\", token, {\"query\": query}\n    )\n    if not response:\n        return None\n    if response.get(\"errors\"):\n        print(f\"GraphQL errors: {response['errors']}\", file=sys.stderr)\n        return None\n\n    repository = response.get(\"data\", {}).get(\"repository\", {})\n    result = {}\n    for index, number in enumerate(pr_numbers):\n        pull_request = repository.get(f\"pr{index}\")\n        if pull_request:\n            result[number] = pull_request\n    return result\n\n\ndef normalize_release_note(content):\n    \"\"\"Normalize a candidate release note and reject standard empty values.\"\"\"\n    normalized = re.sub(r\"\\s+\", \" \", content.strip())\n    if normalized.lower() in NO_RELEASE_NOTE_VALUES or len(normalized) <= 3:\n        return None\n    return normalized\n\n\ndef extract_user_facing_change(pr_body):\n    \"\"\"Extract a user-facing change from supported PR template formats.\"\"\"\n    if not pr_body:\n        return None\n\n    release_note_patterns = [\n        r\"```release-note[^\\r\\n]*[\\r\\n]+(.*?)^[ \\t]*```[ \\t]*$\",\n        r\"```release-note\\s*[\\r\\n]+([^\\r\\n]+(?:[\\r\\n]+[^\\r\\n`]+)*)\",\n    ]\n    for pattern in release_note_patterns:\n        match = re.search(pattern, pr_body, re.MULTILINE | re.DOTALL)\n        if match:\n            return normalize_release_note(match.group(1))\n\n    fallback = re.search(\n        r\"\\*\\*Does this PR introduce a user-facing change\\?\\*\\*:\\s*\"\n        r\"```[^\\r\\n]*[\\r\\n]+([\\s\\S]*?)```\",\n        pr_body,\n        re.IGNORECASE,\n    )\n    if fallback:\n        return normalize_release_note(fallback.group(1))\n    return None\n\n\ndef extract_pr_kind_from_labels(labels):\n    \"\"\"Extract PR kinds from kind-prefixed labels.\"\"\"\n    kinds = []\n    for label in (labels or {}).get(\"nodes\", []):\n        name = (label or {}).get(\"name\", \"\")\n        if name.startswith(\"kind/\") and len(name) > len(\"kind/\"):\n            kind = name[len(\"kind/\") :]\n            if kind not in kinds:\n                kinds.append(kind)\n    return kinds or None\n\n\ndef extract_pr_kind_from_body(pr_body):\n    \"\"\"Extract PR kinds from the Karmada pull request template body.\"\"\"\n    if not pr_body:\n        return None\n\n    section = re.search(\n        r\"\\*\\*What type of PR is this\\?\\*\\*(.*?)\"\n        r\"\\*\\*What this PR does / why we need it\\*\\*:\",\n        pr_body,\n        re.DOTALL | re.IGNORECASE,\n    )\n    if not section:\n        return None\n\n    without_comments = re.sub(r\"<!--.*?-->\", \"\", section.group(1), flags=re.DOTALL)\n    kinds = re.findall(\n        r\"^\\s*/kind\\s+([a-zA-Z0-9-]+)\\s*$\", without_comments, re.MULTILINE\n    )\n    return kinds or None\n\n\ndef extract_pr_kind(labels, pr_body):\n    \"\"\"Extract PR kinds from labels, falling back to the PR body.\"\"\"\n    return extract_pr_kind_from_labels(labels) or extract_pr_kind_from_body(pr_body)\n\n\ndef extract_pr_number(commit_message):\n    \"\"\"Extract a PR number from a GitHub merge or squash commit title.\"\"\"\n    title = commit_message.splitlines()[0] if commit_message else \"\"\n    merge_match = re.match(r\"Merge pull request #(\\d+)\\b\", title)\n    if merge_match:\n        return int(merge_match.group(1))\n\n    squash_match = re.search(r\"\\(#(\\d+)\\)$\", title)\n    if squash_match:\n        return int(squash_match.group(1))\n    return None\n\n\ndef comparison_is_truncated(comparison):\n    \"\"\"Report whether the compare response omitted commits from its list.\"\"\"\n    commits = comparison.get(\"commits\", [])\n    return comparison.get(\"total_commits\", len(commits)) > len(commits)\n\n\ndef parse_arguments():\n    \"\"\"Parse command-line arguments.\"\"\"\n    parser = argparse.ArgumentParser(\n        description=\"Fetch merged PR information for release note generation.\"\n    )\n    parser.add_argument(\"base_tag\", help=\"Base tag or branch to compare from\")\n    parser.add_argument(\"head_branch\", help=\"Head tag or branch to compare to\")\n    parser.add_argument(\n        \"--repo\",\n        required=True,\n        help=\"GitHub repository in owner/name format\",\n    )\n    return parser.parse_args()\n\n\ndef main():\n    \"\"\"Fetch and print PRs that contain user-facing changes.\"\"\"\n    args = parse_arguments()\n    if args.repo.count(\"/\") != 1:\n        print(\"Error: --repo must use owner/name format\", file=sys.stderr)\n        return 2\n\n    token = os.getenv(\"GITHUB_TOKEN\")\n    if not token:\n        print(\"Error: GITHUB_TOKEN is not set\", file=sys.stderr)\n        return 2\n\n    repo_owner, repo_name = args.repo.split(\"/\", 1)\n    print(f\"Fetching PR information for {args.repo}...\")\n    print(f\"Comparing {args.base_tag}...{args.head_branch}\")\n    print(\"=\" * 80)\n\n    comparison = get_commit_comparison(\n        repo_owner, repo_name, args.base_tag, args.head_branch, token\n    )\n    if not comparison:\n        print(\"Failed to get commit comparison\", file=sys.stderr)\n        return 1\n\n    commits = comparison.get(\"commits\", [])\n    if comparison_is_truncated(comparison):\n        print(\n            \"Error: GitHub returned only \"\n            f\"{len(commits)} of {comparison['total_commits']} commits; \"\n            \"refusing to generate incomplete release notes\",\n            file=sys.stderr,\n        )\n        return 1\n\n    pr_numbers = set()\n    for commit in commits:\n        message = commit.get(\"commit\", {}).get(\"message\", \"\")\n        pr_number = extract_pr_number(message)\n        if pr_number is not None:\n            pr_numbers.add(pr_number)\n\n    sorted_pr_numbers = sorted(pr_numbers)\n    print(f\"Found {len(commits)} commits\")\n    print(f\"Found {len(sorted_pr_numbers)} merged PRs\")\n    print(\"=\" * 80)\n\n    pr_details = get_pr_details_batch(\n        repo_owner, repo_name, sorted_pr_numbers, token\n    )\n    if pr_details is None:\n        print(\"Failed to get pull request details\", file=sys.stderr)\n        return 1\n\n    release_notes = []\n    for pr_number in sorted_pr_numbers:\n        pull_request = pr_details.get(pr_number)\n        if not pull_request:\n            continue\n        change = extract_user_facing_change(pull_request.get(\"body\") or \"\")\n        if not change:\n            continue\n        author = pull_request.get(\"author\") or {}\n        release_notes.append(\n            {\n                \"number\": pr_number,\n                \"title\": pull_request.get(\"title\", \"\"),\n                \"author\": author.get(\"login\", \"unknown\"),\n                \"kind\": extract_pr_kind(\n                    pull_request.get(\"labels\"), pull_request.get(\"body\") or \"\"\n                ),\n                \"change\": change,\n            }\n        )\n\n    print(\"\\n\" + \"=\" * 80)\n    print(\"SUMMARY OF PRS WITH USER-FACING CHANGES\")\n    print(\"=\" * 80)\n    if release_notes:\n        for release_note in release_notes:\n            print(f\"\\nPR #{release_note['number']} by @{release_note['author']}\")\n            print(f\"Title: {release_note['title']}\")\n            print(f\"Kind: {release_note['kind']}\")\n            print(f\"Change: {release_note['change']}\")\n    else:\n        print(\"No PRs with user-facing changes found.\")\n    print(f\"\\nTotal PRs with user-facing changes: {len(release_notes)}\")\n    return 0\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n\n<!-- File: vendor/github.com/google/gnostic-models/extensions/README.md (Tokens: ~73 | Category: plugin-manifest) -->\n# Extensions\n\n**Extension Support is experimental.**\n\nThis directory contains support code for building Gnostic extensio handlers and\nassociated examples.\n\nExtension handlers can be used to compile vendor or specification extensions\ninto protocol buffer structures.\n\nLike plugins, extension handlers are built as separate executables. Extension\nbodies are written to extension handlers as serialized\nExtensionHandlerRequests.\n</yakaai_skills>"}