@1aboveio/skills 1.0.7 → 1.0.9

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 (224) hide show
  1. package/.codex-plugin/plugin.json +13 -0
  2. package/.codex-plugin/resources.json +1827 -0
  3. package/README.md +22 -0
  4. package/package.json +9 -2
  5. package/skills/app-dev/airflow/SKILL.md +1 -1
  6. package/skills/app-dev/airflow/release-check.mjs +3 -1
  7. package/skills/app-dev/android-foundation/SKILL.md +92 -34
  8. package/skills/app-dev/android-foundation/assets/android-ci-job.yml +13 -7
  9. package/skills/app-dev/android-foundation/references/ci.md +23 -13
  10. package/skills/app-dev/android-foundation/references/delivery-focus.md +29 -0
  11. package/skills/app-dev/android-foundation/references/diagnosis.md +136 -0
  12. package/skills/app-dev/android-foundation/references/gotchas.md +20 -11
  13. package/skills/app-dev/android-foundation/references/setup.md +15 -6
  14. package/skills/app-dev/android-foundation/release-check.mjs +3 -1
  15. package/skills/app-dev/android-foundation/scripts/device-run.sh +148 -42
  16. package/skills/app-dev/app-debug/SKILL.md +1 -1
  17. package/skills/app-dev/app-debug/release-check.mjs +3 -1
  18. package/skills/app-dev/better-auth/SKILL.md +1 -1
  19. package/skills/app-dev/better-auth/release-check.mjs +3 -1
  20. package/skills/app-dev/data-catalog/SKILL.md +1 -1
  21. package/skills/app-dev/data-catalog/release-check.mjs +3 -1
  22. package/skills/app-dev/data-quality/SKILL.md +1 -1
  23. package/skills/app-dev/data-quality/release-check.mjs +3 -1
  24. package/skills/app-dev/ios-foundation/SKILL.md +1 -1
  25. package/skills/app-dev/ios-foundation/release-check.mjs +3 -1
  26. package/skills/app-dev/monorepo/SKILL.md +1 -1
  27. package/skills/app-dev/monorepo/release-check.mjs +3 -1
  28. package/skills/app-dev/nextjs-fullstack/SKILL.md +1 -1
  29. package/skills/app-dev/nextjs-fullstack/release-check.mjs +3 -1
  30. package/skills/app-dev/prisma-setup/SKILL.md +1 -1
  31. package/skills/app-dev/prisma-setup/release-check.mjs +3 -1
  32. package/skills/app-dev/pyspark/SKILL.md +1 -1
  33. package/skills/app-dev/pyspark/release-check.mjs +3 -1
  34. package/skills/app-dev/python-backend/SKILL.md +1 -1
  35. package/skills/app-dev/python-backend/release-check.mjs +3 -1
  36. package/skills/app-dev/shadcn/SKILL.md +1 -1
  37. package/skills/app-dev/shadcn/release-check.mjs +3 -1
  38. package/skills/app-dev/zod-v4/SKILL.md +1 -1
  39. package/skills/app-dev/zod-v4/release-check.mjs +3 -1
  40. package/skills/cicd/cloud-build/SKILL.md +1 -1
  41. package/skills/cicd/cloud-build/release-check.mjs +3 -1
  42. package/skills/cicd/cloud-debug/SKILL.md +1 -1
  43. package/skills/cicd/cloud-debug/release-check.mjs +3 -1
  44. package/skills/cicd/cloud-deploy/SKILL.md +1 -1
  45. package/skills/cicd/cloud-deploy/release-check.mjs +3 -1
  46. package/skills/cicd/docker/SKILL.md +1 -1
  47. package/skills/cicd/docker/release-check.mjs +3 -1
  48. package/skills/cicd/github-actions/SKILL.md +1 -1
  49. package/skills/cicd/github-actions/release-check.mjs +3 -1
  50. package/skills/cicd/github-flow/SKILL.md +1 -1
  51. package/skills/cicd/github-flow/release-check.mjs +3 -1
  52. package/skills/cicd/google-cloud/SKILL.md +1 -1
  53. package/skills/cicd/google-cloud/release-check.mjs +3 -1
  54. package/skills/cicd/mergify/SKILL.md +1 -1
  55. package/skills/cicd/mergify/release-check.mjs +3 -1
  56. package/skills/cicd/podman/SKILL.md +1 -1
  57. package/skills/cicd/podman/release-check.mjs +3 -1
  58. package/skills/content/article-review/SKILL.md +1 -1
  59. package/skills/content/article-review/release-check.mjs +3 -1
  60. package/skills/content/article-to-image/SKILL.md +1 -1
  61. package/skills/content/article-to-image/release-check.mjs +3 -1
  62. package/skills/content/flowchart-svg/SKILL.md +1 -1
  63. package/skills/content/flowchart-svg/release-check.mjs +3 -1
  64. package/skills/content/image-gen/SKILL.md +1 -1
  65. package/skills/content/image-gen/release-check.mjs +3 -1
  66. package/skills/content/mermaid/SKILL.md +1 -1
  67. package/skills/content/mermaid/release-check.mjs +3 -1
  68. package/skills/content/wechat-article-writer/SKILL.md +1 -1
  69. package/skills/content/wechat-article-writer/release-check.mjs +3 -1
  70. package/skills/content/wechat-publisher/SKILL.md +1 -1
  71. package/skills/content/wechat-publisher/release-check.mjs +3 -1
  72. package/skills/content/wechat-search/SKILL.md +1 -1
  73. package/skills/content/wechat-search/release-check.mjs +3 -1
  74. package/skills/content/xai-search/SKILL.md +88 -13
  75. package/skills/content/xai-search/release-check.mjs +3 -1
  76. package/skills/content/xai-search/scripts/xai_search.sh +125 -149
  77. package/skills/core/distill-writing/SKILL.md +1 -1
  78. package/skills/core/distill-writing/release-check.mjs +3 -1
  79. package/skills/core/doc-to-md/SKILL.md +1 -1
  80. package/skills/core/doc-to-md/release-check.mjs +3 -1
  81. package/skills/core/editorial-review/SKILL.md +1 -1
  82. package/skills/core/editorial-review/release-check.mjs +3 -1
  83. package/skills/core/house-style/SKILL.md +1 -1
  84. package/skills/core/house-style/release-check.mjs +3 -1
  85. package/skills/core/plain-language/SKILL.md +1 -1
  86. package/skills/core/plain-language/release-check.mjs +3 -1
  87. package/skills/core/polish-cn/SKILL.md +1 -1
  88. package/skills/core/polish-cn/release-check.mjs +3 -1
  89. package/skills/core/reg-parser/SKILL.md +1 -1
  90. package/skills/core/reg-parser/release-check.mjs +3 -1
  91. package/skills/core/terminology/SKILL.md +1 -1
  92. package/skills/core/terminology/release-check.mjs +3 -1
  93. package/skills/core/write-doc/SKILL.md +1 -1
  94. package/skills/core/write-doc/release-check.mjs +3 -1
  95. package/skills/engineering/better-review/SKILL.md +1 -1
  96. package/skills/engineering/better-review/release-check.mjs +3 -1
  97. package/skills/engineering/contributing/SKILL.md +1 -1
  98. package/skills/engineering/contributing/release-check.mjs +3 -1
  99. package/skills/engineering/e2e-test/SKILL.md +1 -1
  100. package/skills/engineering/e2e-test/release-check.mjs +3 -1
  101. package/skills/engineering/engineering-runtime/SKILL.md +1 -1
  102. package/skills/engineering/engineering-runtime/coherence/workflow.json +13 -13
  103. package/skills/engineering/engineering-runtime/release-check.mjs +3 -1
  104. package/skills/engineering/engineering-runtime/scripts/installation-resolver.mjs +191 -0
  105. package/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +51 -10
  106. package/skills/engineering/ensure-coverage/SKILL.md +1 -1
  107. package/skills/engineering/ensure-coverage/release-check.mjs +3 -1
  108. package/skills/engineering/golden-path/SKILL.md +1 -1
  109. package/skills/engineering/golden-path/release-check.mjs +3 -1
  110. package/skills/engineering/harness-runtime/SKILL.md +1 -1
  111. package/skills/engineering/harness-runtime/release-check.mjs +3 -1
  112. package/skills/engineering/resolve-issues/SKILL.md +11 -20
  113. package/skills/engineering/resolve-issues/references/delivery-progress.md +63 -0
  114. package/skills/engineering/resolve-issues/references/expire.md +24 -11
  115. package/skills/engineering/resolve-issues/references/implementation.md +9 -1
  116. package/skills/engineering/resolve-issues/references/preflight.md +7 -0
  117. package/skills/engineering/resolve-issues/references/shared-modules.md +27 -0
  118. package/skills/engineering/resolve-issues/references/task-plan.md +12 -5
  119. package/skills/engineering/resolve-issues/release-check.mjs +3 -1
  120. package/skills/engineering/resolve-issues/scripts/active-installation.mjs +32 -0
  121. package/skills/engineering/resolve-issues/scripts/install-hooks.mjs +3 -0
  122. package/skills/engineering/resolve-issues/scripts/rehydrate.mjs +17 -1
  123. package/skills/engineering/resolve-release/SKILL.md +1 -1
  124. package/skills/engineering/resolve-release/release-check.mjs +3 -1
  125. package/skills/engineering/rush-issues/SKILL.md +5 -2
  126. package/skills/engineering/rush-issues/release-check.mjs +3 -1
  127. package/skills/engineering/rush-release/SKILL.md +1 -1
  128. package/skills/engineering/rush-release/release-check.mjs +3 -1
  129. package/skills/engineering/test-quality/SKILL.md +1 -1
  130. package/skills/engineering/test-quality/release-check.mjs +3 -1
  131. package/skills/risk-compliance/compliance-audit/SKILL.md +1 -1
  132. package/skills/risk-compliance/compliance-audit/release-check.mjs +3 -1
  133. package/skills/risk-compliance/contract-compare/SKILL.md +1 -1
  134. package/skills/risk-compliance/contract-compare/release-check.mjs +3 -1
  135. package/skills/risk-compliance/contract-review/SKILL.md +1 -1
  136. package/skills/risk-compliance/contract-review/release-check.mjs +3 -1
  137. package/skills/risk-compliance/doc-authenticity/SKILL.md +1 -1
  138. package/skills/risk-compliance/doc-authenticity/release-check.mjs +3 -1
  139. package/skills/risk-compliance/fraud-analysis/SKILL.md +1 -1
  140. package/skills/risk-compliance/fraud-analysis/release-check.mjs +3 -1
  141. package/skills/risk-compliance/payment-analysis/SKILL.md +1 -1
  142. package/skills/risk-compliance/payment-analysis/release-check.mjs +3 -1
  143. package/skills/risk-compliance/similar-domain/SKILL.md +1 -1
  144. package/skills/risk-compliance/similar-domain/release-check.mjs +3 -1
  145. package/skills/risk-compliance/tld-detection/SKILL.md +1 -1
  146. package/skills/risk-compliance/tld-detection/release-check.mjs +3 -1
  147. package/skills/app-dev/android-foundation/scripts/device-run.test.mjs +0 -172
  148. package/skills/app-dev/data-catalog/scripts/data-catalog.test.mjs +0 -36
  149. package/skills/app-dev/data-quality/scripts/data-quality.test.mjs +0 -12
  150. package/skills/cicd/github-actions/scripts/ci-run-audit.test.mjs +0 -637
  151. package/skills/cicd/github-flow/scripts/flow.test.mjs +0 -114
  152. package/skills/cicd/github-flow/scripts/persist-conventions.test.mjs +0 -108
  153. package/skills/cicd/mergify/scripts/audit.test.mjs +0 -201
  154. package/skills/cicd/mergify/scripts/entrypoints.test.mjs +0 -106
  155. package/skills/cicd/mergify/scripts/pr-620-mergify-comments.fixture.json +0 -30
  156. package/skills/cicd/mergify/scripts/stack-push.test.mjs +0 -233
  157. package/skills/cicd/mergify/scripts/watch-pr-delivery.test.mjs +0 -1826
  158. package/skills/content/image-gen/scripts/credentials.test.mjs +0 -212
  159. package/skills/content/image-gen/scripts/options.test.mjs +0 -245
  160. package/skills/content/xai-search/tests/smoke_test.sh +0 -36
  161. package/skills/core/polish-cn/scripts/polish-cn.test.mjs +0 -244
  162. package/skills/core/reg-parser/scripts/reg-parser.test.mjs +0 -165
  163. package/skills/engineering/better-review/scripts/pr-context.test.mjs +0 -168
  164. package/skills/engineering/better-review/scripts/rereview-scope.test.mjs +0 -587
  165. package/skills/engineering/better-review/scripts/scan-diff.test.mjs +0 -414
  166. package/skills/engineering/better-review/scripts/vendor-sync.test.mjs +0 -96
  167. package/skills/engineering/contributing/scripts/persist-conventions.test.mjs +0 -118
  168. package/skills/engineering/e2e-test/scripts/presentation-checks.test.mjs +0 -106
  169. package/skills/engineering/e2e-test/scripts/presentation-perceivability.test.mjs +0 -117
  170. package/skills/engineering/e2e-test/scripts/presentation-reachability.test.mjs +0 -69
  171. package/skills/engineering/e2e-test/scripts/presentation-render-health.test.mjs +0 -98
  172. package/skills/engineering/e2e-test/scripts/presentation-sweep.test.mjs +0 -71
  173. package/skills/engineering/e2e-test/scripts/presentation-temporal.test.mjs +0 -59
  174. package/skills/engineering/e2e-test/scripts/project-route-manifest.test.mjs +0 -76
  175. package/skills/engineering/engineering-runtime/engineering-runtime.test.mjs +0 -68
  176. package/skills/engineering/engineering-runtime/scripts/invocation-policy.test.mjs +0 -95
  177. package/skills/engineering/engineering-runtime/scripts/main-module.test.mjs +0 -288
  178. package/skills/engineering/engineering-runtime/scripts/required-check-plan.test.mjs +0 -96
  179. package/skills/engineering/ensure-coverage/scripts/adapters/nextjs-inventory.test.mjs +0 -96
  180. package/skills/engineering/ensure-coverage/scripts/check-quarantine-expiry.test.mjs +0 -47
  181. package/skills/engineering/ensure-coverage/scripts/ci-audit.test.mjs +0 -86
  182. package/skills/engineering/ensure-coverage/scripts/coverage-ledger.test.mjs +0 -447
  183. package/skills/engineering/ensure-coverage/scripts/design-parity.test.mjs +0 -552
  184. package/skills/engineering/ensure-coverage/scripts/lint-tests.test.mjs +0 -289
  185. package/skills/engineering/ensure-coverage/scripts/mock-policy-config.test.mjs +0 -119
  186. package/skills/engineering/golden-path/scripts/golden-path.test.mjs +0 -1457
  187. package/skills/engineering/harness-runtime/discover-models.test.mjs +0 -363
  188. package/skills/engineering/harness-runtime/harness-runtime.test.mjs +0 -967
  189. package/skills/engineering/harness-runtime/model-catalog-seed.test.mjs +0 -829
  190. package/skills/engineering/harness-runtime/model-catalog.test.mjs +0 -366
  191. package/skills/engineering/resolve-issues/scripts/cleanup-runs.test.mjs +0 -280
  192. package/skills/engineering/resolve-issues/scripts/dropbox.test.mjs +0 -346
  193. package/skills/engineering/resolve-issues/scripts/findings-reuse.test.mjs +0 -88
  194. package/skills/engineering/resolve-issues/scripts/github-reviews.test.mjs +0 -264
  195. package/skills/engineering/resolve-issues/scripts/install-hooks.test.mjs +0 -79
  196. package/skills/engineering/resolve-issues/scripts/issue-tree.test.mjs +0 -107
  197. package/skills/engineering/resolve-issues/scripts/manual-invocation.test.mjs +0 -14
  198. package/skills/engineering/resolve-issues/scripts/next-actions-capacity.test.mjs +0 -149
  199. package/skills/engineering/resolve-issues/scripts/next-actions.test.mjs +0 -915
  200. package/skills/engineering/resolve-issues/scripts/plan-sync.test.mjs +0 -156
  201. package/skills/engineering/resolve-issues/scripts/preflight-models.test.mjs +0 -448
  202. package/skills/engineering/resolve-issues/scripts/ready-check.test.mjs +0 -217
  203. package/skills/engineering/resolve-issues/scripts/rehydrate.test.mjs +0 -91
  204. package/skills/engineering/resolve-issues/scripts/review-verdict.test.mjs +0 -1060
  205. package/skills/engineering/resolve-issues/scripts/size-check.test.mjs +0 -458
  206. package/skills/engineering/resolve-release/scripts/apply.test.mjs +0 -99
  207. package/skills/engineering/resolve-release/scripts/dark-flags.test.mjs +0 -100
  208. package/skills/engineering/resolve-release/scripts/flow.test.mjs +0 -340
  209. package/skills/engineering/resolve-release/scripts/green-head.test.mjs +0 -101
  210. package/skills/engineering/resolve-release/scripts/npm-version-absence.test.mjs +0 -426
  211. package/skills/engineering/resolve-release/scripts/plan-npm.test.mjs +0 -659
  212. package/skills/engineering/resolve-release/scripts/plan-toml-boundary.test.mjs +0 -317
  213. package/skills/engineering/resolve-release/scripts/plan-uv.test.mjs +0 -291
  214. package/skills/engineering/resolve-release/scripts/plan.test.mjs +0 -428
  215. package/skills/engineering/test-quality/scripts/lint-tests.test.mjs +0 -591
  216. package/skills/engineering/test-quality/scripts/mock-policy-config.test.mjs +0 -119
  217. package/skills/engineering/test-quality/scripts/persist-conventions.test.mjs +0 -104
  218. package/skills/risk-compliance/compliance-audit/scripts/compliance-audit.test.mjs +0 -14
  219. package/skills/risk-compliance/doc-authenticity/manual-invocation.test.mjs +0 -12
  220. package/skills/risk-compliance/doc-authenticity/scripts/validate_verdict.test.mjs +0 -212
  221. package/skills/risk-compliance/tld-detection/scripts/detect_ladders.test.mjs +0 -14
  222. package/skills/risk-compliance/tld-detection/scripts/detect_streams.test.mjs +0 -14
  223. package/skills/risk-compliance/tld-detection/tests/test_detect_ladders.py +0 -134
  224. package/skills/risk-compliance/tld-detection/tests/test_detect_streams.py +0 -520
@@ -1,100 +0,0 @@
1
- import { test } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
4
- import { tmpdir } from 'node:os'
5
- import { join } from 'node:path'
6
- import { spawnSync } from 'node:child_process'
7
- import { checkLive, darkFlags, isOff, listDarkFlags, parseRegistry } from './dark-flags.mjs'
8
-
9
- const SCRIPT = join(import.meta.dirname, 'dark-flags.mjs')
10
-
11
- function git(cwd, args) {
12
- const result = spawnSync('git', args, { cwd, encoding: 'utf8' })
13
- assert.equal(result.status, 0, result.stderr)
14
- return result.stdout
15
- }
16
-
17
- function registry(cwd, flags) {
18
- writeFileSync(join(cwd, 'config', 'flags.json'), `${JSON.stringify({ flags }, null, 2)}\n`)
19
- git(cwd, ['add', 'config/flags.json'])
20
- }
21
-
22
- function repo() {
23
- const cwd = mkdtempSync(join(tmpdir(), 'resolve-release-dark-'))
24
- git(cwd, ['init', '-b', 'main'])
25
- git(cwd, ['config', 'user.email', 'dev@example.com'])
26
- git(cwd, ['config', 'user.name', 'Dev'])
27
- git(cwd, ['config', 'commit.gpgsign', 'false'])
28
- mkdirSync(join(cwd, 'config'))
29
- registry(cwd, { 'old-search': { default: true, description: 'Search (launched)' } })
30
- git(cwd, ['commit', '-m', 'chore: init'])
31
- git(cwd, ['tag', 'v1.0.0'])
32
- return cwd
33
- }
34
-
35
- test('off means off, and nothing else does', () => {
36
- for (const value of [false, 0, 'off', 'OFF', 'false', 'disabled', '0']) assert.equal(isOff(value), true, String(value))
37
- for (const value of [true, 1, 'on', 'true', 'enabled', null, undefined, '']) assert.equal(isOff(value), false, String(value))
38
- })
39
-
40
- test('a registry needs a flags mapping and a default per flag', () => {
41
- assert.throws(() => parseRegistry('{"flags": []}'), /"flags" mapping/)
42
- assert.throws(() => parseRegistry('flags:\n x: {description: y}\n'), /flag x needs a default/)
43
- assert.deepEqual(Object.keys(parseRegistry('flags:\n x: {default: off}\n')), ['x'])
44
- })
45
-
46
- test('new registry entries and Dark-Launch trailers ship dark; each must default off', () => {
47
- const result = darkFlags({
48
- registryAtBase: { launched: { default: true } },
49
- registryAtSha: {
50
- launched: { default: true },
51
- 'bulk-refunds': { default: false, description: 'Bulk refunds' },
52
- 'new-nav': { default: 'on' },
53
- },
54
- trailerNames: ['launched', 'ghost'],
55
- })
56
- assert.deepEqual(result.flags.map((flag) => flag.name), ['bulk-refunds', 'launched', 'new-nav'])
57
- assert.equal(result.flags[0].summary, 'Bulk refunds')
58
- assert.deepEqual(result.errors.sort(), [
59
- 'Dark-Launch trailer names ghost, which is not in the flag registry',
60
- 'flag launched ships with default true; a dark flag defaults to off',
61
- 'flag new-nav ships with default "on"; a dark flag defaults to off',
62
- ])
63
- })
64
-
65
- test('the live check fails on a flag that is on or unread', () => {
66
- const flags = [{ name: 'a' }, { name: 'b' }, { name: 'c' }]
67
- assert.deepEqual(checkLive(flags, { a: false, b: 'off', c: 0 }), { ok: true, failures: [] })
68
- assert.deepEqual(checkLive(flags, { a: false, b: 'on' }).failures, ['b: live value "on" is not off', 'c: no live reading'])
69
- })
70
-
71
- test('listing reads the registry at both refs and the trailers in between', () => {
72
- const cwd = repo()
73
- registry(cwd, {
74
- 'old-search': { default: true, description: 'Search (launched)' },
75
- 'bulk-refunds': { default: false, description: 'Bulk refunds' },
76
- })
77
- git(cwd, ['commit', '-m', 'feat: bulk refunds\n\nDark-Launch: bulk-refunds'])
78
- const result = listDarkFlags({ registry: 'config/flags.json', sha: 'HEAD', base: 'v1.0.0', cwd })
79
- assert.deepEqual(result, { flags: [{ name: 'bulk-refunds', summary: 'Bulk refunds', default: false }], errors: [] })
80
-
81
- const cli = spawnSync('node', [SCRIPT, 'list', '--registry', 'config/flags.json', '--sha', 'HEAD', '--base', 'v1.0.0', '--json'], { cwd, encoding: 'utf8' })
82
- assert.equal(cli.status, 0, cli.stderr)
83
- assert.equal(JSON.parse(cli.stdout).flags[0].name, 'bulk-refunds')
84
-
85
- const missing = spawnSync('node', [SCRIPT, 'list', '--registry', 'nope.json', '--sha', 'HEAD'], { cwd, encoding: 'utf8' })
86
- assert.equal(missing.status, 1)
87
- assert.match(missing.stderr, /does not exist at HEAD/)
88
- })
89
-
90
- test('the check CLI exits non-zero when a dark flag is live', () => {
91
- const cwd = mkdtempSync(join(tmpdir(), 'resolve-release-dark-check-'))
92
- writeFileSync(join(cwd, 'list.json'), JSON.stringify({ flags: [{ name: 'bulk-refunds', summary: 'Bulk refunds' }] }))
93
- writeFileSync(join(cwd, 'off.json'), JSON.stringify({ 'bulk-refunds': false }))
94
- writeFileSync(join(cwd, 'on.json'), JSON.stringify({ 'bulk-refunds': true }))
95
- const check = (observed) => spawnSync('node', [SCRIPT, 'check', '--flags', 'list.json', '--observed', observed], { cwd, encoding: 'utf8' })
96
- assert.equal(check('off.json').status, 0)
97
- const live = check('on.json')
98
- assert.equal(live.status, 1)
99
- assert.match(live.stdout, /bulk-refunds: live value true is not off/)
100
- })
@@ -1,340 +0,0 @@
1
- import { test } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
4
- import { tmpdir } from 'node:os'
5
- import { join } from 'node:path'
6
- import { spawnSync } from 'node:child_process'
7
- import { listPredefined, loadFlow, renderFlow, resolveChoices, validateFlow } from './flow.mjs'
8
-
9
- const SCRIPT = join(import.meta.dirname, 'flow.mjs')
10
-
11
- // The variation pi-better-harness releases with: package-scoped dispatch publishing, no traffic.
12
- const WORKSPACE_FLOW = `---
13
- route: workspace-npm
14
- publisher:
15
- workflow: publish.yml
16
- input: package
17
- ownsTags: true
18
- packages:
19
- exclude: [pi-better-read-aloud, navigator, sandbox-core]
20
- publishLast: [pi-better-harness]
21
- changelog: package-scoped
22
- registry: https://registry.npmjs.org/
23
- ask:
24
- - id: bundle-bump
25
- question: Should pi-better-harness bundle the new component versions?
26
- options: [yes, no]
27
- exposure: n/a
28
- dark: n/a
29
- ---
30
-
31
- # Release flow
32
-
33
- ## before-cut
34
-
35
- - \`npm run verify\`
36
- - \`npm publish --dry-run -w <workspace> --registry=<registry>\` for each package
37
-
38
- ## after-publish
39
-
40
- - \`npm view <pkg> version --registry=<registry>\` prints the planned version
41
- - \`pi install npm:<pkg>\` succeeds
42
-
43
- ## hand-back-if
44
-
45
- - a planned package has never been published on npm
46
- (a first publish needs a human npm login)
47
- `
48
-
49
- const CANARY_FLOW = `---
50
- route: cloud-run
51
- exposure: canary
52
- canary:
53
- steps: [10, 50]
54
- holdMinutes: 15
55
- healthCheck: npm run release:health -- --url {candidateUrl}
56
- dark: on
57
- flagRegistry: config/flags.json
58
- required: [canary]
59
- ---
60
- `
61
-
62
- const flow = (text) => {
63
- const result = validateFlow(text)
64
- assert.deepEqual(result.errors, [])
65
- return result
66
- }
67
-
68
- function run(cwd, args, input) {
69
- return spawnSync('node', [SCRIPT, ...args], { cwd, encoding: 'utf8', input })
70
- }
71
-
72
- function project(text) {
73
- const cwd = mkdtempSync(join(tmpdir(), 'resolve-release-flow-'))
74
- if (text !== undefined) {
75
- mkdirSync(join(cwd, '.release'))
76
- writeFileSync(join(cwd, '.release', 'flow.md'), text)
77
- }
78
- return cwd
79
- }
80
-
81
- test('a workspace-npm flow fills slots and lists its steps per stage', () => {
82
- const result = flow(WORKSPACE_FLOW)
83
- assert.equal(result.settings.route, 'workspace-npm')
84
- assert.deepEqual(result.settings.packages.publishLast, ['pi-better-harness'])
85
- assert.equal(result.steps['before-cut'].length, 2)
86
- assert.deepEqual(result.steps['hand-back-if'], ['a planned package has never been published on npm (a first publish needs a human npm login)'])
87
- assert.match(result.sha256, /^[0-9a-f]{64}$/)
88
- assert.notEqual(validateFlow(`${WORKSPACE_FLOW}\n`).sha256, result.sha256, 'any byte change is a different flow')
89
- })
90
-
91
- test('a flow cannot reshape the skeleton, the invocation policy, or invent slots', () => {
92
- const cases = [
93
- ['---\ntrunk: develop\n---\n', /release skeleton/],
94
- ['---\ndisable-model-invocation: false\n---\n', /manual-only/],
95
- ['---\nconfirm: true\n---\n', /release skeleton/],
96
- ['---\nrolloutSpeed: fast\n---\n', /unknown setting "rolloutSpeed"/],
97
- ['---\nroute: gitflow\n---\n', /route must be one of/],
98
- ['---\n---\n## deploy-first\n- x\n', /unknown section "## deploy-first"/],
99
- ['---\n---\n## before-cut\nrun the checks\n', /only "- " steps/],
100
- ['# no frontmatter\n', /starts with a --- frontmatter/],
101
- ['---\nroute: workspace-npm\nexposure: canary\ncanary: {steps: [10], holdMinutes: 5, healthCheck: x}\n---\n', /only to route cloud-run/],
102
- ['---\nexposure: canary\n---\n', /needs a canary block/],
103
- ['---\ncanary: {steps: [50, 10], holdMinutes: 5, healthCheck: x}\n---\n', /strictly increase/],
104
- ['---\ncanary: {steps: [10, 100], holdMinutes: 5, healthCheck: x}\n---\n', /between 1 and 99/],
105
- ['---\ndark: on\n---\n', /dark on needs flagRegistry/],
106
- ['---\nrequired: [canary]\n---\n', /required canary needs exposure canary/],
107
- ['---\nrequired: [speed]\n---\n', /required lists only canary/],
108
- ['---\nask:\n - {id: exposure, question: Q}\n---\n', /built-in choice/],
109
- ['---\nask:\n - {id: tier, question: Q, options: [a, b], default: c}\n---\n', /default must be one of its options/],
110
- ['---\nregistry: http://registry.npmjs.org/\n---\n', /https:\/\//],
111
- ['---\npackages: {exclude: [a]}\nroute: root-tag\n---\n', /only to route workspace-npm/],
112
- ]
113
- for (const [text, pattern] of cases) {
114
- const result = validateFlow(text)
115
- assert.equal(result.valid, false, text)
116
- assert.ok(result.errors.some((error) => pattern.test(error)), `${text}\n→ ${result.errors.join('; ')}`)
117
- }
118
- })
119
-
120
- test('shortcut mode with no flow is the unchanged rush-release: full, off, no questions', () => {
121
- const result = resolveChoices({ mode: 'shortcut' })
122
- assert.deepEqual(result.choices, { exposure: 'full', dark: 'off', canary: null, flagRegistry: null, ask: {} })
123
- assert.deepEqual(result.questions, [])
124
- assert.deepEqual(result.refusals, [])
125
- })
126
-
127
- test('shortcut mode never asks: flow defaults answer, and anything it cannot answer refuses', () => {
128
- const withDefault = flow(WORKSPACE_FLOW.replace('options: [yes, no]', 'options: [yes, no]\n default: no'))
129
- const answered = resolveChoices({ mode: 'shortcut', flow: withDefault })
130
- assert.deepEqual(answered.choices.ask, { 'bundle-bump': 'no' })
131
- assert.equal(answered.choices.exposure, 'n/a')
132
- assert.deepEqual(answered.questions, [])
133
- assert.deepEqual(answered.refusals, [])
134
-
135
- const noDefault = resolveChoices({ mode: 'shortcut', flow: flow(WORKSPACE_FLOW) })
136
- assert.match(noDefault.refusals.join('\n'), /no default; rush-release never asks/)
137
-
138
- const required = resolveChoices({ mode: 'shortcut', flow: flow(CANARY_FLOW) })
139
- assert.match(required.refusals.join('\n'), /requires canary; rush-release cannot provide it/)
140
- assert.equal(required.choices.exposure, 'full', 'the shortcut never takes the flow canary')
141
-
142
- const option = resolveChoices({ mode: 'shortcut', invocation: { exposure: 'canary' } })
143
- assert.match(option.refusals.join('\n'), /accepts no exposure option; use \/resolve-release/)
144
- })
145
-
146
- test('full mode asks exposure and dark, suggesting what the repository declares', () => {
147
- const bare = resolveChoices({ mode: 'full' })
148
- assert.deepEqual(bare.questions.map((q) => [q.id, q.default]), [['exposure', 'full'], ['dark', 'off']])
149
- const hinted = resolveChoices({ mode: 'full', repo: { canary: true, flagRegistry: true } })
150
- assert.deepEqual(hinted.questions.map((q) => [q.id, q.default]), [['exposure', 'canary'], ['dark', 'on']])
151
- })
152
-
153
- test('full mode asks canary details only once canary is chosen, then resolves', () => {
154
- const chosen = resolveChoices({ mode: 'full', answers: { exposure: 'canary', dark: 'off' }, invocation: { healthCheck: 'npm run health' } })
155
- assert.deepEqual(chosen.questions.map((q) => q.id), ['canarySteps', 'holdMinutes'])
156
- const done = resolveChoices({
157
- mode: 'full',
158
- answers: { exposure: 'canary', dark: 'off', canarySteps: '5,25,50', holdMinutes: '10' },
159
- invocation: { healthCheck: 'npm run health' },
160
- })
161
- assert.deepEqual(done.questions, [])
162
- assert.deepEqual(done.refusals, [])
163
- assert.deepEqual(done.choices.canary, { steps: [5, 25, 50], holdMinutes: 10, healthCheck: 'npm run health' })
164
-
165
- const noHealth = resolveChoices({ mode: 'full', answers: { exposure: 'canary', dark: 'off', canarySteps: '10', holdMinutes: '5' } })
166
- assert.match(noHealth.refusals.join('\n'), /repository-declared health check/)
167
- })
168
-
169
- test('precedence: invocation over flow over preflight answer', () => {
170
- const canaryFlow = flow(CANARY_FLOW.replace('required: [canary]\n', ''))
171
- const fromFlow = resolveChoices({ mode: 'full', flow: canaryFlow, answers: { exposure: 'full', dark: 'off' } })
172
- assert.equal(fromFlow.choices.exposure, 'canary')
173
- assert.equal(fromFlow.sources.exposure, 'flow')
174
- assert.equal(fromFlow.choices.dark, 'on')
175
- assert.equal(fromFlow.choices.flagRegistry, 'config/flags.json')
176
- assert.deepEqual(fromFlow.questions, [])
177
-
178
- const overridden = resolveChoices({ mode: 'full', flow: canaryFlow, invocation: { exposure: 'full', dark: 'off' } })
179
- assert.equal(overridden.choices.exposure, 'full')
180
- assert.equal(overridden.sources.exposure, 'invocation')
181
- assert.equal(overridden.choices.canary, null)
182
-
183
- const requiredFlow = resolveChoices({ mode: 'full', flow: flow(CANARY_FLOW), invocation: { exposure: 'full' } })
184
- assert.match(requiredFlow.refusals.join('\n'), /requires canary exposure/)
185
-
186
- const notApplicable = resolveChoices({ mode: 'full', flow: flow(WORKSPACE_FLOW), invocation: { exposure: 'canary' } })
187
- assert.match(notApplicable.refusals.join('\n'), /marks exposure n\/a/)
188
- })
189
-
190
- test('full mode asks flow questions and checks answers against their options', () => {
191
- const asked = resolveChoices({ mode: 'full', flow: flow(WORKSPACE_FLOW) })
192
- assert.deepEqual(asked.questions.map((q) => q.id), ['bundle-bump'])
193
- const answered = resolveChoices({ mode: 'full', flow: flow(WORKSPACE_FLOW), answers: { 'bundle-bump': 'yes' } })
194
- assert.deepEqual(answered.choices.ask, { 'bundle-bump': 'yes' })
195
- const wrong = resolveChoices({ mode: 'full', flow: flow(WORKSPACE_FLOW), answers: { 'bundle-bump': 'maybe' } })
196
- assert.match(wrong.refusals.join('\n'), /bundle-bump must be one of yes, no/)
197
- })
198
-
199
- test('the CLI exit code says resolved, questions pending, refused, or invalid', () => {
200
- assert.equal(run(project(), ['resolve', '--mode', 'shortcut', '--json']).status, 0)
201
- const pending = run(project(), ['resolve', '--mode', 'full', '--json'])
202
- assert.equal(pending.status, 4)
203
- assert.equal(JSON.parse(pending.stdout).flow, null)
204
- assert.equal(run(project(CANARY_FLOW), ['resolve', '--mode', 'shortcut']).status, 3)
205
- const invalid = run(project('---\ntrunk: develop\n---\n'), ['resolve', '--mode', 'shortcut'])
206
- assert.equal(invalid.status, 1)
207
- assert.match(invalid.stdout, /release skeleton/)
208
- const resolved = run(project(CANARY_FLOW), ['resolve', '--mode', 'full', '--json'])
209
- assert.equal(resolved.status, 0, resolved.stdout)
210
- assert.equal(JSON.parse(resolved.stdout).flow.sha256, validateFlow(CANARY_FLOW).sha256)
211
- assert.equal(run(project(), ['validate']).stdout.trim(), 'no project flow at .release/flow.md')
212
- assert.equal(run(project(), ['resolve']).status, 2)
213
- })
214
-
215
- test('saving a flow records the choices and keeps every existing slot and step', () => {
216
- const cwd = project(WORKSPACE_FLOW.replace('exposure: n/a\ndark: n/a\n', ''))
217
- const existing = loadFlow('.release/flow.md', { cwd })
218
- const text = renderFlow({ choices: { exposure: 'full', dark: 'off', canary: null, flagRegistry: null, ask: { 'bundle-bump': 'yes' } } }, existing)
219
- const saved = flow(text)
220
- assert.equal(saved.settings.exposure, 'full')
221
- assert.equal(saved.settings.dark, 'off')
222
- assert.equal(saved.settings.route, 'workspace-npm')
223
- assert.deepEqual(saved.steps, existing.steps)
224
- assert.doesNotMatch(text, /bundle-bump: yes/, 'per-release answers are asked again, not saved')
225
-
226
- const piped = run(cwd, ['render'], JSON.stringify({ choices: { exposure: 'full', dark: 'off' } }))
227
- assert.equal(piped.status, 0, piped.stderr)
228
- assert.equal(validateFlow(piped.stdout).valid, true)
229
- })
230
-
231
- // The project half of a flow that starts from the skill's predefined workspace-npm flow.
232
- const EXTENDING_FLOW = `---
233
- extends: workspace-npm
234
- publisher: { workflow: publish.yml, input: package, ownsTags: true }
235
- packages:
236
- exclude: [pi-better-read-aloud]
237
- publishLast: [pi-better-harness]
238
- ask:
239
- - id: bundle-bump
240
- question: Should pi-better-harness bundle the new component versions?
241
- options: [yes, no]
242
- ---
243
-
244
- ## after-publish
245
- - \`pi install npm:<pkg>\` succeeds
246
- `
247
-
248
- function predefinedDir(files) {
249
- const dir = mkdtempSync(join(tmpdir(), 'resolve-release-flows-'))
250
- for (const [name, text] of Object.entries(files)) writeFileSync(join(dir, `${name}.md`), text)
251
- return dir
252
- }
253
-
254
- test('every predefined flow the skill ships is valid and describes itself', () => {
255
- const flows = listPredefined()
256
- assert.deepEqual(flows.map((flow) => flow.name), ['cloud-run', 'cloud-run-canary', 'root-tag-npm', 'workspace-npm'])
257
- for (const flow of flows) {
258
- assert.deepEqual(flow.errors, [], flow.name)
259
- assert.ok(flow.description.length > 20, flow.name)
260
- }
261
- const listed = run(project(), ['list', '--json'])
262
- assert.equal(listed.status, 0)
263
- assert.equal(JSON.parse(listed.stdout).length, 4)
264
- })
265
-
266
- test('a project flow extends a predefined one: slots merge, base steps run first', () => {
267
- const flow = loadFlow('.release/flow.md', { cwd: project(EXTENDING_FLOW) })
268
- assert.deepEqual(flow.errors, [])
269
- assert.equal(flow.base, 'workspace-npm')
270
- assert.equal(flow.settings.route, 'workspace-npm', 'from the predefined flow')
271
- assert.equal(flow.settings.registry, 'https://registry.npmjs.org/', 'from the predefined flow')
272
- assert.equal(flow.settings.publisher.workflow, 'publish.yml', 'from the project')
273
- assert.equal('extends' in flow.settings, false)
274
- assert.equal(flow.steps['after-publish'].length, 2)
275
- assert.match(flow.steps['after-publish'][0], /npm view/)
276
- assert.match(flow.steps['after-publish'][1], /pi install/)
277
- assert.equal(flow.steps['hand-back-if'].length, 1)
278
- assert.equal(resolveChoices({ mode: 'shortcut', flow }).choices.exposure, 'n/a')
279
- })
280
-
281
- test('the effective hash covers both files, so a skill update to the base is a different flow', () => {
282
- const cwd = project('---\nextends: base\nregistry: https://r.example/\n---\n')
283
- const before = loadFlow('.release/flow.md', { cwd, dir: predefinedDir({ base: '---\ndescription: base flow for tests\n---\n' }) })
284
- const after = loadFlow('.release/flow.md', { cwd, dir: predefinedDir({ base: '---\ndescription: base flow for tests, changed\n---\n' }) })
285
- assert.equal(before.valid, true, before.errors.join('; '))
286
- assert.notEqual(before.sha256, after.sha256)
287
- })
288
-
289
- test('extends must name a shipped flow, and a predefined flow cannot extend', () => {
290
- const unknown = loadFlow('.release/flow.md', { cwd: project('---\nextends: gitflow\n---\n') })
291
- assert.equal(unknown.valid, false)
292
- assert.match(unknown.errors.join('\n'), /no predefined flow "gitflow"; available: cloud-run, cloud-run-canary/)
293
- const dir = predefinedDir({ chained: '---\nextends: cloud-run\n---\n' })
294
- assert.match(listPredefined(dir)[0].errors.join('\n'), /cannot extend another flow/)
295
- })
296
-
297
- test('cross-field rules apply to the merged flow', () => {
298
- const canary = loadFlow('.release/flow.md', { cwd: project('---\nextends: cloud-run-canary\ncanary: { healthCheck: npm run health }\n---\n') })
299
- assert.deepEqual(canary.errors, [])
300
- assert.deepEqual(canary.settings.canary, { steps: [10, 50], holdMinutes: 15, healthCheck: 'npm run health' })
301
- const full = resolveChoices({ mode: 'full', flow: canary, answers: { dark: 'off' } })
302
- assert.deepEqual(full.refusals, [])
303
- assert.equal(full.choices.canary.healthCheck, 'npm run health')
304
-
305
- const noHealth = loadFlow('.release/flow.md', { cwd: project('---\nextends: cloud-run-canary\n---\n') })
306
- assert.equal(noHealth.valid, true, 'the health check is a resolve-time requirement')
307
- assert.match(resolveChoices({ mode: 'full', flow: noHealth, answers: { dark: 'off' } }).refusals.join('\n'), /health check/)
308
-
309
- const mismatch = loadFlow('.release/flow.md', { cwd: project('---\nextends: root-tag-npm\nexposure: canary\ncanary: { steps: [10], holdMinutes: 5 }\n---\n') })
310
- assert.equal(mismatch.valid, false)
311
- assert.match(mismatch.errors.join('\n'), /\.release\/flow\.md \+ root-tag-npm: canary exposure applies only to route cloud-run/)
312
- })
313
-
314
- test('--flow picks a predefined flow only when the project has none', () => {
315
- const picked = loadFlow('.release/flow.md', { cwd: project(), name: 'cloud-run-canary' })
316
- assert.equal(picked.valid, true)
317
- assert.equal(picked.path, null)
318
- assert.equal(picked.base, 'cloud-run-canary')
319
- const shortcut = resolveChoices({ mode: 'shortcut', flow: picked })
320
- assert.equal(shortcut.choices.exposure, 'full', 'the shortcut never takes a canary it was not required to')
321
- assert.deepEqual(shortcut.refusals, [])
322
-
323
- const clash = run(project(EXTENDING_FLOW), ['validate', '--flow', 'cloud-run'])
324
- assert.equal(clash.status, 1)
325
- assert.match(clash.stdout, /project flow wins/)
326
- assert.equal(run(project(), ['resolve', '--mode', 'full', '--flow', 'nope']).status, 1)
327
- })
328
-
329
- test('saving a flow that extends keeps only the project half', () => {
330
- const cwd = project(EXTENDING_FLOW)
331
- const text = renderFlow({ choices: { exposure: 'n/a', dark: 'off' } }, loadFlow('.release/flow.md', { cwd }))
332
- assert.match(text, /^extends: workspace-npm$/m)
333
- assert.doesNotMatch(text, /registry:/, 'base slots stay in the skill')
334
- assert.equal((text.match(/npm view/g) || []).length, 0, 'base steps are not copied')
335
- assert.match(text, /pi install/)
336
- const fromPredefined = renderFlow({ choices: { exposure: 'canary', dark: 'off', canary: { steps: [10, 50], holdMinutes: 15, healthCheck: 'npm run health' } } },
337
- loadFlow('.release/flow.md', { cwd: project(), name: 'cloud-run-canary' }))
338
- assert.match(fromPredefined, /^extends: cloud-run-canary$/m)
339
- assert.equal(loadFlow('.release/flow.md', { cwd: project(fromPredefined) }).valid, true)
340
- })
@@ -1,101 +0,0 @@
1
- import { test } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import {
4
- findGreenHead,
5
- inspectGreen,
6
- parseGithubRemote,
7
- pickGreenHead,
8
- requiredContexts,
9
- } from './green-head.mjs'
10
-
11
- test('parses GitHub remotes', () => {
12
- assert.deepEqual(parseGithubRemote('git@github.com:acme/app.git'), { owner: 'acme', repo: 'app' })
13
- assert.deepEqual(parseGithubRemote('https://github.com/acme/app.git'), { owner: 'acme', repo: 'app' })
14
- assert.equal(parseGithubRemote('git@gitlab.com:acme/app.git'), null)
15
- })
16
-
17
- test('required contexts union both protection shapes', () => {
18
- assert.deepEqual(
19
- requiredContexts({
20
- required_status_checks: {
21
- contexts: ['build'],
22
- checks: [{ context: 'test' }, { context: 'build' }],
23
- },
24
- }),
25
- ['build', 'test'],
26
- )
27
- })
28
-
29
- test('required checks must all pass; pending is not green', () => {
30
- const required = ['build', 'test']
31
- const pending = inspectGreen({
32
- status: { state: 'pending', statuses: [] },
33
- checkRuns: { check_runs: [{ name: 'build', conclusion: 'success' }, { name: 'test', status: 'in_progress' }] },
34
- required,
35
- })
36
- assert.equal(pending.green, false)
37
- assert.deepEqual(pending.pending, ['test'])
38
-
39
- const green = inspectGreen({
40
- status: { state: 'success', statuses: [] },
41
- checkRuns: { check_runs: [{ name: 'build', conclusion: 'success' }, { name: 'test', conclusion: 'skipped' }] },
42
- required,
43
- })
44
- assert.equal(green.green, true)
45
- })
46
-
47
- test('without required checks, no CI is not green and failures refuse', () => {
48
- assert.equal(inspectGreen({ status: { state: 'pending' }, checkRuns: { check_runs: [] }, required: [] }).green, false)
49
- assert.equal(inspectGreen({
50
- status: { state: 'success' },
51
- checkRuns: { check_runs: [{ name: 'build', conclusion: 'failure' }] },
52
- required: [],
53
- }).green, false)
54
- assert.equal(inspectGreen({
55
- status: { state: 'pending', statuses: [] },
56
- checkRuns: { check_runs: [{ name: 'build', conclusion: 'success' }] },
57
- required: [],
58
- }).green, true, 'check-run-only repositories must not be blocked by an empty combined Status API')
59
-
60
- const pagedFailure = inspectGreen({
61
- status: { state: 'pending', statuses: [] },
62
- checkRuns: [
63
- { check_runs: [{ name: 'build', conclusion: 'success' }] },
64
- { check_runs: [{ name: 'late-page', conclusion: 'failure' }] },
65
- ],
66
- required: [],
67
- })
68
- assert.equal(pagedFailure.green, false)
69
- assert.deepEqual(pagedFailure.failed, ['late-page'])
70
- })
71
-
72
- test('findGreenHead requests every check-run page', () => {
73
- const calls = []
74
- const git = (args) => {
75
- if (args[0] === 'fetch') return { status: 0, stdout: '', stderr: '' }
76
- if (args[0] === 'remote') return { status: 0, stdout: 'https://github.com/acme/app.git\n', stderr: '' }
77
- if (args[0] === 'log') return { status: 0, stdout: `${'a'.repeat(40)}\n`, stderr: '' }
78
- throw new Error(`unexpected git call: ${args.join(' ')}`)
79
- }
80
- const api = (path, options = {}) => {
81
- calls.push({ path, options })
82
- if (path.endsWith('/protection')) return null
83
- if (path.endsWith('/status')) return { state: 'pending', statuses: [] }
84
- if (path.includes('/check-runs')) return [{ check_runs: [{ name: 'build', conclusion: 'success' }] }]
85
- throw new Error(`unexpected API call: ${path}`)
86
- }
87
-
88
- const result = findGreenHead({ cwd: '.', git, api })
89
- assert.equal(result.candidateSha, 'a'.repeat(40))
90
- assert.deepEqual(
91
- calls.find((call) => call.path.includes('/check-runs'))?.options,
92
- { paginate: true },
93
- )
94
- })
95
-
96
- test('picker returns the newest green SHA and stops', () => {
97
- const shas = ['tip-red', 'green', 'older-green']
98
- const inspect = (sha) => ({ green: sha !== 'tip-red' })
99
- assert.deepEqual(pickGreenHead(shas, inspect), { sha: 'green', verdict: { green: true } })
100
- assert.equal(pickGreenHead(['red'], () => ({ green: false })), null)
101
- })