From dd5f7e39a86170dd35c6860c854a2b0ef0293b08 Mon Sep 17 00:00:00 2001 From: Kartik Date: Tue, 21 Jul 2026 21:39:00 +0530 Subject: [PATCH] ci: infer component labels for issues filed without the form (#6471) --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .github/ISSUE_TEMPLATE/feature_request.yml | 2 +- .github/advanced-issue-labeler.yml | 4 +- .github/component-keywords.json | 44 +++++++ .github/labeler.yml | 48 ++++++-- .github/scripts/infer-component-labels.js | 44 +++++++ .../scripts/infer-component-labels.test.js | 108 ++++++++++++++++++ .github/workflows/issue-labeler.yml | 41 +++++++ .github/workflows/pr-labeler.yml | 41 ++++--- 9 files changed, 301 insertions(+), 33 deletions(-) create mode 100644 .github/component-keywords.json create mode 100644 .github/scripts/infer-component-labels.js create mode 100644 .github/scripts/infer-component-labels.test.js diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index c2f2a98b5..07c3d45c4 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -12,7 +12,7 @@ body: - Python SDK - TypeScript SDK - Vector Store - - OpenClaw + - Plugin - REST API - Other validations: diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 29277100b..6f373e230 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -12,7 +12,7 @@ body: - Python SDK - TypeScript SDK - Vector Store - - OpenClaw + - Plugin - REST API - Other validations: diff --git a/.github/advanced-issue-labeler.yml b/.github/advanced-issue-labeler.yml index f9c47d023..51b773cb0 100644 --- a/.github/advanced-issue-labeler.yml +++ b/.github/advanced-issue-labeler.yml @@ -9,7 +9,7 @@ policy: keys: ['TypeScript SDK'] - name: 'vector-store' keys: ['Vector Store'] - - name: 'openclaw' - keys: ['OpenClaw'] + - name: 'plugin' + keys: ['Plugin'] - name: 'rest-api' keys: ['REST API'] diff --git a/.github/component-keywords.json b/.github/component-keywords.json new file mode 100644 index 000000000..7b8d1dcda --- /dev/null +++ b/.github/component-keywords.json @@ -0,0 +1,44 @@ +{ + "language": { + "sdk-python": [ + "python", "pip install", "pypi", "pyproject", "requirements.txt", + "from mem0", "import mem0", "traceback", "pydantic", "asyncmemory", + "poetry", "virtualenv", "venv", "conda", "pytest", "async def" + ], + "sdk-typescript": [ + "typescript", "javascript", "pnpm", "yarn", "node.js", "nodejs", + "mem0-ts", "mem0ai/oss", "tsconfig", "await import", + "=> {", "undefined is not" + ] + }, + "area": { + "plugin": [ + "openclaw", "openclaw-mem0", "openclaw.json", "openclaw plugin", + "claude code", "opencode", "pi agent", "mem0-plugin", + "cursor plugin", "codex plugin", "editor plugin" + ], + "openmemory": [ + "openmemory", "open memory", "localhost:8765", "localhost:3000", + "openmemory ui", "openmemory/api", "openmemory/ui" + ], + "cli": ["mem0-cli", "@mem0/cli", "npx mem0", "command line"], + "vector-store": [ + "pgvector", "pinecone", "chroma", "chromadb", "weaviate", + "milvus", "faiss", "vector store", "vectorstore", + "elasticsearch", "supabase", "azure ai search", + "s3 vectors", "mongodb" + ], + "integrations": [ + "vercel ai", "vercel-ai-sdk", "@mem0/vercel-ai-provider", + "llamaindex", "crewai", "autogen", "langgraph" + ], + "rest-api": [ + "rest api", "fastapi", "docker-compose", "/v1/memories", + "localhost:8000", "localhost:8888", "curl -x", "http endpoint" + ], + "documentation": [ + "docs.mem0.ai", "documentation", "typo", "readme", "docstring", "broken link", + "issue on docs", "docs:", "link to the docs page", "issue with current documentation" + ] + } +} diff --git a/.github/labeler.yml b/.github/labeler.yml index e07b9f78a..3a690adf6 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,14 +1,23 @@ -vector-store: - - changed-files: - - any-glob-to-any-file: 'mem0/vector_stores/**' - sdk-python: - changed-files: - - any-glob-to-any-file: ['mem0/**', 'tests/**'] + - any-glob-to-any-file: + - 'mem0/**' + - 'tests/**' + - 'cli/python/**' + - 'pyproject.toml' + - 'poetry.lock' sdk-typescript: - changed-files: - - any-glob-to-any-file: 'mem0-ts/**' + - any-glob-to-any-file: + - 'mem0-ts/**' + - 'cli/node/**' + +vector-store: + - changed-files: + - any-glob-to-any-file: + - 'mem0/vector_stores/**' + - 'mem0-ts/src/oss/src/vector_stores/**' rest-api: - changed-files: @@ -18,22 +27,37 @@ openmemory: - changed-files: - any-glob-to-any-file: 'openmemory/**' -openclaw: - - changed-files: - - any-glob-to-any-file: 'integrations/openclaw/**' - integrations: - changed-files: - any-glob-to-any-file: 'integrations/**' +plugin: + - changed-files: + - all-globs-to-any-file: + - 'integrations/**' + - '!integrations/vercel-ai-sdk/**' + - any-glob-to-any-file: + - 'skills/**' + - '.agents/**' + - '.claude-plugin/**' + - '.codex-plugin/**' + - '.cursor-plugin/**' + - 'marketplace.json' + cli: - changed-files: - any-glob-to-any-file: 'cli/**' documentation: - changed-files: - - any-glob-to-any-file: 'docs/**' + - any-glob-to-any-file: + - 'docs/**' + - 'examples/**' + - '*.md' ci: - changed-files: - - any-glob-to-any-file: '.github/**' + - any-glob-to-any-file: + - '.github/**' + - 'scripts/**' + - '.pre-commit-config.yaml' diff --git a/.github/scripts/infer-component-labels.js b/.github/scripts/infer-component-labels.js new file mode 100644 index 000000000..5b2a7ebd2 --- /dev/null +++ b/.github/scripts/infer-component-labels.js @@ -0,0 +1,44 @@ +const fs = require('fs'); + +function componentLabels(keywords) { + return Object.values(keywords).flatMap(Object.keys); +} + +function toMatcher(term) { + const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const prefix = /^[a-z0-9]/i.test(term) ? '\\b' : ''; + return new RegExp(prefix + escaped, 'i'); +} + +function scoreGroup(text, group) { + let winner = null; + let best = 0; + for (const [label, terms] of Object.entries(group)) { + const score = terms.reduce((n, term) => n + (toMatcher(term).test(text) ? 1 : 0), 0); + if (score > best) { + winner = label; + best = score; + } + } + return winner; +} + +const UMBRELLA = { plugin: 'integrations' }; + +function inferComponentLabels(text, keywords) { + if (!text) return []; + const labels = [scoreGroup(text, keywords.language), scoreGroup(text, keywords.area)].filter( + Boolean, + ); + for (const label of labels.slice()) { + const parent = UMBRELLA[label]; + if (parent && !labels.includes(parent)) labels.push(parent); + } + return labels; +} + +function loadKeywords(file) { + return JSON.parse(fs.readFileSync(file, 'utf8')); +} + +module.exports = { componentLabels, inferComponentLabels, loadKeywords }; diff --git a/.github/scripts/infer-component-labels.test.js b/.github/scripts/infer-component-labels.test.js new file mode 100644 index 000000000..b5eb36259 --- /dev/null +++ b/.github/scripts/infer-component-labels.test.js @@ -0,0 +1,108 @@ +const assert = require('assert'); +const path = require('path'); +const { inferComponentLabels, loadKeywords } = require('./infer-component-labels.js'); + +const keywords = loadKeywords(path.join(__dirname, '..', 'component-keywords.json')); + +const cases = [ + { + number: 6210, + title: "but(anthropic): sampling parameters returns 400 error for new model", + body: "### Component\n\nCore / Python SDK\n\n### Description\n\n### Summary\n\nWhen using Anthropic latest models such as `claude-opus-4-7`, `claude-opus-4-8`, or `claude-sonnet-5`, Mem0 still sends sampling parameters like `temperature` / `top_p`. These models do not support those parameters, causing Anthropic API requests to fail.\n\nSee https://platform.claude.com/docs/en/about-claude/models/migration-guide\n\n### Steps to Reproduce\n\n```python\n from mem0 import Memory\n\n m = Memory.from_config({\n \"llm\": {\n \"provider\": \"anthropic\",\n \"config\": {\n \"model\": \"claude-opus-4-8\",\n \"api_key\": \"your-anthropic-api-key\"\n },\n },\n ...\n })\n```\n\n### Expected Behavior\n\nMem0 should detect Anthropic models that do not support sampling parameters and omit temperature and top_p from the request.\n\nFor models that still support sampling parameters, such as claude-opus-4-6, claude-sonnet-4-6, and claude-haiku-4-5, Mem0 should continue sending supported sampling parameters till they're deprecated.\n\n### Actual Behavior\n\nMem0 includes temperature by default for Anthropic requests. With newer Anthropic models that do not support sampling parameters, the API request fails because unsupported parameters are sent.\n\n### Environment\n\n - mem0 version: 2.0.11\n - Python/Node version: Python 3.11\n - OS: macOS\n", + expected: ["sdk-python"], + }, + { + number: 5770, + title: "feat(ts-sdk): add FastEmbed embedding provider", + body: "## Summary\n\nThe Python SDK supports **FastEmbed** as an embedding provider, but the TypeScript OSS SDK (`mem0ai/oss`) does not. Add it to bring the TS SDK to parity.\n\n| | |\n|---|---|\n| Python reference | `mem0/embeddings/fastembed.py` |\n| Registered in (Python) | `mem0/utils/factory.py` (EmbedderFactory) |\n| Target file (TypeScript) | `mem0-ts/src/oss/src/embeddings/fastembed.ts` |\n| Suggested implementation | Use the `fastembed` npm package (ONNX local embeddings). |\n\n## Requirements\n\n- [ ] Implement `FastEmbedEmbedder` in `mem0-ts/src/oss/src/embeddings/fastembed.ts`, extending `Embedder` (`mem0-ts/src/oss/src/embeddings/base.ts`) and mirroring the Python provider's behavior (embed / embedBatch).\n- [ ] Register the `\"fastembed\"` provider in `mem0-ts/src/oss/src/utils/factory.ts` (EmbedderFactory).\n- [ ] Add config typing in `mem0-ts/src/oss/src/types/`.\n- [ ] Add a unit test under `mem0-ts/src/oss/src/tests/`.\n- [ ] Add `fastembed` to `mem0-ts/package.json` (optional/peer dependency, lazy-imported like other providers).\n- [ ] Update docs under `docs/` if this provider is user-facing.\n\n## Reference pattern\n\nMirror an existing TS provider: `embeddings/openai.ts`.\n\n## Notes\n\n`fastembed` (v2.x) is the JS port of Qdrant's FastEmbed β€” local/offline embeddings. Mirror the default model in `mem0/embeddings/fastembed.py`.\n\n---\n_Part of the TypeScript ↔ Python SDK provider-parity effort. One provider per issue (atomic)._\n", + expected: ["sdk-typescript"], + }, + { + number: 3940, + title: "Milvus database will return distance not similarity score", + body: "### πŸ› Describe the bug\n\nMilvus database will return distance not similarity score\n\n## in milvus.py\n\ndef _parse_output(self, data: list):\n \"\"\"\n Parse the output data.\n\n Args:\n data (Dict): Output data.\n\n Returns:\n List[OutputData]: Parsed output data.\n \"\"\"\n memory = []\n\n for value in data:\n uid, score, metadata = (\n value.get(\"id\"),\n value.get(\"distance\"), # here\n value.get(\"entity\", {}).get(\"metadata\"),\n )\n\n memory_obj = OutputData(id=uid, score=score, payload=metadata)\n memory.append(memory_obj)\n\n return memory\n", + expected: ["vector-store"], + }, + { + number: 5290, + title: "Recall search failed: Bad Request Using OpenAI Embedding Model", + body: "### Component\n\nOpenClaw\n\n### Description\n\n### Summary\nuse openclaw.json config:\n\n```json\n...\n\"embedder\": {\n \"provider\": \"openai\",\n \"config\": {\n \"model\": \"bge-base-zh-v1.5\",\n \"embedding_dims\": 1024,\n \"embeddingDims\": 1024,\n \"url\": \"https://xxxxxxxxx/v1\",\n \"apiKey\": \"xxxxxxxxxxxx\"\n }\n },\n\"vectorStore\": {\n \"provider\": \"qdrant\",\n \"config\": {\n \"url\": \"http://qdrant:6333\",\n \"apiKey\": \"${QDRANT_API_KEY}\",\n \"collectionName\": \"mem0\",\n \"embeddingModelDims\": 1024\n }\n }\n```\n```\n\nopenclaw log info is:\n\n```\n23:14:20 Api key is used with unsecure connection.\n23:14:21 [mem0] Recall search failed: Bad Request\n23:14:21 [plugins] openclaw-mem0: skills-mode recall (strategy=smart) injecting 0 memories (~20 tokens)\n23:14:22 [ws] ⇄ res βœ“ sessions.list 256ms conn=d1eb9bc4…17da id=201b8113…c9dc\n23:14:22 [ws] ⇄ res βœ“ sessions.list 264ms conn=d1eb9bc4…17da id=4939f962…2f16\n23:14:34 [ws] ⇄ res βœ“ sessions.list 250ms conn=d1eb9bc4…17da id=f7ad503f…baa6\n23:15:12 [mem0] **Recall search failed: Bad Request**\n23:15:12 [plugins] openclaw-mem0: skills-mode recall (strategy=smart) injecting 0 memories (~20 tokens)\n23:15:12 [ws] ⇄ res βœ“ sessions.list 288ms conn=d1eb9bc4…17da id=9e20bb86…371e\n23:15:13 [ws] ⇄ res βœ“ sessions.list 268ms conn=d1eb9bc4…17da id=3b49a2ad…7ada\n23:15:20 [ws] ⇄ res βœ“ sessions.list 235ms conn=d1eb9bc4…17da id=a192da30…069f\n```\n\n### Actual Behavior\n\nembedding model response ok,response message has 1024 vectors,but the vectors are submitted to vector-db:qdrant with all zero vectors,and vectors has only 256 size.\n\n```http\nPOST /collections/mem0/points/search HTTP/1.1\nhost: qdrant:6333\nconnection: keep-alive\nuser-agent: qdrant-js/1.13.0\napi-key: xxxxxxxxxxxxxxxxxxxxxxxxxxxx\nContent-Type: application/json\nAccept: application/json\naccept-language: *\nsec-fetch-mode: cors\naccept-encoding: gzip, deflate\ncontent-length: 651\n\n{\"vector\":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],\"limit\":120,\"offset\":0,\"filter\":{\"must\":[{\"key\":\"user_id\",\"match\":{\"value\":\"agent\"}}]},\"with_payload\":true,\"with_vector\":false}\n\n**HTTP/1.1 400 Bad Request**\ntransfer-encoding: chunked\ncontent-type: application/json\nvary: accept-encoding, Origin, Access-Control-Request-Method, Access-Control-Request-Headers\ncontent-encoding: gzip\n\n```\n\n### Expected Behavior\n\nembedding model response ok by tcpdump, response message has 1024 vectors,and this vectors are submitted to vector-db:qdrant with the same vectors,and vectors has also 1024 size.\n\n\n### Environment\n\n- openclaw-mem0 version: 1.0.11\n- qdrant: 1.13.6\n", + expected: ["plugin", "integrations"], + }, + { + number: 3696, + title: "Cannot set expiration_date for memory in REST API server (Docker Compose)", + body: "### πŸ› Describe the bug\n\nI'm using docker compose to deploy a REST API server. When adding memory, I'm unable to set the expiration_date. Is this feature not supported?", + expected: ["rest-api"], + }, + { + number: 3444, + title: "Fix: Openmemory run.sh non-existent vector-store route", + body: "### πŸ› Describe the bug\n\n# Vector_store not implemented\nThere is many references to ` ${NEXT_PUBLIC_API_URL}/api/v1/config/mem0/vector_store` in lines 280, 293, 306, 319, 332, 345, 358, and 371. \n```bash\ncurl -fsS -X PUT \"${NEXT_PUBLIC_API_URL}/api/v1/config/mem0/vector_store\" # Line 280 and for each vector store\n```\nBut the api route is not implemented in `api/app/routers/config.py`.\n# Suggested solution\nI would implement `vector_store` route or remove and use `update_configuration` for all config updates. Also Create class with all config keys for vector_store", + expected: ["openmemory"], + }, + { + number: 6252, + title: "cursor: on_file_read_cursor.sh ignores auto_search / MEM0_AUTO_SEARCH", + body: "### Component\n\nCursor / mem0-plugin\n\n### Description\n\n`on_file_read_cursor.sh` never checks `MEM0_AUTO_SEARCH`. In Claude Code, #6065/#6071 added a guard on `on_file_read.sh`, but the Cursor PreToolUse variant still always calls `file_context.py` (and thus Platform search) once `MEM0_API_KEY` is set.\n\n### Expected\n\nWhen `auto_search: false` / `MEM0_AUTO_SEARCH=false`, `on_file_read_cursor.sh` should exit 0 without searching.\n\n### Actual\n\nTimeline search still runs.\n\n### Related\n\n#6065, #6071, #6250\n", + expected: ["plugin", "integrations"], + }, + { + number: 6032, + title: "docs: fix typos and punctuation errors across docs", + body: "### Description\n\n### Page\nMultiple pages β€” see list below.\n\n### What's Wrong or Missing\n1. https://docs.mem0.ai/components/llms/overview β€” \"a llm\" should be \"an LLM\"\n2. https://docs.mem0.ai/components/vectordbs/dbs/azure β€” 2 comma splices + \"setup\" used as a verb (should be \"set up\")\n3. https://docs.mem0.ai/components/embedders/models/azure_openai β€” \"from the Azure.\" is an incomplete sentence\n4. https://docs.mem0.ai/components/llms/models/azure_openai β€” same incomplete \"from the Azure\" phrasing\n5. https://docs.mem0.ai/cookbooks/companions/voice-companion-openai β€” \"an important information\" (uncountable noun)\n6. https://docs.mem0.ai/cookbooks/essentials/exporting-memories β€” comma splice\n7. https://docs.mem0.ai/cookbooks/integrations/tavily-search β€” \"usecase\" should be \"use case\"\n8. https://docs.mem0.ai/cookbooks/overview β€” broken parallelism in bullet list\n9. README.md β€” \"Github App\" should be \"GitHub App\"\n10. https://docs.mem0.ai/platform/overview β€” table cell not capitalized like other rows\n\n### Suggested Fix\nApply the corrections listed above for each page. I will submit a PR soon addressing all of the issues mentioned.", + expected: ["documentation"], + }, +]; + +const cliRegressionCase = { + number: 3144, + title: "Bug Report: Memory Score Does Not Match Expected Relevance in Local Search", + body: "### πŸ› Describe the bug\n\n#### Description\n\nWhen using the locally deployed `mem0` server, the returned memory `score` from the `search` interface does not align with the expected semantic relevance. In particular, irrelevant or less relevant memories sometimes receive higher scores than directly related ones.\n\n#### Reproduction Steps\n\n```python\nmem0 = mem0_client(mode=\"local\")\nprint(\"Mem0 client initialized successfully.\")\n\nprint(\"Adding memories...\")\nresult = mem0.add(messages=[\n {\"role\": \"user\", \"content\": \"I like drinking coffee in the morning\"},\n {\"role\": \"user\", \"content\": \"I enjoy reading books at night\"}\n], user_id=\"alice\")\nprint(\"Memory added:\", result)\n\nprint(\"Searching memories...\")\nsearch_result = mem0.search(query=\"coffee\", user_id=\"alice\", top_k=2)\nprint(\"Search results:\", search_result)\n```\n\n#### Actual Output\n\n```json\n{\n \"results\": [\n {\n \"id\": \"5099b5be-c673-4f09-99de-a196f43b6476\",\n \"memory\": \"Likes drinking coffee in the morning\",\n \"score\": 0.5115111920687857\n },\n {\n \"id\": \"08df5c51-c52b-4c45-a5b6-b3f864ea149a\",\n \"memory\": \"Enjoys reading books at night\",\n \"score\": 0.7755568273863331\n }\n ],\n \"relations\": [\n {\"source\": \"coffee\", \"relationship\": \"consumed_in\", \"destination\": \"morning\"},\n {\"source\": \"user_id:_alice\", \"relationship\": \"likes\", \"destination\": \"coffee\"},\n {\"source\": \"user_id:_alice\", \"relationship\": \"likes_drinking\", \"destination\": \"coffee\"},\n {\"source\": \"user_id:_alice\", \"relationship\": \"in_time\", \"destination\": \"morning\"},\n {\"source\": \"user_id:_alice\", \"relationship\": \"drinks_in\", \"destination\": \"morning\"}\n ]\n}\n```\n\n#### Expected Behavior\n\nThe memory `\"Likes drinking coffee in the morning\"` should have a **higher score** than `\"Enjoys reading books at night\"` when querying for `\"coffee\"`, since it is directly semantically related.", +}; + +let failures = 0; + +function run(name, fn) { + try { + fn(); + console.log(`PASS ${name}`); + } catch (err) { + failures++; + console.error(`FAIL ${name}: ${err.message}`); + } +} + +for (const { number, title, body, expected } of cases) { + const text = `${title} + +${body}`; + run(`#${number}`, () => { + assert.deepStrictEqual(inferComponentLabels(text, keywords), expected); + }); +} + +run('#3144 cliKeywordPrefixSubstringRegression', () => { + const text = `${cliRegressionCase.title} + +${cliRegressionCase.body}`; + const inferred = inferComponentLabels(text, keywords); + assert.ok(!inferred.includes('cli'), `expected 'cli' absent (body contains 'Mem0 client', a substring of the removed 'mem0 cli' term), got ${JSON.stringify(inferred)}`); +}); + +run('noKeywordMatchReturnsEmptyArray', () => { + const text = 'The weather today is sunny and I went for a walk in the park with my dog.'; + assert.deepStrictEqual(inferComponentLabels(text, keywords), []); +}); + +run('emptyStringReturnsEmptyArray', () => { + assert.deepStrictEqual(inferComponentLabels('', keywords), []); +}); + +if (failures > 0) { + console.error(` +${failures} test(s) failed.`); + process.exit(1); +} +console.log(` +All ${cases.length + 3} tests passed.`); diff --git a/.github/workflows/issue-labeler.yml b/.github/workflows/issue-labeler.yml index fc6dd70d5..f747e4784 100644 --- a/.github/workflows/issue-labeler.yml +++ b/.github/workflows/issue-labeler.yml @@ -16,11 +16,52 @@ jobs: - uses: stefanbuck/github-issue-parser@v3 id: issue-parser + continue-on-error: true with: template-path: .github/ISSUE_TEMPLATE/bug_report.yml - uses: redhat-plumbers-in-action/advanced-issue-labeler@v3 + continue-on-error: true with: issue-form: ${{ steps.issue-parser.outputs.jsonString }} token: ${{ secrets.GITHUB_TOKEN }} config-path: .github/advanced-issue-labeler.yml + + - name: Infer component from text when the form was not used + uses: actions/github-script@v7 + with: + script: | + const { + componentLabels, + inferComponentLabels, + loadKeywords, + } = require(`${process.env.GITHUB_WORKSPACE}/.github/scripts/infer-component-labels.js`); + + const { data: issue } = await github.rest.issues.get({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const keywords = loadKeywords(`${process.env.GITHUB_WORKSPACE}/.github/component-keywords.json`); + const known = componentLabels(keywords); + const existing = issue.labels.map((label) => label.name || label); + if (existing.some((name) => known.includes(name))) { + core.info(`Component label already present: ${existing.join(', ')}`); + return; + } + + const labels = inferComponentLabels(`${issue.title}\n\n${issue.body || ''}`, keywords); + + if (labels.length === 0) { + core.info('No component could be inferred from the issue text'); + return; + } + + core.info(`Inferred: ${labels.join(', ')}`); + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels, + }); diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml index 3635abe49..236f66780 100644 --- a/.github/workflows/pr-labeler.yml +++ b/.github/workflows/pr-labeler.yml @@ -4,6 +4,10 @@ on: pull_request_target: types: [opened, synchronize, reopened, edited] +concurrency: + group: pr-labeler-${{ github.event.pull_request.number }} + cancel-in-progress: true + permissions: contents: read pull-requests: write @@ -22,31 +26,34 @@ jobs: with: script: | const allowed = new Set([ - 'sdk-python', 'sdk-typescript', 'vector-store', 'openclaw', + 'sdk-python', 'sdk-typescript', 'vector-store', 'plugin', 'rest-api', 'openmemory', 'documentation', 'ci', 'cli', 'integrations', ]); - const body = context.payload.pull_request.body || ''; - const matches = [...body.matchAll(/\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s*:?\s*(?:#|https:\/\/github\.com\/mem0ai\/mem0\/issues\/)(\d+)/gi)]; - const numbers = [...new Set(matches.map((m) => Number(m[1])))]; + const umbrella = { plugin: 'integrations' }; + const { repository } = await github.graphql( + `query ($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $number) { + closingIssuesReferences(first: 20) { + nodes { labels(first: 50) { nodes { name } } } + } + } + } + }`, + { owner: context.repo.owner, repo: context.repo.repo, number: context.issue.number }, + ); const labels = new Set(); - for (const issue_number of numbers) { - let issue; - try { - ({ data: issue } = await github.rest.issues.get({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number, - })); - } catch { - continue; - } - if (issue.pull_request) continue; - for (const label of issue.labels) { + for (const issue of repository.pullRequest.closingIssuesReferences.nodes) { + for (const label of issue.labels.nodes) { if (allowed.has(label.name)) labels.add(label.name); } } + for (const label of [...labels]) { + if (umbrella[label]) labels.add(umbrella[label]); + } + if (labels.size > 0) { await github.rest.issues.addLabels({ owner: context.repo.owner,