fastretrieval 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. fastretrieval-1.0.0/.coderabbit.yaml +23 -0
  2. fastretrieval-1.0.0/.github/CODEOWNERS +2 -0
  3. fastretrieval-1.0.0/.github/ISSUE_TEMPLATE/bug_report.md +34 -0
  4. fastretrieval-1.0.0/.github/ISSUE_TEMPLATE/feature_request.md +19 -0
  5. fastretrieval-1.0.0/.github/PULL_REQUEST_TEMPLATE.md +25 -0
  6. fastretrieval-1.0.0/.github/review-learnings.md +14 -0
  7. fastretrieval-1.0.0/.github/rulesets/main.json +63 -0
  8. fastretrieval-1.0.0/.github/workflows/bot-governance.yml +99 -0
  9. fastretrieval-1.0.0/.github/workflows/cd.yml +355 -0
  10. fastretrieval-1.0.0/.github/workflows/ci.yml +174 -0
  11. fastretrieval-1.0.0/.github/workflows/codeql.yml +30 -0
  12. fastretrieval-1.0.0/.github/workflows/convert.yml +71 -0
  13. fastretrieval-1.0.0/.github/workflows/integration.yml +184 -0
  14. fastretrieval-1.0.0/.github/workflows/opencode.yml +71 -0
  15. fastretrieval-1.0.0/.github/workflows/pr-title.yml +59 -0
  16. fastretrieval-1.0.0/.gitignore +74 -0
  17. fastretrieval-1.0.0/.infisical.json +5 -0
  18. fastretrieval-1.0.0/.mise.toml +41 -0
  19. fastretrieval-1.0.0/.pre-commit-config.yaml +89 -0
  20. fastretrieval-1.0.0/.skret.yaml +8 -0
  21. fastretrieval-1.0.0/AGENTS.md +175 -0
  22. fastretrieval-1.0.0/CHANGELOG.md +1698 -0
  23. fastretrieval-1.0.0/CLAUDE.md +124 -0
  24. fastretrieval-1.0.0/CODE_OF_CONDUCT.md +46 -0
  25. fastretrieval-1.0.0/CONTRIBUTING.md +21 -0
  26. fastretrieval-1.0.0/Dockerfile +143 -0
  27. fastretrieval-1.0.0/LICENSE +201 -0
  28. fastretrieval-1.0.0/NOTICE +13 -0
  29. fastretrieval-1.0.0/PKG-INFO +460 -0
  30. fastretrieval-1.0.0/README.md +423 -0
  31. fastretrieval-1.0.0/SECURITY.md +15 -0
  32. fastretrieval-1.0.0/fastretrieval/__init__.py +37 -0
  33. fastretrieval-1.0.0/fastretrieval/common/__init__.py +0 -0
  34. fastretrieval-1.0.0/fastretrieval/common/custom_model.py +120 -0
  35. fastretrieval-1.0.0/fastretrieval/common/model_description.py +60 -0
  36. fastretrieval-1.0.0/fastretrieval/common/model_management.py +883 -0
  37. fastretrieval-1.0.0/fastretrieval/common/onnx_model.py +273 -0
  38. fastretrieval-1.0.0/fastretrieval/common/preprocessor_utils.py +74 -0
  39. fastretrieval-1.0.0/fastretrieval/common/types.py +25 -0
  40. fastretrieval-1.0.0/fastretrieval/common/utils.py +180 -0
  41. fastretrieval-1.0.0/fastretrieval/contract/__init__.py +36 -0
  42. fastretrieval-1.0.0/fastretrieval/contract/manifest.py +168 -0
  43. fastretrieval-1.0.0/fastretrieval/contract/model.py +203 -0
  44. fastretrieval-1.0.0/fastretrieval/contract/preprocessor.py +129 -0
  45. fastretrieval-1.0.0/fastretrieval/convert/__init__.py +40 -0
  46. fastretrieval-1.0.0/fastretrieval/convert/__main__.py +7 -0
  47. fastretrieval-1.0.0/fastretrieval/convert/card.py +100 -0
  48. fastretrieval-1.0.0/fastretrieval/convert/cli.py +212 -0
  49. fastretrieval-1.0.0/fastretrieval/convert/gguf.py +174 -0
  50. fastretrieval-1.0.0/fastretrieval/convert/heads.py +103 -0
  51. fastretrieval-1.0.0/fastretrieval/convert/manifest.py +42 -0
  52. fastretrieval-1.0.0/fastretrieval/convert/modal_backend.py +231 -0
  53. fastretrieval-1.0.0/fastretrieval/convert/onnx.py +230 -0
  54. fastretrieval-1.0.0/fastretrieval/convert/profiles.py +378 -0
  55. fastretrieval-1.0.0/fastretrieval/convert/requirements.txt +10 -0
  56. fastretrieval-1.0.0/fastretrieval/convert/verify.py +360 -0
  57. fastretrieval-1.0.0/fastretrieval/export.py +68 -0
  58. fastretrieval-1.0.0/fastretrieval/models/__init__.py +14 -0
  59. fastretrieval-1.0.0/fastretrieval/models/image/__init__.py +4 -0
  60. fastretrieval-1.0.0/fastretrieval/models/image/image_embedding.py +83 -0
  61. fastretrieval-1.0.0/fastretrieval/models/image/image_embedding_base.py +45 -0
  62. fastretrieval-1.0.0/fastretrieval/models/image/onnx_embedding.py +168 -0
  63. fastretrieval-1.0.0/fastretrieval/models/image/onnx_image_model.py +186 -0
  64. fastretrieval-1.0.0/fastretrieval/models/image/transform/__init__.py +5 -0
  65. fastretrieval-1.0.0/fastretrieval/models/image/transform/functional.py +216 -0
  66. fastretrieval-1.0.0/fastretrieval/models/image/transform/operators.py +491 -0
  67. fastretrieval-1.0.0/fastretrieval/models/late_interaction/__init__.py +6 -0
  68. fastretrieval-1.0.0/fastretrieval/models/late_interaction/colbert.py +315 -0
  69. fastretrieval-1.0.0/fastretrieval/models/late_interaction/late_interaction_embedding_base.py +82 -0
  70. fastretrieval-1.0.0/fastretrieval/models/late_interaction/late_interaction_text_embedding.py +180 -0
  71. fastretrieval-1.0.0/fastretrieval/models/late_interaction/token_embeddings.py +84 -0
  72. fastretrieval-1.0.0/fastretrieval/models/late_interaction_multimodal/__init__.py +6 -0
  73. fastretrieval-1.0.0/fastretrieval/models/late_interaction_multimodal/colpali.py +219 -0
  74. fastretrieval-1.0.0/fastretrieval/models/late_interaction_multimodal/late_interaction_multimodal_embedding.py +115 -0
  75. fastretrieval-1.0.0/fastretrieval/models/late_interaction_multimodal/late_interaction_multimodal_embedding_base.py +57 -0
  76. fastretrieval-1.0.0/fastretrieval/models/late_interaction_multimodal/onnx_multimodal_model.py +281 -0
  77. fastretrieval-1.0.0/fastretrieval/models/sparse/__init__.py +5 -0
  78. fastretrieval-1.0.0/fastretrieval/models/sparse/sparse_embedding_base.py +86 -0
  79. fastretrieval-1.0.0/fastretrieval/models/sparse/sparse_text_embedding.py +131 -0
  80. fastretrieval-1.0.0/fastretrieval/models/sparse/splade_pp.py +187 -0
  81. fastretrieval-1.0.0/fastretrieval/parallel_processor.py +318 -0
  82. fastretrieval-1.0.0/fastretrieval/preprocessing/__init__.py +6 -0
  83. fastretrieval-1.0.0/fastretrieval/preprocessing/graph.py +39 -0
  84. fastretrieval-1.0.0/fastretrieval/preprocessing/image.py +53 -0
  85. fastretrieval-1.0.0/fastretrieval/py.typed +1 -0
  86. fastretrieval-1.0.0/fastretrieval/rerank/__init__.py +3 -0
  87. fastretrieval-1.0.0/fastretrieval/rerank/cross_encoder/__init__.py +0 -0
  88. fastretrieval-1.0.0/fastretrieval/rerank/cross_encoder/custom_text_cross_encoder.py +69 -0
  89. fastretrieval-1.0.0/fastretrieval/rerank/cross_encoder/gguf_cross_encoder.py +247 -0
  90. fastretrieval-1.0.0/fastretrieval/rerank/cross_encoder/onnx_text_cross_encoder.py +211 -0
  91. fastretrieval-1.0.0/fastretrieval/rerank/cross_encoder/onnx_text_model.py +217 -0
  92. fastretrieval-1.0.0/fastretrieval/rerank/cross_encoder/qwen3_cross_encoder.py +324 -0
  93. fastretrieval-1.0.0/fastretrieval/rerank/cross_encoder/text_cross_encoder.py +210 -0
  94. fastretrieval-1.0.0/fastretrieval/rerank/cross_encoder/text_cross_encoder_base.py +64 -0
  95. fastretrieval-1.0.0/fastretrieval/text/__init__.py +3 -0
  96. fastretrieval-1.0.0/fastretrieval/text/custom_text_embedding.py +124 -0
  97. fastretrieval-1.0.0/fastretrieval/text/gguf_embedding.py +211 -0
  98. fastretrieval-1.0.0/fastretrieval/text/onnx_embedding.py +188 -0
  99. fastretrieval-1.0.0/fastretrieval/text/onnx_text_model.py +188 -0
  100. fastretrieval-1.0.0/fastretrieval/text/pooled_embedding.py +68 -0
  101. fastretrieval-1.0.0/fastretrieval/text/pooled_normalized_embedding.py +60 -0
  102. fastretrieval-1.0.0/fastretrieval/text/qwen3_embedding.py +197 -0
  103. fastretrieval-1.0.0/fastretrieval/text/text_embedding.py +263 -0
  104. fastretrieval-1.0.0/fastretrieval/text/text_embedding_base.py +76 -0
  105. fastretrieval-1.0.0/pyproject.toml +102 -0
  106. fastretrieval-1.0.0/renovate.json +151 -0
  107. fastretrieval-1.0.0/scripts/clean-venv.mjs +112 -0
  108. fastretrieval-1.0.0/scripts/enforce-commit.sh +8 -0
  109. fastretrieval-1.0.0/scripts/export_optimized_reranker.py +221 -0
  110. fastretrieval-1.0.0/scripts/preserve-diacritics.py +290 -0
  111. fastretrieval-1.0.0/scripts/repo-bootstrap/_lib.py +533 -0
  112. fastretrieval-1.0.0/scripts/repo-bootstrap/verify_readme_sync.py +512 -0
  113. fastretrieval-1.0.0/scripts/test_preserve_diacritics.py +180 -0
  114. fastretrieval-1.0.0/tests/__init__.py +0 -0
  115. fastretrieval-1.0.0/tests/convert/__init__.py +0 -0
  116. fastretrieval-1.0.0/tests/convert/fixtures/qwen3/config.json +6 -0
  117. fastretrieval-1.0.0/tests/convert/fixtures/tiny-e5/config.json +7 -0
  118. fastretrieval-1.0.0/tests/convert/test_card.py +64 -0
  119. fastretrieval-1.0.0/tests/convert/test_cli.py +73 -0
  120. fastretrieval-1.0.0/tests/convert/test_gguf.py +46 -0
  121. fastretrieval-1.0.0/tests/convert/test_heads.py +32 -0
  122. fastretrieval-1.0.0/tests/convert/test_manifest.py +239 -0
  123. fastretrieval-1.0.0/tests/convert/test_modal_backend.py +80 -0
  124. fastretrieval-1.0.0/tests/convert/test_onnx.py +61 -0
  125. fastretrieval-1.0.0/tests/convert/test_verify.py +109 -0
  126. fastretrieval-1.0.0/tests/test_colpali.py +97 -0
  127. fastretrieval-1.0.0/tests/test_common_custom_model.py +95 -0
  128. fastretrieval-1.0.0/tests/test_contract_preprocessor.py +69 -0
  129. fastretrieval-1.0.0/tests/test_cross_encoder_onnx.py +857 -0
  130. fastretrieval-1.0.0/tests/test_custom_cross_encoder.py +113 -0
  131. fastretrieval-1.0.0/tests/test_custom_model.py +217 -0
  132. fastretrieval-1.0.0/tests/test_custom_reranker_registry.py +124 -0
  133. fastretrieval-1.0.0/tests/test_custom_text_cross_encoder.py +95 -0
  134. fastretrieval-1.0.0/tests/test_custom_text_embedding.py +372 -0
  135. fastretrieval-1.0.0/tests/test_env_compat.py +20 -0
  136. fastretrieval-1.0.0/tests/test_export.py +80 -0
  137. fastretrieval-1.0.0/tests/test_gguf_cross_encoder.py +577 -0
  138. fastretrieval-1.0.0/tests/test_gguf_embedding.py +484 -0
  139. fastretrieval-1.0.0/tests/test_image_embedding.py +97 -0
  140. fastretrieval-1.0.0/tests/test_integration.py +629 -0
  141. fastretrieval-1.0.0/tests/test_integration_gguf.py +203 -0
  142. fastretrieval-1.0.0/tests/test_integration_q4f16.py +197 -0
  143. fastretrieval-1.0.0/tests/test_last_token_pool_robustness.py +61 -0
  144. fastretrieval-1.0.0/tests/test_late_interaction.py +16 -0
  145. fastretrieval-1.0.0/tests/test_model_contract.py +114 -0
  146. fastretrieval-1.0.0/tests/test_model_description.py +69 -0
  147. fastretrieval-1.0.0/tests/test_model_management.py +2066 -0
  148. fastretrieval-1.0.0/tests/test_model_management_extra.py +362 -0
  149. fastretrieval-1.0.0/tests/test_onnx_embedding.py +184 -0
  150. fastretrieval-1.0.0/tests/test_onnx_embedding_shapes.py +30 -0
  151. fastretrieval-1.0.0/tests/test_onnx_model.py +447 -0
  152. fastretrieval-1.0.0/tests/test_package_identity.py +29 -0
  153. fastretrieval-1.0.0/tests/test_parallel_processor.py +819 -0
  154. fastretrieval-1.0.0/tests/test_pooled_embedding.py +105 -0
  155. fastretrieval-1.0.0/tests/test_pooled_normalized_embedding.py +83 -0
  156. fastretrieval-1.0.0/tests/test_pooling.py +94 -0
  157. fastretrieval-1.0.0/tests/test_preprocessing_graph.py +37 -0
  158. fastretrieval-1.0.0/tests/test_preprocessor_utils.py +187 -0
  159. fastretrieval-1.0.0/tests/test_qwen3_embedding.py +96 -0
  160. fastretrieval-1.0.0/tests/test_qwen3_reranker.py +336 -0
  161. fastretrieval-1.0.0/tests/test_security_limits.py +111 -0
  162. fastretrieval-1.0.0/tests/test_sparse_embedding.py +21 -0
  163. fastretrieval-1.0.0/tests/test_text_cross_encoder.py +56 -0
  164. fastretrieval-1.0.0/tests/test_text_cross_encoder_base.py +44 -0
  165. fastretrieval-1.0.0/tests/test_text_embedding.py +131 -0
  166. fastretrieval-1.0.0/tests/test_text_embedding_base.py +124 -0
  167. fastretrieval-1.0.0/tests/test_text_embedding_embed.py +52 -0
  168. fastretrieval-1.0.0/tests/test_text_onnx_text_model.py +745 -0
  169. fastretrieval-1.0.0/tests/test_types.py +69 -0
  170. fastretrieval-1.0.0/tests/test_utils.py +261 -0
  171. fastretrieval-1.0.0/uv.lock +1106 -0
@@ -0,0 +1,23 @@
1
+ language: "en-US"
2
+ reviews:
3
+ auto_review:
4
+ enabled: true
5
+ drafts: false
6
+ ignore_usernames:
7
+ - "n24q02m"
8
+ - "dependabot[bot]"
9
+ - "renovate[bot]"
10
+ - "github-actions[bot]"
11
+ - "devin-ai-integration[bot]"
12
+ - "google-labs-jules[bot]"
13
+ profile: "chill"
14
+ request_changes_workflow: false
15
+ high_level_summary: true
16
+ auto_incremental_review: true
17
+ issue_enrichment:
18
+ auto_enrich:
19
+ enabled: true
20
+ planning:
21
+ enabled: true
22
+ chat:
23
+ auto_reply: false
@@ -0,0 +1,2 @@
1
+ # Default code owner — required reviewer for all PRs
2
+ * @n24q02m
@@ -0,0 +1,34 @@
1
+ ---
2
+ name: Bug report
3
+ about: Create a report to help us improve
4
+ title: ''
5
+ labels: ''
6
+ assignees: ''
7
+ ---
8
+
9
+ **Describe the bug**
10
+ A clear and concise description of what the bug is.
11
+
12
+ **To Reproduce**
13
+ Steps to reproduce the behavior:
14
+
15
+ 1. Go to '...'
16
+ 2. Click on '....'
17
+ 3. Scroll down to '....'
18
+ 4. See error
19
+
20
+ **Expected behavior**
21
+ A clear and concise description of what you expected to happen.
22
+
23
+ **Screenshots**
24
+ If applicable, add screenshots to help explain your problem.
25
+
26
+ **Environment (please complete the following information):**
27
+
28
+ - OS: [e.g. macOS, Linux, Windows]
29
+ - Go version: [output of `go version`]
30
+ - fastretrieval version: [output of `fastretrieval --version`]
31
+ - Installation method: [e.g. go install, Docker, brew, scoop, apt, direct binary]
32
+
33
+ **Additional context**
34
+ Add any other context about the problem here.
@@ -0,0 +1,19 @@
1
+ ---
2
+ name: Feature request
3
+ about: Suggest an idea for this project
4
+ title: ''
5
+ labels: ''
6
+ assignees: ''
7
+ ---
8
+
9
+ **Is your feature request related to a problem? Please describe.**
10
+ A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
11
+
12
+ **Describe the solution you'd like**
13
+ A clear and concise description of what you want to happen.
14
+
15
+ **Describe alternatives you've considered**
16
+ A clear and concise description of any alternative solutions or features you've considered.
17
+
18
+ **Additional context**
19
+ Add any other context or screenshots about the feature request here.
@@ -0,0 +1,25 @@
1
+ ## Description
2
+ <!-- Describe what this PR does and why -->
3
+
4
+ ## Changes
5
+ <!-- List the key changes -->
6
+ -
7
+
8
+ ## Type of Change
9
+ - [ ] Bug fix (non-breaking change which fixes an issue)
10
+ - [ ] New feature (non-breaking change which adds functionality)
11
+ - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
12
+
13
+ ## Testing
14
+ - [ ] Existing tests pass
15
+ - [ ] Added new tests for the changes
16
+ - [ ] Tested manually
17
+
18
+ ## Screenshots
19
+ <!-- If applicable, add screenshots to help explain your changes -->
20
+
21
+ ## Checklist
22
+ - [ ] My code follows the project's coding standards
23
+ - [ ] I have commented my code where necessary
24
+ - [ ] I have updated documentation if needed
25
+ - [ ] My changes generate no new warnings
@@ -0,0 +1,14 @@
1
+ # Review learnings
2
+
3
+ Binding rules for the automated reviewer. It reads this file before every review
4
+ and before answering an issue, and it must obey every rule here.
5
+
6
+ Add a rule whenever the reviewer gives feedback that turns out to be wrong for
7
+ this codebase, so it never repeats that feedback. One rule per bullet, scoped by
8
+ path when it only applies in one place.
9
+
10
+ Format: `` `path/glob` `` — the rule, and the reason it exists.
11
+
12
+ ## Rules
13
+
14
+ _None recorded yet._
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "Main Branch Rules",
3
+ "target": "branch",
4
+ "source_type": "Repository",
5
+ "source": "n24q02m/fastretrieval",
6
+ "enforcement": "active",
7
+ "bypass_actors": [
8
+ {
9
+ "actor_id": 5,
10
+ "actor_type": "RepositoryRole",
11
+ "bypass_mode": "always"
12
+ },
13
+ {
14
+ "actor_id": 3200052,
15
+ "actor_type": "Integration",
16
+ "bypass_mode": "always"
17
+ }
18
+ ],
19
+ "conditions": {
20
+ "ref_name": {
21
+ "exclude": [],
22
+ "include": [
23
+ "~DEFAULT_BRANCH"
24
+ ]
25
+ }
26
+ },
27
+ "rules": [
28
+ {
29
+ "type": "deletion"
30
+ },
31
+ {
32
+ "type": "non_fast_forward"
33
+ },
34
+ {
35
+ "type": "required_linear_history"
36
+ },
37
+ {
38
+ "type": "pull_request",
39
+ "parameters": {
40
+ "required_approving_review_count": 0,
41
+ "dismiss_stale_reviews_on_push": true,
42
+ "require_code_owner_review": false,
43
+ "require_last_push_approval": false,
44
+ "required_review_thread_resolution": false,
45
+ "allowed_merge_methods": [
46
+ "squash"
47
+ ]
48
+ }
49
+ },
50
+ {
51
+ "type": "code_scanning",
52
+ "parameters": {
53
+ "code_scanning_tools": [
54
+ {
55
+ "tool": "CodeQL",
56
+ "security_alerts_threshold": "high_or_higher",
57
+ "alerts_threshold": "errors"
58
+ }
59
+ ]
60
+ }
61
+ }
62
+ ]
63
+ }
@@ -0,0 +1,99 @@
1
+ name: Bot PR Governance
2
+
3
+ on:
4
+ pull_request:
5
+ types:
6
+ - opened
7
+
8
+ permissions:
9
+ pull-requests: write
10
+ contents: read
11
+
12
+ jobs:
13
+ governance:
14
+ name: Bot PR Rate Limit & Duplicate Check
15
+ # Internal PRs only. This repo is public, so `pull_request` gives a fork a
16
+ # read-only GITHUB_TOKEN that the `permissions:` block above cannot raise --
17
+ # `gh pr close` would then fail under `set -e` and abort the job. Closing an
18
+ # outside contributor's PR because someone else spent the rate limit is the
19
+ # wrong outcome anyway; the bots this governs push their branches to this
20
+ # repo, so fork PRs are never in scope. Comparing full_name rather than
21
+ # head.repo.fork keeps this correct if this repository is ever itself a
22
+ # fork, where head.repo.fork would be true for internal branches too.
23
+ if: github.event.pull_request.head.repo.full_name == github.repository
24
+ runs-on: ubuntu-latest
25
+ steps:
26
+ - name: Detect and govern bot PRs
27
+ env:
28
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29
+ BRANCH: ${{ github.head_ref }}
30
+ ACTOR: ${{ github.actor }}
31
+ PR_NUMBER: ${{ github.event.pull_request.number }}
32
+ PR_TITLE: ${{ github.event.pull_request.title }}
33
+ REPO: ${{ github.repository }}
34
+ run: |
35
+ set -e
36
+
37
+ # Detect if this is a bot-created PR by branch name pattern.
38
+ # Two signals:
39
+ # 1. Explicit bot-tool prefixes (Bolt, Palette, Jules, fix/web-scraper)
40
+ # 2. Any branch ending with a long numeric task/trace ID (-[0-9]{14,})
41
+ IS_BOT_PR=false
42
+ if echo "$BRANCH" | grep -qE \
43
+ '^(bolt[-/]|palette-ux-|fix/web-scraper-|jules[-/])'; then
44
+ IS_BOT_PR=true
45
+ elif echo "$BRANCH" | grep -qE -- \
46
+ '-[0-9]{14,}$'; then
47
+ IS_BOT_PR=true
48
+ fi
49
+
50
+ if [ "$IS_BOT_PR" = "false" ]; then
51
+ echo "Not a bot PR ($BRANCH), skipping governance checks."
52
+ exit 0
53
+ fi
54
+
55
+ echo "Bot PR detected on branch: $BRANCH"
56
+ gh pr edit "$PR_NUMBER" --add-label "bot-generated" --repo "$REPO" || true
57
+
58
+ # --- Rate limit: max 5 bot PRs/actor/day ---
59
+ SINCE=$(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || \
60
+ date -u -v-24H +%Y-%m-%dT%H:%M:%SZ)
61
+ COUNT=$(gh pr list --repo "$REPO" --state open \
62
+ --json createdAt,headRefName,author \
63
+ --jq "[.[] |
64
+ select(.author.login == \"$ACTOR\") |
65
+ select(.createdAt > \"$SINCE\") |
66
+ select(.headRefName | test(
67
+ \"^(bolt[-/]|palette-ux-|fix/web-scraper-|jules[-/])\" +
68
+ \"|-[0-9]{14,}$\"
69
+ ))] | length" 2>/dev/null || echo 0)
70
+
71
+ echo "Bot PRs by $ACTOR in last 24h: $COUNT"
72
+
73
+ if [ "${COUNT:-0}" -gt 5 ]; then
74
+ gh pr close "$PR_NUMBER" --repo "$REPO" \
75
+ --comment "**Bot rate limit exceeded**: $COUNT PRs created in the last 24 hours (limit: 5/day). This PR has been automatically closed to reduce PR backlog and CI resource waste. Please consolidate related fixes."
76
+ echo "PR #$PR_NUMBER closed: rate limit exceeded ($COUNT/5 today)"
77
+ exit 0
78
+ fi
79
+
80
+ # --- Duplicate detection by title keyword ---
81
+ KEYWORD=$(echo "$PR_TITLE" | sed 's/[^a-zA-Z0-9 _-]//g' | \
82
+ tr '[:upper:]' '[:lower:]' | cut -c1-50 | xargs)
83
+ if [ -z "$KEYWORD" ]; then
84
+ echo "No keyword extracted, skipping duplicate check."
85
+ exit 0
86
+ fi
87
+
88
+ DUPES=$(gh pr list --repo "$REPO" --state open --search "$KEYWORD" \
89
+ --json number \
90
+ --jq "[.[] | select(.number != $PR_NUMBER)] | length" \
91
+ 2>/dev/null || echo 0)
92
+
93
+ echo "Similar open PRs for '$KEYWORD': $DUPES"
94
+ if [ "${DUPES:-0}" -ge 1 ]; then
95
+ gh pr edit "$PR_NUMBER" --repo "$REPO" \
96
+ --add-label "possible-duplicate" || true
97
+ gh pr comment "$PR_NUMBER" --repo "$REPO" \
98
+ --body "**Possible duplicate**: found ${DUPES} open PR(s) with similar title ('${KEYWORD}'). Check before merging: \`gh pr list --search '${KEYWORD}' --state open\`"
99
+ fi
@@ -0,0 +1,355 @@
1
+ # =============================================================================
2
+ # CD Template: Python. Publish steps are opt-in per repo via
3
+ # vars.PUBLISH_PYPI / vars.PUBLISH_DOCKER / vars.PUBLISH_MCP_REGISTRY.
4
+ # Managed by n24q02m repo-bootstrap; local changes may be overwritten.
5
+ #
6
+ # Pipeline: PSR v10 -> PyPI -> Docker multi-arch (amd64+arm64) -> MCP Registry
7
+ # CUSTOMIZE: Search for "# CUSTOMIZE:" comments and adjust for your repo.
8
+ # =============================================================================
9
+ name: CD
10
+
11
+ on:
12
+ workflow_dispatch:
13
+ inputs:
14
+ release_type:
15
+ description: "Release type"
16
+ required: true
17
+ type: choice
18
+ options:
19
+ - beta
20
+ - stable
21
+
22
+ permissions:
23
+ contents: read
24
+
25
+ env:
26
+ DOCKERHUB_IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/fastretrieval
27
+ GHCR_IMAGE: ghcr.io/${{ github.repository }}
28
+
29
+ concurrency:
30
+ group: release
31
+ cancel-in-progress: false
32
+
33
+ jobs:
34
+ # ============================================================================
35
+ # README registry metadata: the package carries the repo's README to PyPI, so
36
+ # a manifest that names a file which is not there publishes an empty project
37
+ # page. Runs before the release rather than beside it -- a PyPI upload cannot
38
+ # be undone, only yanked.
39
+ # ============================================================================
40
+ readme-sync:
41
+ name: Verify README registry metadata
42
+ runs-on: ubuntu-latest
43
+ steps:
44
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
45
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
46
+ with:
47
+ python-version: "3.13"
48
+ - name: Verify README registry metadata
49
+ run: python scripts/repo-bootstrap/verify_readme_sync.py --repo-root=.
50
+
51
+ # ============================================================================
52
+ # Release: PSR v10
53
+ # ============================================================================
54
+ release:
55
+ name: Semantic Release
56
+ runs-on: ubuntu-latest
57
+ outputs:
58
+ released: ${{ steps.release.outputs.released }}
59
+ tag: ${{ steps.release.outputs.tag }}
60
+ version: ${{ steps.release.outputs.version }}
61
+ is_prerelease: ${{ steps.release.outputs.is_prerelease }}
62
+ steps:
63
+ - name: Harden Runner
64
+ uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
65
+ with:
66
+ egress-policy: audit
67
+
68
+ - name: Generate GitHub App Token
69
+ id: app-token
70
+ uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
71
+ with:
72
+ app-id: ${{ vars.CI_APP_ID }}
73
+ private-key: ${{ secrets.CI_APP_KEY }}
74
+
75
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
76
+ with:
77
+ token: ${{ steps.app-token.outputs.token }}
78
+ fetch-depth: 0
79
+
80
+ # Version bump runs on the fork (orphan-tag + registry-collision guards).
81
+ # The publish step below stays upstream — the fork does not replace it.
82
+ - uses: n24q02m/better-semantic-release@9b8ee6275b7dae995bac090282ccb1cabc99ae0f # v1.2.3
83
+ id: release
84
+ with:
85
+ github_token: ${{ steps.app-token.outputs.token }}
86
+ prerelease: ${{ inputs.release_type == 'beta' }}
87
+ prerelease_token: beta
88
+
89
+ - uses: python-semantic-release/publish-action@5a5718ce47b892ef699f2972dae122297771d641 # v10.6.1
90
+ if: steps.release.outputs.released == 'true'
91
+ with:
92
+ github_token: ${{ steps.app-token.outputs.token }}
93
+ tag: ${{ steps.release.outputs.tag }}
94
+
95
+ # ============================================================================
96
+ # Notify Downstream
97
+ # ============================================================================
98
+ # A core/shared repo cutting a stable release has to open a tracking issue in
99
+ # every repo that pins it, or downstream pins drift silently. The dispatch goes
100
+ # to one hub repo that fans the issues out; `n24q02m/n24q02m` is that hub as a
101
+ # literal, copied from the repo this template was measured against rather than
102
+ # derived from `n24q02m` -- the hub happening to share the owner's name is not
103
+ # a rule this template gets to assume.
104
+ #
105
+ # Stable only: `is_prerelease != 'true'`. A beta cutting downstream issues would
106
+ # file the same issue again at stable.
107
+ #
108
+ # NOTIFY_DOWNSTREAM, because that rule binds CORE repos and this template lands
109
+ # on every Python repo. Ungated, a non-core repo in this stack would dispatch
110
+ # `core-update` on a stable release with client_payload[core] naming itself.
111
+ # Two measurements say that is latent rather than visible: the hub repo has
112
+ # exactly one workflow (opencode.yml) and no `repository_dispatch` trigger
113
+ # anywhere, so nothing handles `core-update` today; and the job this was
114
+ # copied from is `skipped` in 10 of its 10 recorded CD runs. A job that has
115
+ # never once executed is not evidence that it is safe to fan out -- it is a
116
+ # wrong fan-out waiting for the day a handler is added.
117
+ notify-downstream:
118
+ name: Notify Downstream
119
+ needs: [release]
120
+ if: >-
121
+ needs.release.outputs.released == 'true'
122
+ && needs.release.outputs.is_prerelease != 'true'
123
+ && vars.NOTIFY_DOWNSTREAM == 'true'
124
+ runs-on: ubuntu-latest
125
+ steps:
126
+ - name: Harden Runner
127
+ uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
128
+ with:
129
+ egress-policy: audit
130
+
131
+ - name: Generate GitHub App Token
132
+ id: app-token
133
+ uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
134
+ with:
135
+ app-id: ${{ vars.CI_APP_ID }}
136
+ private-key: ${{ secrets.CI_APP_KEY }}
137
+
138
+ - name: Dispatch core-update
139
+ env:
140
+ GH_TOKEN: ${{ steps.app-token.outputs.token }}
141
+ run: |
142
+ gh api repos/n24q02m/n24q02m/dispatches \
143
+ -f event_type=core-update \
144
+ -f 'client_payload[core]=fastretrieval' \
145
+ -f 'client_payload[version]=${{ needs.release.outputs.version }}' \
146
+ -f 'client_payload[tag]=${{ needs.release.outputs.tag }}' \
147
+ -f 'client_payload[release_url]=https://github.com/${{ github.repository }}/releases/tag/${{ needs.release.outputs.tag }}'
148
+
149
+ # ============================================================================
150
+ # Publish to PyPI
151
+ # ============================================================================
152
+ publish-pypi:
153
+ name: Publish to PyPI
154
+ needs: [readme-sync, release]
155
+ if: needs.release.outputs.released == 'true' && vars.PUBLISH_PYPI == 'true'
156
+ runs-on: ubuntu-latest
157
+ environment: pypi
158
+ permissions:
159
+ contents: read
160
+ id-token: write
161
+ steps:
162
+ - name: Harden Runner
163
+ uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
164
+ with:
165
+ egress-policy: audit
166
+
167
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
168
+ with:
169
+ ref: ${{ needs.release.outputs.tag }}
170
+ - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
171
+ - run: uv build
172
+ - name: Publish to PyPI
173
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
174
+
175
+ # ============================================================================
176
+ # Build Docker (multi-arch: amd64 + arm64)
177
+ # ============================================================================
178
+ build-docker:
179
+ name: Build Docker (${{ matrix.platform }})
180
+ # readme-sync, because this job pushes an image to a registry and
181
+ # merge-docker then publishes ./README.md as the Docker Hub description.
182
+ # The gate is only a gate if it runs BEFORE the push.
183
+ needs: [readme-sync, release]
184
+ if: needs.release.outputs.released == 'true' && vars.PUBLISH_DOCKER == 'true'
185
+ permissions:
186
+ contents: read
187
+ packages: write
188
+ strategy:
189
+ fail-fast: false
190
+ matrix:
191
+ include:
192
+ - platform: linux/amd64
193
+ runner: ubuntu-latest
194
+ artifact: linux-amd64
195
+ - platform: linux/arm64
196
+ runner: ubuntu-24.04-arm
197
+ artifact: linux-arm64
198
+ runs-on: ${{ matrix.runner }}
199
+ steps:
200
+ - name: Harden Runner
201
+ uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
202
+ with:
203
+ egress-policy: audit
204
+
205
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
206
+ with:
207
+ ref: ${{ needs.release.outputs.tag }}
208
+ - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
209
+ - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
210
+ with:
211
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
212
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
213
+ - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
214
+ with:
215
+ registry: ghcr.io
216
+ username: ${{ github.actor }}
217
+ password: ${{ secrets.GITHUB_TOKEN }}
218
+ - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
219
+ id: build
220
+ with:
221
+ context: .
222
+ platforms: ${{ matrix.platform }}
223
+ outputs: type=image,"name=${{ env.DOCKERHUB_IMAGE }},${{ env.GHCR_IMAGE }}",push-by-digest=true,name-canonical=true,push=true
224
+ cache-from: type=gha,scope=${{ github.ref_name }}-${{ matrix.artifact }}
225
+ cache-to: type=gha,mode=max,scope=${{ github.ref_name }}-${{ matrix.artifact }}
226
+ - run: |
227
+ mkdir -p ${{ runner.temp }}/digests
228
+ echo "${{ steps.build.outputs.digest }}" > "${{ runner.temp }}/digests/${{ matrix.artifact }}"
229
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
230
+ with:
231
+ name: digest-${{ matrix.artifact }}
232
+ path: ${{ runner.temp }}/digests/*
233
+ retention-days: 1
234
+
235
+ # ============================================================================
236
+ # Merge Docker Manifests
237
+ # ============================================================================
238
+ merge-docker:
239
+ name: Merge Docker Manifests
240
+ needs: [release, build-docker]
241
+ if: needs.release.outputs.released == 'true' && vars.PUBLISH_DOCKER == 'true'
242
+ runs-on: ubuntu-latest
243
+ permissions:
244
+ contents: read
245
+ packages: write
246
+ steps:
247
+ - name: Harden Runner
248
+ uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
249
+ with:
250
+ egress-policy: audit
251
+
252
+ - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
253
+ with:
254
+ pattern: digest-*
255
+ path: ${{ runner.temp }}/digests
256
+ merge-multiple: true
257
+ - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
258
+ with:
259
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
260
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
261
+ - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
262
+ with:
263
+ registry: ghcr.io
264
+ username: ${{ github.actor }}
265
+ password: ${{ secrets.GITHUB_TOKEN }}
266
+ - name: Create and push manifests
267
+ env:
268
+ VERSION: ${{ needs.release.outputs.version }}
269
+ IS_PRERELEASE: ${{ needs.release.outputs.is_prerelease }}
270
+ run: |
271
+ if [ "$IS_PRERELEASE" = "true" ]; then
272
+ TAGS="beta ${VERSION}"
273
+ else
274
+ MAJOR=$(echo "$VERSION" | cut -d. -f1)
275
+ MINOR=$(echo "$VERSION" | cut -d. -f1-2)
276
+ TAGS="latest ${VERSION} ${MINOR} ${MAJOR}"
277
+ fi
278
+
279
+ for IMAGE in "$DOCKERHUB_IMAGE" "$GHCR_IMAGE"; do
280
+ tag_args=()
281
+ for TAG in $TAGS; do
282
+ tag_args+=("-t" "${IMAGE}:${TAG}")
283
+ done
284
+
285
+ digest_args=()
286
+ for digest_file in "${{ runner.temp }}/digests/"*; do
287
+ while IFS= read -r digest; do
288
+ if [ -n "$digest" ]; then
289
+ digest_args+=("${IMAGE}@${digest}")
290
+ fi
291
+ done < "$digest_file"
292
+ done
293
+
294
+ docker buildx imagetools create "${tag_args[@]}" "${digest_args[@]}"
295
+ done
296
+
297
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
298
+ with:
299
+ sparse-checkout: README.md
300
+ - uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v5.0.0
301
+ with:
302
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
303
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
304
+ # CUSTOMIZE: DockerHub repository name
305
+ repository: ${{ env.DOCKERHUB_IMAGE }}
306
+ short-description: ${{ github.event.repository.description }}
307
+ readme-filepath: ./README.md
308
+
309
+ # ============================================================================
310
+ # Publish to MCP Registry (stable only)
311
+ # ============================================================================
312
+ publish-mcp-registry:
313
+ name: Publish to MCP Registry
314
+ needs: [release, publish-pypi, merge-docker]
315
+ if: >-
316
+ needs.release.outputs.released == 'true'
317
+ && needs.release.outputs.is_prerelease != 'true'
318
+ && vars.PUBLISH_PYPI == 'true'
319
+ && vars.PUBLISH_DOCKER == 'true'
320
+ && vars.PUBLISH_MCP_REGISTRY == 'true'
321
+ runs-on: ubuntu-latest
322
+ permissions:
323
+ contents: read
324
+ id-token: write
325
+ steps:
326
+ - name: Harden Runner
327
+ uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
328
+ with:
329
+ egress-policy: audit
330
+
331
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
332
+ with:
333
+ ref: ${{ needs.release.outputs.tag }}
334
+
335
+ - name: Update server.json version
336
+ run: |
337
+ VERSION="${{ needs.release.outputs.version }}"
338
+ jq --arg v "$VERSION" '
339
+ .version = $v |
340
+ .packages = [.packages[] |
341
+ if .registryType == "oci" then del(.version)
342
+ else .version = $v
343
+ end
344
+ ]
345
+ ' server.json > server.json.tmp && mv server.json.tmp server.json
346
+
347
+ - name: Install MCP Publisher
348
+ run: |
349
+ curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher
350
+
351
+ - name: Login to MCP Registry
352
+ run: ./mcp-publisher login github-oidc
353
+
354
+ - name: Publish to MCP Registry
355
+ run: ./mcp-publisher publish