mcpField
U
unripe

unripe

本地服务
github.com/glumly开发工具02 天前更新

关于 unripe

unripe 检查锁文件和 GitHub Actions 中版本的发布时间,并可为多种包生态设置版本冷却规则。它支持 CI、pre-commit、Python 和 MCP,让编码 Agent 在添加依赖前检查版本。

主要功能

  1. 01检查依赖版本的发布时间
  2. 02支持多种包生态
  3. 03可配置冷却规则和例外
  4. 04检查 GitHub Actions 引用
  5. 05支持 CI、pre-commit、Python 和 MCP

适用场景

  1. 01在 Pull Request 中拦截新发布版本
  2. 02添加软件包前检查依赖版本时长
  3. 03在多语言仓库统一版本策略

MCP 相关

unripe 可作为 MCP 服务运行,使 AI 编码 Agent 在添加依赖前检查版本发布时间。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

mcp.json · 通用配置
{
"mcpServers": {
"glumly-unripe": {
"command": "uvx",
"args": [
"unripe"
]
}
}
}
启动命令uvx unripe

本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
pypiunripe0.10.2

工具

还没有获取到工具列表

本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。

详细介绍

unripe

PyPI CI License: MIT GitHub Marketplace

unripe 会检查你的锁定文件,如果其中有任何依赖版本的发布时间过近,就会检查失败。默认期限为 3 天。它支持 npm、pnpm、yarn、bun、Deno、PyPI、Cargo、Go、Composer、RubyGems、NuGet、Gradle、Dart 和 Flutter、CocoaPods、Swift、Terraform、Conan、R、Elixir、Nix、Bazel 和 vcpkg,也能检查 GitHub 工作流使用的 Actions。一个包含 Python 后端、JavaScript 前端和 Flutter 应用的仓库,只需一项检查和一套策略,无需在每个工具中分别设置。

它可以在 CI 中运行,作为 pre-commit 钩子运行,从 Python 调用,也可以作为 MCP 服务运行,让 AI 编码 Agent 在添加版本前检查该版本的发布时间。

下面是在一个包含 Deno、Python 和 PHP 部分的小型测试项目中的运行结果,时间为 2026 年 10 月 21 日晚上。uv add 刚刚锁定了一个两天前发布的 certifi 版本:

$ unripe check
unripe: checked 867 versions in 4 lockfiles, minimum age 3d

unripe: 2 versions published less than 3d ago
  composer laravel/framework 12.70.1  13h old, published 2026-10-21 12:55 UTC, old enough at 2026-10-24 12:55 UTC
           php/composer.lock:1
           12.70.0 is old enough (published 2026-10-13)
  pypi     certifi 2026.10.19         47h old, published 2026-10-20 02:24 UTC, old enough at 2026-10-23 02:24 UTC
           api/pylock.toml:8, api/uv.lock:6
           2026.10.18 is old enough (published 2026-10-18)

fail: 2 unripe, 0 unknown, 865 ok, 0 excluded

这两个版本都没有问题。这很正常,大多数新版本都没问题。关键在于,当某个版本出了问题时,你能在别人发现之后,而不是之前得知。而如果某个版本太新,unripe 会告诉你哪个较旧版本符合要求。

为什么要在意

当热门软件包遭到劫持时,恶意版本通常会在几小时内被发现并撤下。2026 年 8 月,一个蠕虫侵入 npm,在约四小时内发布了444 个软件包的恶意版本,窃取所有安装这些版本的用户的 CI 和云凭据。3 月,两个植入后门的 LiteLLM 版本在 PyPI 上存在了约40 分钟到 3 小时。如果推迟几天再采用新版本,这两起事件都能避开。

你可以验证这一说法。下面是一个锁定文件,其中包含 2025 年 9 月 npm 事件中受影响的 debug 和 chalk 版本,检查时间为它们发布两小时后:

$ unripe check --as-of 2025-09-08T15:00Z
unripe: checked 3 versions in 1 lockfile, minimum age 3d

unripe: 2 versions published less than 3d ago
  npm      chalk 5.6.1  1h old, published 2025-09-08 13:13 UTC, old enough at 2025-09-11 13:13 UTC
           package-lock.json:3
           5.6.0 is old enough (published 2025-08-17)
  npm      debug 4.4.2  1h old, published 2025-09-08 13:12 UTC, old enough at 2025-09-11 13:12 UTC
           package-lock.json:2
           4.4.1 is old enough (published 2025-05-13)

fail: 2 unripe, 0 unknown, 1 ok, 0 excluded

如今运行检查时,这两个版本都会被标记为已撤销;npm 就是通过这种方式下架恶意版本的。因此,即使锁定文件仍包含其中一个版本,也会检查失败。

过去一年里,大多数包管理器都新增了相关设置;如果你的包管理器支持,就应该启用。unripe 用于弥补这些设置的不足。它会检查锁定文件中已有的版本,包括由机器人、Agent 或旧工具写入的版本。它只报告拉取请求新增的版本。它会对仓库中的所有生态系统应用同一套策略。它还支持 Go、Composer、NuGet、Gradle、Dart、CocoaPods、Swift、Terraform、Conan、R、Nix、Bazel、vcpkg 和 GitHub Actions,这些目前都还没有相应设置。

docs/case-studies.md 以相同方式回顾了更多事件,docs/threat-model.md 则明确说明了冷却期无法防范哪些风险。

安装

你需要 Python 3.11 或更高版本。unripe 刻意不依赖其他软件包:检查供应链的工具不应该给它增加负担。

uvx unripe check

也可以运行 pipx install unripe 或 pip install unripe。若要一次性创建配置和 GitHub 工作流:

unripe init --github

unripe doctor 会检查设置:找到的配置和锁定文件、每个锁定文件是否可读取,以及它们使用的每个注册表是否有响应。

示例

检查当前目录下的所有内容:

unripe

只检查分支新增的版本,如同拉取请求检查:

unripe check --since origin/main

添加版本前先挑选一个版本。如果没有指定版本,explain 会列出近期发布版本,并标出其中符合最短等待时间的最新版本:

$ unripe explain pypi certifi
pypi certifi: 77 versions, minimum age 3d
  2026.10.19  2026-10-20 02:24 UTC    2d 8h ago  too new, old enough at 2026-10-23 02:24 UTC
  2026.10.18  2026-10-18 16:21 UTC   3d 19h ago  old enough  <- newest old enough
  2026.7.22   2026-07-22 03:35 UTC      13w ago  old enough
  ...

在 .unripe.toml 中,为一个生态系统设置更严格的规则,为类型包设置更宽松的规则:

min-age = "3d"

[min-age-for]
npm = "7d"
"npm:@types/*" = "1d"

自己的包始终视为新包;跳过它们:

unripe check --exclude "npm:@myorg/*"

现在就需要的版本,可放行至指定日期,并在报告中注明原因:

[[allow]]
package = "npm:[email protected]"
reason = "fixes the redirect bug we hit in production"
until = 2026-11-15

供脚本使用的结果:

unripe check --format json | jq -r '.findings[] | select(.state == "unripe") | "\(.name) \(.version)"'

用于 Git 钩子或 cron 作业,除非出错,否则保持安静:

unripe check --quiet

它读取的锁文件

文件发布日期来自
package-lock.json、npm-shrinkwrap.jsonnpm registry
pnpm-lock.yamlnpm registry
yarn.lock(classic 和 berry)npm registry
bun.locknpm registry
deno.locknpm registry 和 jsr.io
uv.lock锁文件自身的上传时间
pylock.toml、pylock.*.toml锁文件自身的上传时间,否则为 PyPI
poetry.lock、Pipfile.lockPyPI
requirements*.txt、constraints*.txtPyPI(仅精确的 == 固定版本)
Cargo.lockcrates.io 稀疏索引
go.modGo 模块代理和校验和数据库
composer.lock锁文件自身的 time 字段,否则为 Packagist
Gemfile.lockrubygems.org
mix.lockHex(hex.pm)
flake.lock(Nix)nixpkgs 发布版本对应的 NixOS 渠道;GitHub 输入对应 GitHub
MODULE.bazel.lock(Bazel 7.2+)Bazel Central Registry
vcpkg.json、vcpkg-configuration.jsonGitHub,用于 microsoft/vcpkg 基线
packages.lock.jsonnuget.org
gradle.lockfile、gradle/dependency-locks/*.lockfileMaven Central、Google 的 Maven 仓库、Gradle 插件门户
pubspec.lock(Dart 和 Flutter)pub.dev
Podfile.lockCocoaPods trunk
Package.resolved(Swift)GitHub 上软件包对应的 GitHub releases
.terraform.lock.hclTerraform registry
conan.lock(Conan 1 和 2)ConanCenter,按配方修订版本
renv.lockCRAN
.github/workflows/*.yml、action.yml(使用 --workflows 时)GitHub releases

来自 git、本地路径、工作区或其他注册表的依赖项会被跳过,因为没有公开信息可供查询。旧版二进制文件 bun.lockb 无法读取;bun 1.2 及更高版本会写入文本格式的 bun.lock。

NuGet 的 packages.lock.json 不记录软件包来自哪个源,因此 unripe 会在 nuget.org 上查询所有软件包。来自私有源的软件包会显示为未知;请将它们排除。对于 Gradle,可以在配置中的 maven-repositories 里添加其他 Maven 仓库(例如 Confluent 的仓库),以查找其中的构件。

对于 Go,它读取的是 go.mod,而不是 go.sum。从 Go 1.17 开始,go.mod 会列出构建中的所有模块,而 go.sum 还会记住未使用的版本。

Swift 软件包的日期计算方式与下文的 GitHub Actions 相同,因此只会检查托管在 GitHub 上的软件包。其他注册表中的 Terraform 提供程序(包括 OpenTofu 的提供程序)会被跳过。Conan 版本的日期取决于锁文件固定的配方修订版本,因为旧版本的新修订意味着新代码;包含用户和渠道的软件包不在 ConanCenter 上,因此会被跳过。

在 flake.lock 中,来自 NixOS 渠道的 nixpkgs,其日期取自渠道发布该版本的时间,这通常比提交日期晚一天。GitHub 上的输入按照 GitHub Actions 的方式计算日期;其他主机、普通 git 服务器和本地路径都会被跳过。MODULE.bazel.lock 会列出 Bazel 在解析时读取的所有模块版本,而 Bazel 会采用每个模块的最高版本,因此检查的就是该版本;如果某个版本已被注册表撤回,则会报告为未知。vcpkg 没有锁文件:清单会固定一个 builtin-baseline,即 microsoft/vcpkg 中每个 port 所用版本的来源提交。项目通过更新基线来采用新版本,因此 unripe 会检查基线提交是否足够旧。

renv.lock 中的软件包从 CRAN 读取,或从提供 CRAN 内容且路径为 /cran/ 的 Posit Package Manager 镜像读取,仓库名称不限。来自 Bioconductor、GitHub、本地路径或其他仓库的软件包会被跳过,因为 CRAN 是 unripe 唯一可以查询的公共注册表。

GitHub Actions

启用 --workflows(或在配置中设置 workflows = true)后,unripe 还会读取工作流和复合操作中的 uses: 行。每个版本都会根据指向同一提交的 GitHub 发布标签确定日期,因此 actions/checkout@v7 和 actions/checkout@3d3c42e... 都会根据该提交的发布时间进行判断。没有发布的提交则使用 提交时间。如果操作使用的是分支(例如 @main),就没有可依据的发布时间,因此结果会标为未知。设置 GITHUB_TOKEN 可突破 GitHub 每小时 60 次请求的限制;在 Action 中会自动为你设置。

提交时间取决于提交作者填写的时间,因此通过此方式判定的版本会在文本报告中标记为:by its commit time,在 JSON 中标记为 "dated_by": "commit"。

在 GitHub Actions 中

name: unripe
on: pull_request

permissions:
  contents: read
  pull-requests: write  # only needed for comment: true

jobs:
  unripe:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: glumly/[email protected]
        with:
          min-age: 3d
          comment: true

对于拉取请求,它只会检查该 PR 相对于基准提交新增的版本。如果检出的提交是浅克隆,它会获取该提交,因此无需设置 fetch-depth: 0。对于推送,它会检查所有内容。

每个过新的版本都会在锁文件对应行上标记,显示在拉取请求的文件视图中。报告会写入作业摘要;设置 comment: true 后,还会在拉取请求中发布评论,让审查者无需打开日志就能看到结果。只会发布一条评论:有过新或未知版本时发布,后续运行会就地更新,不会不断累积。如果作业无法发表评论(例如,拉取请求来自复刻仓库),日志会说明原因,但检查结果不变。

如果可以,请将操作固定到完整的 commit sha,而不是标签。该操作会从自身的检出目录运行 unripe,因此运行的代码就是你固定的提交,不会从 PyPI 下载任何内容。

输入:

输入默认值
min-age3d90m、72h、3d、1w
sincePR 基准提交git 引用,或使用 none 检查所有内容
paths整个仓库文件或目录,以空格或换行分隔
exclude匹配模式,每行一个
unknownfail设置为 warn,即可让未知时长通过
failtrue设置为 false,即可只报告而不使检查失败,供试用
workflowsfalse同时检查工作流使用的操作
annotationstrue在锁文件对应行上标记问题
summarytrue写入作业摘要
sarif同时将 SARIF 写入此路径
commentfalse有过新或未知版本时在拉取请求中发表评论
github-tokengithub.token用于发表评论和确定操作发布时间的令牌
config非默认配置文件

输出:result(pass、fail 或 error)、unripe 和 unknown(版本数量),以及 report 和 json(Markdown 和 JSON 报告的路径)。

如果希望将结果作为代码扫描警报,请生成 SARIF 并上传(这需要 security-events: write):

      - uses: glumly/[email protected]
        with:
          sarif: unripe.sarif
      - uses: github/codeql-action/upload-sarif@v4
        if: always()
        with:
          sarif_file: unripe.sarif

作为 pre-commit 钩子

repos:
  - repo: https://github.com/glumly/unripe
    rev: v0.10.2
    hooks:
      - id: unripe

锁文件变更时会运行。如果只想检查提交新增的内容,请改用 unripe-changed 钩子。

在 GitLab CI 中

include:
  - remote: https://raw.githubusercontent.com/glumly/unripe/v0.10.2/ci/gitlab.yml

这会在 test 阶段添加一个 unripe 作业。对于合并请求,它只会检查该合并请求新增的版本;对于默认分支,它会检查所有内容。发现的问题会显示在合并请求的代码质量小组件和测试报告中,Markdown 报告则会作为作业构件保留。你可以在自己的 variables: 中设置 UNRIPE_MIN_AGE 或 UNRIPE_ARGS(例如 --exclude npm:@myorg/*)来调整设置。

在其他 CI 系统中

docs/ci.md 提供了 Azure Pipelines、Jenkins、CircleCI 和 Bitbucket 的流水线配置。--format junit 适用于上述所有系统,而 --report FORMAT:FILE 会将任意格式的报告写入一个文件,与常规输出并列。docs/bots.md 介绍了 Renovate 和 Dependabot:也为它们设置冷却期,避免它们创建 unripe 会判定为失败的拉取请求。

与 AI 编码 Agent 配合使用

claude mcp add unripe -- uvx [email protected] mcp

unripe mcp 是一个 Model Context Protocol 服务,提供三个只读工具: check_package(查询某个版本的发布时间,或查询已足够旧的最新版本)、check_lockfiles 和 cooldown_settings。它适用于 Claude Code、Codex、VS Code 中的 Copilot、Cursor、Claude Desktop、Windsurf 和 Gemini CLI;docs/agents.md 提供了各自的配置方法,以及一行可添加到 AGENTS.md 的内容,确保 Agent 在添加依赖前使用它。

配置

将设置写入 .unripe.toml,或写入 pyproject.toml 中的 [tool.unripe]。unripe 会从当前目录开始查找,然后逐级向上查找,直到 Git 仓库顶层;也可以读取 UNRIPE_CONFIG 指定的文件。命令行标志优先级更高。两处配置中的排除项会合并。

min-age = "3d"
unknown = "fail"
paths = ["web", "api"]
maven-repositories = ["https://packages.confluent.io/maven"]
exclude = [
  "npm:@myorg/*",
  "[email protected]",
]

# check workflow actions too
workflows = true

# let a version through early when a security advisory names it as the fix
allow-security-fixes = true

# ask your own registries instead of the public ones
[registries]
npm = { url = "https://artifactory.example.com/api/npm/npm-remote", token-env = "NPM_TOKEN" }
pypi = "https://pypi.example.com"

# a different minimum age for an ecosystem or some packages
[min-age-for]
npm = "7d"
"npm:@types/*" = "1d"

# one version let through early, with a reason and an end date
[[allow]]
package = "npm:[email protected]"
reason = "fixes the redirect bug we hit in production"
until = 2026-11-15

排除项可以是软件包名称或 glob。添加生态系统前缀 (npm:、pypi:、cargo:、go:、composer:、rubygems:、nuget:、 maven:、jsr:、actions:、pub:、cocoapods:、swift:、terraform:、 conan:、cran:、hex:、nix:、bazel:、vcpkg:)可将其限定到一个生态系统;添加 @version 可将其限定到特定版本,例如 @types/node@20.*。Gradle 软件包的名称格式为 group:artifact,例如 maven:com.mycorp:*。PyPI 名称会按 PyPI 的规则规范化,因此 Typing_Extensions 和 typing-extensions 是同一个软件包;NuGet 名称不区分大小写。

排除项适用于你永远不想检查的软件包,例如自己的软件包。对于当前需要使用的版本,请改用 [[allow]]。它只会在版本过新时允许该版本通过,并会在报告中将其标记为已允许;until 日期过后,该条目就不再生效。若某个允许条目已不再需要,因为该版本已经超过最低期限,unripe 也会告知你。

[registries] 可将 npm、PyPI、Conan 或 Go 代理指向其他服务,例如 Artifactory、Nexus 或 Verdaccio 镜像。每项可以是一个 URL,也可以是附带 token-env(存放令牌的环境变量名称,会以 bearer token 的形式发送)或 basic-env(存放 user:password 的环境变量名称)的 URL。机密信息本身绝不会写入文件。它只会通过 https 发送到该 URL 的主机和路径,且绝不会跟随重定向到其他主机。以下环境变量的优先级高于文件中的设置,凭据也只会用于设置它们的 URL。unripe config 和 unripe doctor 会说明各变量是否已设置,但不会显示其值。

min-age-for 接受生态系统名称或软件包模式。软件包模式优先于生态系统名称,并使用第一个匹配的模式。

allow-security-fixes 会针对每个过新的版本查询 osv.dev。如果某条公告将该确切版本列为修复版本,则会允许该版本通过,并在报告中注明公告。修复版本之后恰好发布的较新版本不算,恶意软件报告也不算。此功能默认关闭,因为它会为每个过新的版本额外发送一次请求。

unripe config 会显示当前使用的设置及其来源。它还会读取你的包管理器和更新机器人已有的冷却设置(.npmrc、pnpm-workspace.yaml、.yarnrc.yml、bunfig.toml、uv、 Poetry、Deno、Cargo、Bundler、Dependabot 和 Renovate),指出它们与 unripe 不一致之处,并打印可用于补齐缺失设置的配置行。

命令行

unripe check [paths...]
  --min-age AGE             default 3d
  --since REF               only versions added since this git ref
  --exclude PATTERN         can be repeated
  --unknown fail|warn       default fail
  --allow-security-fixes    let through versions an advisory names as the fix
  --workflows               also check GitHub Actions in workflows
  --format FORMAT           text, json, sarif, markdown, github, codequality or junit
  --output FILE             write the report here instead of stdout
  --summary FILE            also append a markdown report, e.g. $GITHUB_STEP_SUMMARY
  --sarif FILE              also write a SARIF report
  --json FILE               also write a JSON report
  --report FORMAT:FILE      also write any other format, can be repeated
  --annotate                also print GitHub annotations
  --as-of TIME              check as if it were then, like 2025-09-08T15:00Z
  --exit-zero               report, but don't fail
  --offline                 use only cached publish times
  --no-suggest              don't look for an older version that passes
  --config FILE
  --no-cache
  --verbose                 list the versions that passed too, and the timing
  --quiet                   print nothing when the check passes

unripe explain ECOSYSTEM NAME [VERSION]
unripe config
unripe doctor [paths...]
unripe init [--github] [--dry-run]
unripe mcp

不带命令运行 unripe 时,会执行 unripe check。所有软件包都已足够旧时,退出码为 0;有软件包过新或发布时间未知时,退出码为 1;无法读取锁文件、未找到锁文件或选项有误时,退出码为 2。JSON 报告的字段说明见 docs/json.md。

explain 用于回答单个问题:

$ unripe explain npm left-pad 1.3.0
npm left-pad 1.3.0: published 2018-04-09 01:10 UTC, 8y ago
old enough: the minimum age is 3d

省略版本号可查看近期发布版本,以及今天可以使用的最新版本:

$ unripe explain composer laravel/framework
composer laravel/framework: 1303 versions, minimum age 3d, 11 prereleases not shown
  12.70.1  2026-10-21 12:55 UTC      13h ago  too new, old enough at 2026-10-24 12:55 UTC
  13.38.0  2026-10-20 10:45 UTC      39h ago  too new, old enough at 2026-10-23 10:45 UTC
  13.37.0  2026-10-16 04:59 UTC   5d 21h ago  old enough  <- newest old enough

值得了解的细节

如果注册表无法告知 unripe 某个版本的发布时间(例如注册表宕机、受到速率限制,或从未听说过该版本),那么该版本会被视为未知,而未知会导致检查失败。如果注册表无法访问时检查却悄悄通过,攻击者就能利用这一点绕过检查。如果你更倾向于不这样做,可以使用 --unknown warn。npm 已撤下的版本也会被视为未知,因为 npm 就是通过这种方式下架恶意版本的。

发布时间在未来超过一小时,就不算有效时间,因此该版本也会被视为未知。

当某个版本过新时,unripe 会在同一发布系列中查找最新的、发布时间足够久远的版本(相同的主版本号;低于 1.0 时则是相同的次版本号),跳过预发布版本以及任何已撤回或已下架的版本,并优先选择未标记为弃用的版本。它会对 npm、PyPI、Cargo、Composer、RubyGems、NuGet、JSR、pub.dev、CocoaPods、Terraform、CRAN 和 Hex 执行此操作。

发布时间不会改变,因此查询结果会永久缓存在磁盘上,位置为 ~/.cache/unripe(Windows 上为 %LOCALAPPDATA%\unripe\cache),或 UNRIPE_CACHE 指定的位置。再次运行并处理相同的锁文件时,不会发送请求,只有遇到过新的版本时才会查找更旧的版本;而 --offline 完全不会发送请求。即使多个锁文件中包含同一版本,该版本也只会查询一次;查询每批并发 16 个,并且每个查询都会在请求之间保持与注册表的连接。对于 npm、Cargo、Composer、RubyGems、JSR、pub.dev、CocoaPods、Terraform、CRAN 和 Hex,每个请求都会涵盖某个软件包的所有版本。GitHub Actions 标签(例如 v7)会移动,因此每次运行都会重新查询。

NuGet 和 Maven 也需要特别处理。nuget.org 会将未列出的版本报告为发布于 1900 年,因此 unripe 会改为从 NuGet 目录读取其实际时间;否则,在发布当天就被列出的版本会看起来已经有 126 年历史。Maven 仓库不会更改已发布的文件,因此 POM 的 Last-Modified 标头就是该版本的发布时间;文件一出现,这个标头就会存在,无需等待搜索索引更新。

还可以通过 UNRIPE_NPM_REGISTRY、UNRIPE_PYPI_URL 和 UNRIPE_CONAN_REMOTE(任何提供 Conan v2 API 的服务),以及 UNRIPE_MAVEN_REPOS(以逗号分隔)设置镜像;后者会像 maven-repositories 一样添加 Maven 仓库。对于 Go,unripe 会使用 GOPROXY 中的第一个代理,然后使用 [registries] 中的代理。 在企业代理之后,HTTPS_PROXY 和 NO_PROXY 会照常生效;也可以通过 SSL_CERT_FILE、REQUESTS_CA_BUNDLE 或 UNRIPE_CA_BUNDLE 指定公司 CA 证书包。遇到证书错误时会直接报告,而不会重试。

Go 需要额外处理。模块代理只会报告版本的提交时间,而提交时间由作者自行设置,因此今天推送的旧提交标签(或故意标注为去年日期的提交)会看起来很旧。因此,unripe 还会查询 Go 校验和数据库 sum.golang.org;它会为每个模块版本分配一个按首次发现顺序递增的编号,而且该编号无法改写。将该编号与 Go 模块索引的时间线对照,就能大致确定该版本首次出现的时间。如果提交所标注的时间更早,unripe 就会以首次出现时间为准。对于较新的版本,估算误差在几分钟以内;对于无法进一步缩小范围的旧版本,则采用无法排除的最短时间。它会遵循 GOSUMDB、GONOSUMDB 和 GOPRIVATE 的设置;私有模块则会回退到提交时间。docs/go.md 说明了其工作原理。

uv.lock、pylock.toml 和 composer.lock 本身包含发布时间,unripe 会信任这些时间。能够编辑锁文件的人也能更改这些时间,但他们也可以让锁文件指向任意软件包,因此 unripe 不会尝试防范这种情况。

内置设置之外

截至 2026 年 10 月:

设置
npm 11.10+.npmrc 中的 min-release-age,单位为天
pnpm 10.16+pnpm-workspace.yaml 中的 minimumReleaseAge,单位为分钟(从 pnpm 11 起默认启用,时长为一天)
yarn 4.10+.yarnrc.yml 中的 npmMinimalAgeGate
bun 1.3+bunfig.toml 中的 minimumReleaseAge,单位为秒
Denodeno.json 中的 minimumDependencyAge(从 Deno 2.9 起默认启用,时长为 24 小时)
uvexclude-newer,可设为日期或类似 7 days 的时长
pip 26.1+--uploaded-prior-to P3D
Poetry 2.4+solver.min-release-age,单位为天
Bundler 4.0.13+bundle config set cooldown 3,单位为天
Hex 2.5.0+mix hex.config cooldown 3d
Cargoregistry.global-min-publish-age,目前不稳定,预计在 Rust 1.100 中稳定
Composer提案中
Go、NuGet、Maven、Gradle、GitHub Actions暂无
Dart、CocoaPods、Swift、Terraform、Conan、R、Nix、Bazel、vcpkg据我们所知,暂无

这些设置会在安装时阻止新版本,而 unripe 无法做到这一点,因此请尽可能使用它们;unripe config 会输出适用于你仓库的配置行。unripe 则是在 CI 中随后运行的检查:针对整个仓库使用统一策略,并检查实际锁定的内容。还有一点区别:某些内置设置在无法找到版本的发布时间时,仍会放行该版本。unripe 则会失败。

cooldowns.dev 收录了更完整且持续更新的设置列表,并附有示例。

在 Python 中使用

import unripe

for finding in unripe.scan(["."], min_age="3d", exclude=["npm:@myorg/*"]):
    if finding.state != "ok":
        print(finding.package, finding.state, finding.reason)

每项发现的状态为 ok、unripe、unknown、allowed 或 excluded。如果无法读取锁文件,或者 since 引用不存在,scan() 会抛出 unripe.check.Problem。

常见问题

我们自己的包总是新的。 将它们排除,例如 npm:@myorg/*。

我们需要一个昨天刚发布的版本。 为那个确切版本添加一条 [[allow]] 条目,并注明原因和 until 日期。如果这是安全修复,设置 allow-security-fixes = true 即可,无需添加条目。

我们使用私有注册表。 在配置中通过 [registries] 指定它;如果需要令牌,可从环境变量读取。代理公共注册表的私有注册表会对所有内容提供响应。如果你的注册表只存放自己的包,则改为将它们排除。

这能替代 npm audit 或类似工具吗? 不能。审计工具了解已报告的漏洞。unripe 对任何包都一无所知。它只是在某个版本发布一段时间之前不信任它。它们能发现不同的问题,因此请同时运行两者。

我们可以先试用而不影响构建吗? 在 Action 中使用 fail: false,或在命令行中使用 --exit-zero。所有结果仍会照常报告。

它运行得慢吗? 不算慢。Discourse 的 Gemfile.lock、Cargo 的 Cargo.lock 和 Vite 的 pnpm-lock.yaml 合计包含 2,048 个版本,未使用缓存时耗时 7 秒,之后耗时三分之一秒。

更多内容

贡献

欢迎提交 Bug 报告和拉取请求。CONTRIBUTING.md 介绍了如何运行测试,以及如何添加新的生态系统;这是目前任何人都能做的最有价值的贡献。

许可证

MIT

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。