@1aboveio/skills 1.0.8 → 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 (214) 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 +1 -1
  8. package/skills/app-dev/android-foundation/release-check.mjs +3 -1
  9. package/skills/app-dev/app-debug/SKILL.md +1 -1
  10. package/skills/app-dev/app-debug/release-check.mjs +3 -1
  11. package/skills/app-dev/better-auth/SKILL.md +1 -1
  12. package/skills/app-dev/better-auth/release-check.mjs +3 -1
  13. package/skills/app-dev/data-catalog/SKILL.md +1 -1
  14. package/skills/app-dev/data-catalog/release-check.mjs +3 -1
  15. package/skills/app-dev/data-quality/SKILL.md +1 -1
  16. package/skills/app-dev/data-quality/release-check.mjs +3 -1
  17. package/skills/app-dev/ios-foundation/SKILL.md +1 -1
  18. package/skills/app-dev/ios-foundation/release-check.mjs +3 -1
  19. package/skills/app-dev/monorepo/SKILL.md +1 -1
  20. package/skills/app-dev/monorepo/release-check.mjs +3 -1
  21. package/skills/app-dev/nextjs-fullstack/SKILL.md +1 -1
  22. package/skills/app-dev/nextjs-fullstack/release-check.mjs +3 -1
  23. package/skills/app-dev/prisma-setup/SKILL.md +1 -1
  24. package/skills/app-dev/prisma-setup/release-check.mjs +3 -1
  25. package/skills/app-dev/pyspark/SKILL.md +1 -1
  26. package/skills/app-dev/pyspark/release-check.mjs +3 -1
  27. package/skills/app-dev/python-backend/SKILL.md +1 -1
  28. package/skills/app-dev/python-backend/release-check.mjs +3 -1
  29. package/skills/app-dev/shadcn/SKILL.md +1 -1
  30. package/skills/app-dev/shadcn/release-check.mjs +3 -1
  31. package/skills/app-dev/zod-v4/SKILL.md +1 -1
  32. package/skills/app-dev/zod-v4/release-check.mjs +3 -1
  33. package/skills/cicd/cloud-build/SKILL.md +1 -1
  34. package/skills/cicd/cloud-build/release-check.mjs +3 -1
  35. package/skills/cicd/cloud-debug/SKILL.md +1 -1
  36. package/skills/cicd/cloud-debug/release-check.mjs +3 -1
  37. package/skills/cicd/cloud-deploy/SKILL.md +1 -1
  38. package/skills/cicd/cloud-deploy/release-check.mjs +3 -1
  39. package/skills/cicd/docker/SKILL.md +1 -1
  40. package/skills/cicd/docker/release-check.mjs +3 -1
  41. package/skills/cicd/github-actions/SKILL.md +1 -1
  42. package/skills/cicd/github-actions/release-check.mjs +3 -1
  43. package/skills/cicd/github-flow/SKILL.md +1 -1
  44. package/skills/cicd/github-flow/release-check.mjs +3 -1
  45. package/skills/cicd/google-cloud/SKILL.md +1 -1
  46. package/skills/cicd/google-cloud/release-check.mjs +3 -1
  47. package/skills/cicd/mergify/SKILL.md +1 -1
  48. package/skills/cicd/mergify/release-check.mjs +3 -1
  49. package/skills/cicd/podman/SKILL.md +1 -1
  50. package/skills/cicd/podman/release-check.mjs +3 -1
  51. package/skills/content/article-review/SKILL.md +1 -1
  52. package/skills/content/article-review/release-check.mjs +3 -1
  53. package/skills/content/article-to-image/SKILL.md +1 -1
  54. package/skills/content/article-to-image/release-check.mjs +3 -1
  55. package/skills/content/flowchart-svg/SKILL.md +1 -1
  56. package/skills/content/flowchart-svg/release-check.mjs +3 -1
  57. package/skills/content/image-gen/SKILL.md +1 -1
  58. package/skills/content/image-gen/release-check.mjs +3 -1
  59. package/skills/content/mermaid/SKILL.md +1 -1
  60. package/skills/content/mermaid/release-check.mjs +3 -1
  61. package/skills/content/wechat-article-writer/SKILL.md +1 -1
  62. package/skills/content/wechat-article-writer/release-check.mjs +3 -1
  63. package/skills/content/wechat-publisher/SKILL.md +1 -1
  64. package/skills/content/wechat-publisher/release-check.mjs +3 -1
  65. package/skills/content/wechat-search/SKILL.md +1 -1
  66. package/skills/content/wechat-search/release-check.mjs +3 -1
  67. package/skills/content/xai-search/SKILL.md +88 -13
  68. package/skills/content/xai-search/release-check.mjs +3 -1
  69. package/skills/content/xai-search/scripts/xai_search.sh +125 -149
  70. package/skills/core/distill-writing/SKILL.md +1 -1
  71. package/skills/core/distill-writing/release-check.mjs +3 -1
  72. package/skills/core/doc-to-md/SKILL.md +1 -1
  73. package/skills/core/doc-to-md/release-check.mjs +3 -1
  74. package/skills/core/editorial-review/SKILL.md +1 -1
  75. package/skills/core/editorial-review/release-check.mjs +3 -1
  76. package/skills/core/house-style/SKILL.md +1 -1
  77. package/skills/core/house-style/release-check.mjs +3 -1
  78. package/skills/core/plain-language/SKILL.md +1 -1
  79. package/skills/core/plain-language/release-check.mjs +3 -1
  80. package/skills/core/polish-cn/SKILL.md +1 -1
  81. package/skills/core/polish-cn/release-check.mjs +3 -1
  82. package/skills/core/reg-parser/SKILL.md +1 -1
  83. package/skills/core/reg-parser/release-check.mjs +3 -1
  84. package/skills/core/terminology/SKILL.md +1 -1
  85. package/skills/core/terminology/release-check.mjs +3 -1
  86. package/skills/core/write-doc/SKILL.md +1 -1
  87. package/skills/core/write-doc/release-check.mjs +3 -1
  88. package/skills/engineering/better-review/SKILL.md +1 -1
  89. package/skills/engineering/better-review/release-check.mjs +3 -1
  90. package/skills/engineering/contributing/SKILL.md +1 -1
  91. package/skills/engineering/contributing/release-check.mjs +3 -1
  92. package/skills/engineering/e2e-test/SKILL.md +1 -1
  93. package/skills/engineering/e2e-test/release-check.mjs +3 -1
  94. package/skills/engineering/engineering-runtime/SKILL.md +1 -1
  95. package/skills/engineering/engineering-runtime/coherence/workflow.json +13 -13
  96. package/skills/engineering/engineering-runtime/release-check.mjs +3 -1
  97. package/skills/engineering/engineering-runtime/scripts/installation-resolver.mjs +191 -0
  98. package/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +51 -10
  99. package/skills/engineering/ensure-coverage/SKILL.md +1 -1
  100. package/skills/engineering/ensure-coverage/release-check.mjs +3 -1
  101. package/skills/engineering/golden-path/SKILL.md +1 -1
  102. package/skills/engineering/golden-path/release-check.mjs +3 -1
  103. package/skills/engineering/harness-runtime/SKILL.md +1 -1
  104. package/skills/engineering/harness-runtime/release-check.mjs +3 -1
  105. package/skills/engineering/resolve-issues/SKILL.md +1 -1
  106. package/skills/engineering/resolve-issues/references/preflight.md +7 -0
  107. package/skills/engineering/resolve-issues/references/shared-modules.md +27 -0
  108. package/skills/engineering/resolve-issues/references/task-plan.md +5 -1
  109. package/skills/engineering/resolve-issues/release-check.mjs +3 -1
  110. package/skills/engineering/resolve-issues/scripts/active-installation.mjs +32 -0
  111. package/skills/engineering/resolve-issues/scripts/install-hooks.mjs +3 -0
  112. package/skills/engineering/resolve-issues/scripts/rehydrate.mjs +17 -1
  113. package/skills/engineering/resolve-release/SKILL.md +1 -1
  114. package/skills/engineering/resolve-release/release-check.mjs +3 -1
  115. package/skills/engineering/rush-issues/SKILL.md +1 -1
  116. package/skills/engineering/rush-issues/release-check.mjs +3 -1
  117. package/skills/engineering/rush-release/SKILL.md +1 -1
  118. package/skills/engineering/rush-release/release-check.mjs +3 -1
  119. package/skills/engineering/test-quality/SKILL.md +1 -1
  120. package/skills/engineering/test-quality/release-check.mjs +3 -1
  121. package/skills/risk-compliance/compliance-audit/SKILL.md +1 -1
  122. package/skills/risk-compliance/compliance-audit/release-check.mjs +3 -1
  123. package/skills/risk-compliance/contract-compare/SKILL.md +1 -1
  124. package/skills/risk-compliance/contract-compare/release-check.mjs +3 -1
  125. package/skills/risk-compliance/contract-review/SKILL.md +1 -1
  126. package/skills/risk-compliance/contract-review/release-check.mjs +3 -1
  127. package/skills/risk-compliance/doc-authenticity/SKILL.md +1 -1
  128. package/skills/risk-compliance/doc-authenticity/release-check.mjs +3 -1
  129. package/skills/risk-compliance/fraud-analysis/SKILL.md +1 -1
  130. package/skills/risk-compliance/fraud-analysis/release-check.mjs +3 -1
  131. package/skills/risk-compliance/payment-analysis/SKILL.md +1 -1
  132. package/skills/risk-compliance/payment-analysis/release-check.mjs +3 -1
  133. package/skills/risk-compliance/similar-domain/SKILL.md +1 -1
  134. package/skills/risk-compliance/similar-domain/release-check.mjs +3 -1
  135. package/skills/risk-compliance/tld-detection/SKILL.md +1 -1
  136. package/skills/risk-compliance/tld-detection/release-check.mjs +3 -1
  137. package/skills/app-dev/android-foundation/scripts/device-run.test.mjs +0 -436
  138. package/skills/app-dev/data-catalog/scripts/data-catalog.test.mjs +0 -36
  139. package/skills/app-dev/data-quality/scripts/data-quality.test.mjs +0 -12
  140. package/skills/cicd/github-actions/scripts/ci-run-audit.test.mjs +0 -637
  141. package/skills/cicd/github-flow/scripts/flow.test.mjs +0 -114
  142. package/skills/cicd/github-flow/scripts/persist-conventions.test.mjs +0 -108
  143. package/skills/cicd/mergify/scripts/audit.test.mjs +0 -201
  144. package/skills/cicd/mergify/scripts/entrypoints.test.mjs +0 -106
  145. package/skills/cicd/mergify/scripts/pr-620-mergify-comments.fixture.json +0 -30
  146. package/skills/cicd/mergify/scripts/stack-push.test.mjs +0 -233
  147. package/skills/cicd/mergify/scripts/watch-pr-delivery.test.mjs +0 -1826
  148. package/skills/content/image-gen/scripts/credentials.test.mjs +0 -212
  149. package/skills/content/image-gen/scripts/options.test.mjs +0 -245
  150. package/skills/content/xai-search/tests/smoke_test.sh +0 -36
  151. package/skills/core/polish-cn/scripts/polish-cn.test.mjs +0 -244
  152. package/skills/core/reg-parser/scripts/reg-parser.test.mjs +0 -165
  153. package/skills/engineering/better-review/scripts/pr-context.test.mjs +0 -168
  154. package/skills/engineering/better-review/scripts/rereview-scope.test.mjs +0 -587
  155. package/skills/engineering/better-review/scripts/scan-diff.test.mjs +0 -414
  156. package/skills/engineering/better-review/scripts/vendor-sync.test.mjs +0 -96
  157. package/skills/engineering/contributing/scripts/persist-conventions.test.mjs +0 -118
  158. package/skills/engineering/e2e-test/scripts/presentation-checks.test.mjs +0 -106
  159. package/skills/engineering/e2e-test/scripts/presentation-perceivability.test.mjs +0 -117
  160. package/skills/engineering/e2e-test/scripts/presentation-reachability.test.mjs +0 -69
  161. package/skills/engineering/e2e-test/scripts/presentation-render-health.test.mjs +0 -98
  162. package/skills/engineering/e2e-test/scripts/presentation-sweep.test.mjs +0 -71
  163. package/skills/engineering/e2e-test/scripts/presentation-temporal.test.mjs +0 -59
  164. package/skills/engineering/e2e-test/scripts/project-route-manifest.test.mjs +0 -76
  165. package/skills/engineering/engineering-runtime/engineering-runtime.test.mjs +0 -68
  166. package/skills/engineering/engineering-runtime/scripts/invocation-policy.test.mjs +0 -95
  167. package/skills/engineering/engineering-runtime/scripts/main-module.test.mjs +0 -288
  168. package/skills/engineering/engineering-runtime/scripts/required-check-plan.test.mjs +0 -96
  169. package/skills/engineering/ensure-coverage/scripts/adapters/nextjs-inventory.test.mjs +0 -96
  170. package/skills/engineering/ensure-coverage/scripts/check-quarantine-expiry.test.mjs +0 -47
  171. package/skills/engineering/ensure-coverage/scripts/ci-audit.test.mjs +0 -86
  172. package/skills/engineering/ensure-coverage/scripts/coverage-ledger.test.mjs +0 -447
  173. package/skills/engineering/ensure-coverage/scripts/design-parity.test.mjs +0 -552
  174. package/skills/engineering/ensure-coverage/scripts/lint-tests.test.mjs +0 -289
  175. package/skills/engineering/ensure-coverage/scripts/mock-policy-config.test.mjs +0 -119
  176. package/skills/engineering/golden-path/scripts/golden-path.test.mjs +0 -1457
  177. package/skills/engineering/harness-runtime/discover-models.test.mjs +0 -363
  178. package/skills/engineering/harness-runtime/harness-runtime.test.mjs +0 -967
  179. package/skills/engineering/harness-runtime/model-catalog-seed.test.mjs +0 -829
  180. package/skills/engineering/harness-runtime/model-catalog.test.mjs +0 -366
  181. package/skills/engineering/resolve-issues/scripts/cleanup-runs.test.mjs +0 -280
  182. package/skills/engineering/resolve-issues/scripts/dropbox.test.mjs +0 -346
  183. package/skills/engineering/resolve-issues/scripts/findings-reuse.test.mjs +0 -88
  184. package/skills/engineering/resolve-issues/scripts/github-reviews.test.mjs +0 -264
  185. package/skills/engineering/resolve-issues/scripts/install-hooks.test.mjs +0 -79
  186. package/skills/engineering/resolve-issues/scripts/issue-tree.test.mjs +0 -107
  187. package/skills/engineering/resolve-issues/scripts/manual-invocation.test.mjs +0 -14
  188. package/skills/engineering/resolve-issues/scripts/next-actions-capacity.test.mjs +0 -149
  189. package/skills/engineering/resolve-issues/scripts/next-actions.test.mjs +0 -915
  190. package/skills/engineering/resolve-issues/scripts/plan-sync.test.mjs +0 -156
  191. package/skills/engineering/resolve-issues/scripts/preflight-models.test.mjs +0 -448
  192. package/skills/engineering/resolve-issues/scripts/ready-check.test.mjs +0 -217
  193. package/skills/engineering/resolve-issues/scripts/rehydrate.test.mjs +0 -91
  194. package/skills/engineering/resolve-issues/scripts/review-verdict.test.mjs +0 -1060
  195. package/skills/engineering/resolve-issues/scripts/size-check.test.mjs +0 -458
  196. package/skills/engineering/resolve-release/scripts/apply.test.mjs +0 -99
  197. package/skills/engineering/resolve-release/scripts/dark-flags.test.mjs +0 -100
  198. package/skills/engineering/resolve-release/scripts/flow.test.mjs +0 -340
  199. package/skills/engineering/resolve-release/scripts/green-head.test.mjs +0 -101
  200. package/skills/engineering/resolve-release/scripts/npm-version-absence.test.mjs +0 -426
  201. package/skills/engineering/resolve-release/scripts/plan-npm.test.mjs +0 -659
  202. package/skills/engineering/resolve-release/scripts/plan-toml-boundary.test.mjs +0 -317
  203. package/skills/engineering/resolve-release/scripts/plan-uv.test.mjs +0 -291
  204. package/skills/engineering/resolve-release/scripts/plan.test.mjs +0 -428
  205. package/skills/engineering/test-quality/scripts/lint-tests.test.mjs +0 -591
  206. package/skills/engineering/test-quality/scripts/mock-policy-config.test.mjs +0 -119
  207. package/skills/engineering/test-quality/scripts/persist-conventions.test.mjs +0 -104
  208. package/skills/risk-compliance/compliance-audit/scripts/compliance-audit.test.mjs +0 -14
  209. package/skills/risk-compliance/doc-authenticity/manual-invocation.test.mjs +0 -12
  210. package/skills/risk-compliance/doc-authenticity/scripts/validate_verdict.test.mjs +0 -212
  211. package/skills/risk-compliance/tld-detection/scripts/detect_ladders.test.mjs +0 -14
  212. package/skills/risk-compliance/tld-detection/scripts/detect_streams.test.mjs +0 -14
  213. package/skills/risk-compliance/tld-detection/tests/test_detect_ladders.py +0 -134
  214. 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
- })