@factorialco/specbandit

中文文档
1.5.0发布于 2026/11/13MITTypeScriptES Module构建来源已验证近 30 天下载 14.8万

以 Redis 为工作队列分配测试跑取器 。 将文件路径推向 Redis 列表,然后多位 CI 跑者在原子上会偷取批次,并通过可配置的命令执行.

中文使用文档
文档目录19 节
  1. 安装
  2. 使用量
  3. 1. 将测试文件推给 Redis
  4. 2. 盗窃和从多个工人那里逃跑
  5. CLI 参考
  6. 环境变量
  7. 节点.js API
  8. 示例: GitHub 动作( 基本)
  9. 重运行失败的 CI 任务
  10. 问题
  11. 解决办法是: --key-rerun
  12. 已公布的标记可以防止无声的假通行证
  13. 如何运作:操作模式
  14. 使用重运行支持完成 GitHub 动作示例
  15. 屡次出击: --reset
  16. 失败快取 : --fail-fast
  17. 如何运作
  18. 发展
  19. 许可证

specbandit logo

光谱带

以 Redis 为工作队列分配测试跑取器 。 一个过程将测试文件路径推向Redis列表;多个 CI 跑者在原子上会偷取批次,并通过可配置的命令执行.

CI Job 1 (push):    RPUSH key f1 f2 f3 ... fN  -->  [Redis List]
CI Job 2 (worker):  LPOP key 5  <--  [Redis List]  -->  npx jest
CI Job 3 (worker):  LPOP key 5  <--  [Redis List]  -->  npx jest
CI Job N (worker):  LPOP key 5  <--  [Redis List]  -->  npx jest

LPOP 与一个计数参数(Redis 6.2+)是原子——多个工人同时调用它永远不会收到同一个文件.

这是 TypeScript 端口 光谱带 (鲁比/RSpec). 工人不是在进程中运行RSpec,而是将您提供的任何命令(jest, vitest, node等)以被窃取的文件路径作为参数生成.

安装

npm install @factorialco/specbandit

或者直接运行 :

npx specbandit --help

所需资源: Node.js 18, Redis 6.2 (英语)

使用量

1. 将测试文件推给 Redis

单一的 CI 任务在工人开始前排入所有测试文件路径 .

# Via glob pattern
specbandit push --key pr-123-run-456 --pattern 'test/**/*.test.ts'

# Via stdin pipe (for large file lists or custom filtering)
find test -name '*.test.ts' | specbandit push --key pr-123-run-456

# Via direct arguments (for small lists)
specbandit push --key pr-123-run-456 test/models/user.test.ts test/models/order.test.ts

文件输入优先级 : stdin > -- pattern > 直接参数。 。 。 .

添加 --reset 当制片人可以为同一密钥运行不止一次时:

specbandit push --key pr-123-run-456 --reset --pattern 'test/**/*.test.ts'

见 屡次出击: --reset。 。 。 .

2. 盗窃和从多个工人那里逃跑

每个CI跑者偷取批发并运行. 随你喜欢多少跑步者,他们就会自动分出工作.

specbandit work --key pr-123-run-456 --command "npx jest" --batch-size 10

每个工人循环:

  1. LPOP 来自 Redis (原子) 的 N 文件路径
  2. 将命令与文件路径合并为参数
  3. 重复到队列为空
  4. 如果所有批次都通过,则退出 0, 如果有任何失败,则退出 1

默认情况下,失败的批次会 没有 拦住工人。 它继续偷剩下的工作 所以其他的跑者不会被阻止 等待永远不会被消耗的文件。 为了在第一次失败时停止整个运行,使用 --fail-fast。 。 。 .

CLI 参考

specbandit push [options] [files...]
  --key KEY              Redis queue key (required)
  --pattern PATTERN      Glob pattern for file discovery
  --redis-url URL        Redis URL (default: redis://localhost:6379)
  --key-ttl SECONDS      TTL for all Redis keys (default: 604800 / 1 week)
  --reset                Empty the key before pushing (see below)

specbandit reset [options]
  --key KEY              Redis queue key (required)
  --redis-url URL        Redis URL (default: redis://localhost:6379)

specbandit work [options]
  --key KEY              Redis queue key (required)
  --command CMD          Command to run with file paths (required, e.g. "npx jest")
  --command-opts OPTS    Extra options forwarded to the command (space-separated)
  --batch-size N         Files per batch (default: 5)
  --redis-url URL        Redis URL (default: redis://localhost:6379)
  --key-rerun KEY        Per-runner rerun key for re-run support (see below)
  --key-ttl SECONDS      TTL for all Redis keys (default: 604800 / 1 week)
  --verbose              Show per-batch file list and full command output
  --json-out PATH        Write merged JSON results to file
  --fail-fast            Stop at the first failed batch and empty the shared queue (see below)

环境变量

所有 CLI 选项可以通过环境变量设置:

变量说明默认
SPECBANDIT_KEY重写队列键(必须)
SPECBANDIT_REDIS_URL重做连接 URLredis://localhost:6379
SPECBANDIT_COMMAND要运行的命令(工作所需)
SPECBANDIT_COMMAND_OPTS空格分隔命令选项(无)
SPECBANDIT_BATCH_SIZE每起窃取的文件5
SPECBANDIT_KEY_RERUN每个运行者重运行密钥(无)
SPECBANDIT_KEY_FAILED失败测试文件的 Redis 密钥(无)
SPECBANDIT_KEY_TTL以秒计的所有 Redis 密钥过期604800 (1个星期)
SPECBANDIT_VERBOSE启用动词输出( 1/ true/ 是)false
SPECBANDIT_FAIL_FAST停止于第一批失败( 1/ 是真的/ 是)false
SPECBANDIT_JEST_BATCH_TIMEOUTJest适配器闲置(无进展)超时数秒;这段长时间没有Jest输出的批次被视为挂起并失败. 0 关闭它.600

CLI旗优先于环境变量.

节点.js API

import { Configuration, Publisher, Worker, RedisQueue } from '@factorialco/specbandit'

// Push
const queue = new RedisQueue('redis://my-redis:6379')
const publisher = new Publisher({
  key: 'pr-123-run-456',
  keyTtl: 7200,
  queue,
})
await publisher.publish({ pattern: 'test/**/*.test.ts' })

// Work
const worker = new Worker({
  key: 'pr-123-run-456',
  command: 'npx jest',
  commandOpts: ['--coverage'],
  batchSize: 10,
  queue,
})
const exitCode = await worker.run()

await queue.close()
process.exit(exitCode)

示例: GitHub 动作( 基本)

jobs:
  push-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: |
          npx specbandit push \
            --key "pr-${{ github.event.number }}-${{ github.run_id }}" \
            --redis-url "${{ secrets.REDIS_URL }}" \
            --pattern 'test/**/*.test.ts'
  run-tests:
    runs-on: ubuntu-latest
    needs: push-tests
    strategy:
      matrix:
        runner: [1, 2, 3, 4]
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: |
          npx specbandit work \
            --key "pr-${{ github.event.number }}-${{ github.run_id }}" \
            --redis-url "${{ secrets.REDIS_URL }}" \
            --command "npx jest" \
            --batch-size 10

重运行失败的 CI 任务

问题

当您使用 specbandit 在多个 CI 跑者之间分配测试时( 例如一个 GitHub 动作矩阵, 其中包含 4 个跑者), 每个跑者 偷窃 共享的 Redis 队列中随机的测试文件子集。 分布为非定相.

这造成了CI重跑的问题:

  1. 首发: Runner #3 窃取并执行文件 X, Y, Z. File Y 失败. 共享队列现在是空的 .
  2. 3号跑道重跑: GitHub 动作只重跑失败的跑者. 开始了 specbandit work 同样的 --key,但队列已经空出。 跑步者三号无所事事 0出口 -- -- 测试失败无声通过。 。 。 .

解决办法是: --key-rerun

这个 --key-rerun 旗帜给每个矩阵跑取者在Redis中自己的"记忆". 它可以使光谱带 记录 每一个被执行的跑者的文件,以及 重播 确切地说,那些文件在重新运行.

specbandit work \
  --key "pr-42-run-100" \
  --key-rerun "pr-42-run-100-runner-3" \
            --command "npx jest" \
            --batch-size 10

已公布的标记可以防止无声的假通行证

Redis 一旦最后一个元素被弹出就会删除列表, 所以 a 排水量 队列和 a 队列 从未出版 看起来一模一样 告诉他们, push 设置a 相伴标记键( e)<key>:published)与队列相同的TTL. 标记 超过排出的名单,所以 work 相信这部作品真的已经出版.

如果标记过期( TTL) 或 Redis 在第一次运行和再次运行之间被冲出, work 没有标记和 立即失败 而不是进行零测试和 正在离开 0。 换成旧的 --rerun 旗 -- -- 现在保护是自动的 不需要任意配置.

如何运作:操作模式

work 读取已公布的标记加上共享队列和重运行密钥的状态,然后 沿着这个真相表:

发表共享队列重运行密钥结果
没有任意任意崩溃(出局 1) - 没有发表(从未被推动,或TTL过期).
对空空还好 工人来晚了 已排出队列,无所事事(出局0).
对人口数空摆 经典运行 -- 从共享队列中取出, 记录每批到重运行键 --key-rerun 已经设定.
对空人口数摆 经典重运行 -- 完全重放记录的文件, 忽略共享的队列 .
对人口数人口数摆 全部重运行 - 队列被重排, 而这个跑者仍然持有前一次运行的重运行内存 。 stale再运行的密钥被删除,跑者从共享的队列中窃取,就像经典跑道一样,随行再录.

使用重运行支持完成 GitHub 动作示例

jobs:
  push-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: |
          npx specbandit push \
            --key "pr-${{ github.event.number }}-${{ github.run_id }}" \
            --redis-url "${{ secrets.REDIS_URL }}" \
            --pattern 'test/**/*.test.ts'
  run-tests:
    runs-on: ubuntu-latest
    needs: push-tests
    strategy:
      matrix:
        runner: [1, 2, 3, 4]
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: |
          npx specbandit work \
            --key "pr-${{ github.event.number }}-${{ github.run_id }}" \
            --key-rerun "pr-${{ github.event.number }}-${{ github.run_id }}-runner-${{ matrix.runner }}" \
            --redis-url "${{ secrets.REDIS_URL }}" \
            --command "npx jest" \
            --batch-size 10

屡次出击: --reset

队列密钥由CI运行范围,而非CI尝试范围. 必须是: 重跑一个失败的跑者不会重跑所推动的工作,因此跑者仍然必须找到队列和已公布的标记,这是创建的第一个尝试.

成本是生产者不是一能的。 一个推动然后失败的制片人,或者作为整个工作流程的一部分重新运行的制片人,将工作列表的第二个副本附加到同一个密钥上. 每一个文件然后被排出两次,所以套房运行两次,任何两份到达同一个工人的拷贝都会在一个过程中被装入两次. 对于在模块范围定义常数的测试文件,第二个负载是致命的.

--reset 使推能:

specbandit push --key "pr-42-run-100" --reset --pattern 'test/**/*.test.ts'

队列及其 <key>:published 标记被删除,然后推出工作列表,所以无论制片人运行了多少次,密钥只持有一份副本。 剩下的数字被记录, 一个非零的告诉你, 早先的尝试推了一个名单 没有人消耗:

[specbandit] Reset key 'pr-42-run-100': discarded 4213 queued files from a previous push.

对于单独清理和推进的呼叫者,也有一个独立的命令:

specbandit reset --key "pr-42-run-100"

两者都留下了每个跑者重跑的密钥和失败的密钥,因此一个单跑者重跑仍然可以重放自己的文件. 在共享队列及其重运行密钥中查找数据的跑者是 全部重运行 上表中的大小写,并重排自己的内存.

重置为无重置不是错误 。 无论是哪一个命令都无法在无事可做的情况下清除钥匙:自行丢下标记会让钥匙崩溃的每个工人都成为"从未公布".

失败快取 : --fail-fast

使用 --fail-fast 当一次失败使得其余的跑道无用时,你要CI尽快报告.

specbandit work --key pr-123-run-456 --command "npx jest" --fail-fast

当一个批次失败,工人:

  1. 将批次记录在摘要和 --report 档案,和往常一样.
  2. 删除共享队列列表( E)DEL <key>),所以没有工人可以偷取待处理的文件.
  3. 站出站口1个.

这个 <key>:published 标记留下。 检查排队的工人在prune后看到"被发表但被排出"并出道0,而不是用"从未发表"来崩溃. 一个有批次飞行的工人完成批次,然后发现队列空出并停止. Specbandit在飞行中不会中断一个批次.

斯佩克班迪特每批都看到结果,所以工人在分批失败后停止,而不是在分批失败测试后停止. 也停止在一个批次内, 转发您的测试跑者选项, 例如 -- --bail 为杰斯特.

在回放模式下,工人在第一批失败后停止并跳过所剩的回放文件. 重放从不触碰共享的队列,所以没有什么可推的.

--fail-fast 无法使用 --key-failed。失败的密钥会反馈一个后期的重试通过,并在大多数文件运行之前的故障快速停止,这样该列表就不完整了 。 Specbandit在启动和出站时拒绝组合 1. 一个空的 --key-failed "" 计数为未设置.

如何运作

  • 推开 用途 RPUSH ,然后设定 EXPIRE 键上(默认:1周)。 也设置了 <key>:published 标记相同的TTL,这样工人就可以从从未公布的排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排的排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排出排的排出排出排出排出排出排出排出排出排出.
  • 偷窃 用途 LPOP key count (Redis 6.2+),在原子上会弹出到N元素. 没有卢亚脚本,没有锁,没有种族条件.
  • 记录 (当 --key-rerun 设定 : 每次被窃之后, 批次也是 RPUSHed以相同的TTL重跑键重跑.
  • 重播 (当 --key-rerun 有数据, 共享队列已排出 : 读取重运行密钥的所有文件 LRANGE (无损)分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出,分出分出分出分出分出分出分出分出分出分出,分出分出分出分出分出分出分出分出分出,分出分出分出分出分出分出分出分出出出出出出出出出出分出分出分出分出出出分出分出分出分出分出出分出出出分出分出出分出分出出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出分出.
  • 全部重运行 (当共享队列和重跑密钥都有数据时): stale 重跑密钥被删除 DEL,然后跑者从共享队列中偷取并重新录入经典跑道.
  • 重设 (单位:千美元)push --reset 或者说 specbandit reset)删除队列及其 <key>:published 在单个标记中 DEL,因此,运行两次的制片人不能为工作名单排出两次. 重运行和失败的密钥未受影响 .
  • 运行 将配置的命令通过 child_process.spawnSync() 以文件路径作为参数。 上面没有弹壳扩张.
  • 失败快 (单位:千美元)--fail-fast: 第一组失败后, 共享队列列表被删除 DEL (单位:千美元) <key>:published 标记留下),工人停止.
  • 退出代码 如果每批通过(或队列已经空出),则为0;如果任何批次出现故障,则为1.

发展

npm install
npm test              # unit tests (no Redis needed)
npm run build         # compile TypeScript
npm run typecheck     # type-check without emitting

许可证

麻省理工学院

版本与发布记录

@factorialco/specbandit 共发布 25 个版本,首次发布于 2026/04/09;过去 12 个月发布了 25 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
1.5.0latest2026/11/13今天
1.4.12026/09/161 个月前
1.4.02026/08/123 个月前
1.3.02026/07/293 个月前
1.2.02026/07/173 个月前
1.1.12026/07/163 个月前

关于 @factorialco/specbandit 的常见问题

@factorialco/specbandit 是什么?

以 Redis 为工作队列分配测试跑取器 。 将文件路径推向 Redis 列表,然后多位 CI 跑者在原子上会偷取批次,并通过可配置的命令执行.

如何安装 @factorialco/specbandit?

在项目目录运行 npm install @factorialco/specbandit,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 specbandit,可以用 npx specbandit 直接运行。

@factorialco/specbandit 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@factorialco/specbandit 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@factorialco/specbandit 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@factorialco/specbandit 可以商用吗?

@factorialco/specbandit 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@factorialco/specbandit 的下载量有多少?

根据 npm 官方下载统计,@factorialco/specbandit 在 2026/10/14 – 2026/11/12 这 30 天内被下载 147,707 次,日均约 4,924 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@factorialco/specbandit 还在维护吗?

最新版本 1.5.0 发布于 2026/11/13(今天);至今共发布 25 个版本,其中过去 12 个月发布了 25 个;首次发布于 2026/04/09。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。