@1aboveio/skills 1.0.8 → 1.0.10
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.
- package/.codex-plugin/plugin.json +13 -0
- package/.codex-plugin/resources.json +1827 -0
- package/README.md +22 -0
- package/package.json +9 -2
- package/skills/app-dev/airflow/SKILL.md +1 -1
- package/skills/app-dev/airflow/release-check.mjs +3 -1
- package/skills/app-dev/android-foundation/SKILL.md +1 -1
- package/skills/app-dev/android-foundation/release-check.mjs +3 -1
- package/skills/app-dev/app-debug/SKILL.md +1 -1
- package/skills/app-dev/app-debug/release-check.mjs +3 -1
- package/skills/app-dev/better-auth/SKILL.md +1 -1
- package/skills/app-dev/better-auth/release-check.mjs +3 -1
- package/skills/app-dev/data-catalog/SKILL.md +1 -1
- package/skills/app-dev/data-catalog/release-check.mjs +3 -1
- package/skills/app-dev/data-quality/SKILL.md +1 -1
- package/skills/app-dev/data-quality/release-check.mjs +3 -1
- package/skills/app-dev/ios-foundation/SKILL.md +1 -1
- package/skills/app-dev/ios-foundation/release-check.mjs +3 -1
- package/skills/app-dev/monorepo/SKILL.md +1 -1
- package/skills/app-dev/monorepo/release-check.mjs +3 -1
- package/skills/app-dev/nextjs-fullstack/SKILL.md +1 -1
- package/skills/app-dev/nextjs-fullstack/release-check.mjs +3 -1
- package/skills/app-dev/prisma-setup/SKILL.md +1 -1
- package/skills/app-dev/prisma-setup/release-check.mjs +3 -1
- package/skills/app-dev/pyspark/SKILL.md +1 -1
- package/skills/app-dev/pyspark/release-check.mjs +3 -1
- package/skills/app-dev/python-backend/SKILL.md +1 -1
- package/skills/app-dev/python-backend/release-check.mjs +3 -1
- package/skills/app-dev/shadcn/SKILL.md +1 -1
- package/skills/app-dev/shadcn/release-check.mjs +3 -1
- package/skills/app-dev/zod-v4/SKILL.md +1 -1
- package/skills/app-dev/zod-v4/release-check.mjs +3 -1
- package/skills/cicd/cloud-build/SKILL.md +1 -1
- package/skills/cicd/cloud-build/release-check.mjs +3 -1
- package/skills/cicd/cloud-debug/SKILL.md +1 -1
- package/skills/cicd/cloud-debug/release-check.mjs +3 -1
- package/skills/cicd/cloud-deploy/SKILL.md +1 -1
- package/skills/cicd/cloud-deploy/release-check.mjs +3 -1
- package/skills/cicd/docker/SKILL.md +1 -1
- package/skills/cicd/docker/release-check.mjs +3 -1
- package/skills/cicd/github-actions/SKILL.md +1 -1
- package/skills/cicd/github-actions/release-check.mjs +3 -1
- package/skills/cicd/github-flow/SKILL.md +1 -1
- package/skills/cicd/github-flow/release-check.mjs +3 -1
- package/skills/cicd/google-cloud/SKILL.md +1 -1
- package/skills/cicd/google-cloud/release-check.mjs +3 -1
- package/skills/cicd/mergify/SKILL.md +1 -1
- package/skills/cicd/mergify/release-check.mjs +3 -1
- package/skills/cicd/podman/SKILL.md +1 -1
- package/skills/cicd/podman/release-check.mjs +3 -1
- package/skills/content/article-review/SKILL.md +1 -1
- package/skills/content/article-review/release-check.mjs +3 -1
- package/skills/content/article-to-image/SKILL.md +1 -1
- package/skills/content/article-to-image/release-check.mjs +3 -1
- package/skills/content/flowchart-svg/SKILL.md +1 -1
- package/skills/content/flowchart-svg/release-check.mjs +3 -1
- package/skills/content/image-gen/SKILL.md +1 -1
- package/skills/content/image-gen/release-check.mjs +3 -1
- package/skills/content/mermaid/SKILL.md +1 -1
- package/skills/content/mermaid/release-check.mjs +3 -1
- package/skills/content/wechat-article-writer/SKILL.md +1 -1
- package/skills/content/wechat-article-writer/release-check.mjs +3 -1
- package/skills/content/wechat-publisher/SKILL.md +1 -1
- package/skills/content/wechat-publisher/release-check.mjs +3 -1
- package/skills/content/wechat-search/SKILL.md +1 -1
- package/skills/content/wechat-search/release-check.mjs +3 -1
- package/skills/content/xai-search/SKILL.md +88 -13
- package/skills/content/xai-search/release-check.mjs +3 -1
- package/skills/content/xai-search/scripts/xai_search.sh +125 -149
- package/skills/core/distill-writing/SKILL.md +1 -1
- package/skills/core/distill-writing/release-check.mjs +3 -1
- package/skills/core/doc-to-md/SKILL.md +1 -1
- package/skills/core/doc-to-md/release-check.mjs +3 -1
- package/skills/core/editorial-review/SKILL.md +1 -1
- package/skills/core/editorial-review/release-check.mjs +3 -1
- package/skills/core/house-style/SKILL.md +1 -1
- package/skills/core/house-style/release-check.mjs +3 -1
- package/skills/core/plain-language/SKILL.md +1 -1
- package/skills/core/plain-language/release-check.mjs +3 -1
- package/skills/core/polish-cn/SKILL.md +1 -1
- package/skills/core/polish-cn/release-check.mjs +3 -1
- package/skills/core/reg-parser/SKILL.md +1 -1
- package/skills/core/reg-parser/release-check.mjs +3 -1
- package/skills/core/terminology/SKILL.md +1 -1
- package/skills/core/terminology/release-check.mjs +3 -1
- package/skills/core/write-doc/SKILL.md +1 -1
- package/skills/core/write-doc/release-check.mjs +3 -1
- package/skills/engineering/better-review/SKILL.md +1 -1
- package/skills/engineering/better-review/release-check.mjs +3 -1
- package/skills/engineering/contributing/SKILL.md +1 -1
- package/skills/engineering/contributing/release-check.mjs +3 -1
- package/skills/engineering/e2e-test/SKILL.md +1 -1
- package/skills/engineering/e2e-test/release-check.mjs +3 -1
- package/skills/engineering/engineering-runtime/SKILL.md +1 -1
- package/skills/engineering/engineering-runtime/coherence/workflow.json +13 -13
- package/skills/engineering/engineering-runtime/release-check.mjs +3 -1
- package/skills/engineering/engineering-runtime/scripts/installation-resolver.mjs +191 -0
- package/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +51 -10
- package/skills/engineering/ensure-coverage/SKILL.md +1 -1
- package/skills/engineering/ensure-coverage/release-check.mjs +3 -1
- package/skills/engineering/golden-path/SKILL.md +1 -1
- package/skills/engineering/golden-path/release-check.mjs +3 -1
- package/skills/engineering/harness-runtime/SKILL.md +1 -1
- package/skills/engineering/harness-runtime/release-check.mjs +3 -1
- package/skills/engineering/resolve-issues/SKILL.md +1 -1
- package/skills/engineering/resolve-issues/references/preflight.md +7 -0
- package/skills/engineering/resolve-issues/references/shared-modules.md +27 -0
- package/skills/engineering/resolve-issues/references/task-plan.md +5 -1
- package/skills/engineering/resolve-issues/release-check.mjs +3 -1
- package/skills/engineering/resolve-issues/scripts/active-installation.mjs +32 -0
- package/skills/engineering/resolve-issues/scripts/install-hooks.mjs +3 -0
- package/skills/engineering/resolve-issues/scripts/rehydrate.mjs +17 -1
- package/skills/engineering/resolve-release/SKILL.md +1 -1
- package/skills/engineering/resolve-release/release-check.mjs +3 -1
- package/skills/engineering/rush-issues/SKILL.md +1 -1
- package/skills/engineering/rush-issues/release-check.mjs +3 -1
- package/skills/engineering/rush-release/SKILL.md +1 -1
- package/skills/engineering/rush-release/release-check.mjs +3 -1
- package/skills/engineering/test-quality/SKILL.md +1 -1
- package/skills/engineering/test-quality/release-check.mjs +3 -1
- package/skills/risk-compliance/compliance-audit/SKILL.md +1 -1
- package/skills/risk-compliance/compliance-audit/release-check.mjs +3 -1
- package/skills/risk-compliance/contract-compare/SKILL.md +1 -1
- package/skills/risk-compliance/contract-compare/release-check.mjs +3 -1
- package/skills/risk-compliance/contract-review/SKILL.md +1 -1
- package/skills/risk-compliance/contract-review/release-check.mjs +3 -1
- package/skills/risk-compliance/doc-authenticity/SKILL.md +1 -1
- package/skills/risk-compliance/doc-authenticity/release-check.mjs +3 -1
- package/skills/risk-compliance/fraud-analysis/SKILL.md +1 -1
- package/skills/risk-compliance/fraud-analysis/release-check.mjs +3 -1
- package/skills/risk-compliance/payment-analysis/SKILL.md +1 -1
- package/skills/risk-compliance/payment-analysis/release-check.mjs +3 -1
- package/skills/risk-compliance/similar-domain/SKILL.md +1 -1
- package/skills/risk-compliance/similar-domain/release-check.mjs +3 -1
- package/skills/risk-compliance/tld-detection/SKILL.md +1 -1
- package/skills/risk-compliance/tld-detection/release-check.mjs +3 -1
- package/skills/app-dev/android-foundation/scripts/device-run.test.mjs +0 -436
- package/skills/app-dev/data-catalog/scripts/data-catalog.test.mjs +0 -36
- package/skills/app-dev/data-quality/scripts/data-quality.test.mjs +0 -12
- package/skills/cicd/github-actions/scripts/ci-run-audit.test.mjs +0 -637
- package/skills/cicd/github-flow/scripts/flow.test.mjs +0 -114
- package/skills/cicd/github-flow/scripts/persist-conventions.test.mjs +0 -108
- package/skills/cicd/mergify/scripts/audit.test.mjs +0 -201
- package/skills/cicd/mergify/scripts/entrypoints.test.mjs +0 -106
- package/skills/cicd/mergify/scripts/pr-620-mergify-comments.fixture.json +0 -30
- package/skills/cicd/mergify/scripts/stack-push.test.mjs +0 -233
- package/skills/cicd/mergify/scripts/watch-pr-delivery.test.mjs +0 -1826
- package/skills/content/image-gen/scripts/credentials.test.mjs +0 -212
- package/skills/content/image-gen/scripts/options.test.mjs +0 -245
- package/skills/content/xai-search/tests/smoke_test.sh +0 -36
- package/skills/core/polish-cn/scripts/polish-cn.test.mjs +0 -244
- package/skills/core/reg-parser/scripts/reg-parser.test.mjs +0 -165
- package/skills/engineering/better-review/scripts/pr-context.test.mjs +0 -168
- package/skills/engineering/better-review/scripts/rereview-scope.test.mjs +0 -587
- package/skills/engineering/better-review/scripts/scan-diff.test.mjs +0 -414
- package/skills/engineering/better-review/scripts/vendor-sync.test.mjs +0 -96
- package/skills/engineering/contributing/scripts/persist-conventions.test.mjs +0 -118
- package/skills/engineering/e2e-test/scripts/presentation-checks.test.mjs +0 -106
- package/skills/engineering/e2e-test/scripts/presentation-perceivability.test.mjs +0 -117
- package/skills/engineering/e2e-test/scripts/presentation-reachability.test.mjs +0 -69
- package/skills/engineering/e2e-test/scripts/presentation-render-health.test.mjs +0 -98
- package/skills/engineering/e2e-test/scripts/presentation-sweep.test.mjs +0 -71
- package/skills/engineering/e2e-test/scripts/presentation-temporal.test.mjs +0 -59
- package/skills/engineering/e2e-test/scripts/project-route-manifest.test.mjs +0 -76
- package/skills/engineering/engineering-runtime/engineering-runtime.test.mjs +0 -68
- package/skills/engineering/engineering-runtime/scripts/invocation-policy.test.mjs +0 -95
- package/skills/engineering/engineering-runtime/scripts/main-module.test.mjs +0 -288
- package/skills/engineering/engineering-runtime/scripts/required-check-plan.test.mjs +0 -96
- package/skills/engineering/ensure-coverage/scripts/adapters/nextjs-inventory.test.mjs +0 -96
- package/skills/engineering/ensure-coverage/scripts/check-quarantine-expiry.test.mjs +0 -47
- package/skills/engineering/ensure-coverage/scripts/ci-audit.test.mjs +0 -86
- package/skills/engineering/ensure-coverage/scripts/coverage-ledger.test.mjs +0 -447
- package/skills/engineering/ensure-coverage/scripts/design-parity.test.mjs +0 -552
- package/skills/engineering/ensure-coverage/scripts/lint-tests.test.mjs +0 -289
- package/skills/engineering/ensure-coverage/scripts/mock-policy-config.test.mjs +0 -119
- package/skills/engineering/golden-path/scripts/golden-path.test.mjs +0 -1457
- package/skills/engineering/harness-runtime/discover-models.test.mjs +0 -363
- package/skills/engineering/harness-runtime/harness-runtime.test.mjs +0 -967
- package/skills/engineering/harness-runtime/model-catalog-seed.test.mjs +0 -829
- package/skills/engineering/harness-runtime/model-catalog.test.mjs +0 -366
- package/skills/engineering/resolve-issues/scripts/cleanup-runs.test.mjs +0 -280
- package/skills/engineering/resolve-issues/scripts/dropbox.test.mjs +0 -346
- package/skills/engineering/resolve-issues/scripts/findings-reuse.test.mjs +0 -88
- package/skills/engineering/resolve-issues/scripts/github-reviews.test.mjs +0 -264
- package/skills/engineering/resolve-issues/scripts/install-hooks.test.mjs +0 -79
- package/skills/engineering/resolve-issues/scripts/issue-tree.test.mjs +0 -107
- package/skills/engineering/resolve-issues/scripts/manual-invocation.test.mjs +0 -14
- package/skills/engineering/resolve-issues/scripts/next-actions-capacity.test.mjs +0 -149
- package/skills/engineering/resolve-issues/scripts/next-actions.test.mjs +0 -915
- package/skills/engineering/resolve-issues/scripts/plan-sync.test.mjs +0 -156
- package/skills/engineering/resolve-issues/scripts/preflight-models.test.mjs +0 -448
- package/skills/engineering/resolve-issues/scripts/ready-check.test.mjs +0 -217
- package/skills/engineering/resolve-issues/scripts/rehydrate.test.mjs +0 -91
- package/skills/engineering/resolve-issues/scripts/review-verdict.test.mjs +0 -1060
- package/skills/engineering/resolve-issues/scripts/size-check.test.mjs +0 -458
- package/skills/engineering/resolve-release/scripts/apply.test.mjs +0 -99
- package/skills/engineering/resolve-release/scripts/dark-flags.test.mjs +0 -100
- package/skills/engineering/resolve-release/scripts/flow.test.mjs +0 -340
- package/skills/engineering/resolve-release/scripts/green-head.test.mjs +0 -101
- package/skills/engineering/resolve-release/scripts/npm-version-absence.test.mjs +0 -426
- package/skills/engineering/resolve-release/scripts/plan-npm.test.mjs +0 -659
- package/skills/engineering/resolve-release/scripts/plan-toml-boundary.test.mjs +0 -317
- package/skills/engineering/resolve-release/scripts/plan-uv.test.mjs +0 -291
- package/skills/engineering/resolve-release/scripts/plan.test.mjs +0 -428
- package/skills/engineering/test-quality/scripts/lint-tests.test.mjs +0 -591
- package/skills/engineering/test-quality/scripts/mock-policy-config.test.mjs +0 -119
- package/skills/engineering/test-quality/scripts/persist-conventions.test.mjs +0 -104
- package/skills/risk-compliance/compliance-audit/scripts/compliance-audit.test.mjs +0 -14
- package/skills/risk-compliance/doc-authenticity/manual-invocation.test.mjs +0 -12
- package/skills/risk-compliance/doc-authenticity/scripts/validate_verdict.test.mjs +0 -212
- package/skills/risk-compliance/tld-detection/scripts/detect_ladders.test.mjs +0 -14
- package/skills/risk-compliance/tld-detection/scripts/detect_streams.test.mjs +0 -14
- package/skills/risk-compliance/tld-detection/tests/test_detect_ladders.py +0 -134
- 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
|
-
})
|