kaloko 0.5.2 → 0.6.0
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/README.md +75 -0
- package/package.json +1 -1
- package/schemas/changelog.v1.json +83 -0
- package/schemas/run.v1.json +19 -0
- package/schemas/scenario.v1.json +6 -1
- package/skills/kaloko/SKILL.md +28 -7
- package/skills/kaloko/reference/design.md +55 -0
- package/skills/kaloko/reference/docs.md +122 -3
- package/skills/kaloko/reference/integrations.md +67 -0
- package/skills/kaloko/reference/mcp.md +7 -1
- package/src/cli.mjs +213 -10
- package/src/lib/changelog.mjs +74 -8
- package/src/lib/ci.mjs +71 -0
- package/src/lib/design-cli.mjs +22 -2
- package/src/lib/design-notes.mjs +191 -0
- package/src/lib/docs-core.mjs +450 -0
- package/src/lib/docs-export.mjs +271 -0
- package/src/lib/docs.mjs +221 -0
- package/src/lib/figma.mjs +584 -0
- package/src/lib/git.mjs +23 -3
- package/src/lib/integrations-cli.mjs +211 -0
- package/src/lib/screens-cli.mjs +36 -0
- package/src/lib/service.mjs +20 -1
- package/viewer/index.html +266 -30
package/src/cli.mjs
CHANGED
|
@@ -36,6 +36,7 @@ import { upsertPrComment } from './lib/pr.mjs'
|
|
|
36
36
|
import { formatReviewQueue, shareReviewLine } from './lib/review.mjs'
|
|
37
37
|
import { htmlViewAllowed } from './lib/htmlview.mjs'
|
|
38
38
|
import { service, serviceUrl } from './lib/service.mjs'
|
|
39
|
+
import { issueCmd, scheduleCmd } from './lib/integrations-cli.mjs'
|
|
39
40
|
import { login } from './lib/login.mjs'
|
|
40
41
|
import { initNextSteps, loginNextSteps } from './lib/onboarding.mjs'
|
|
41
42
|
import { specCmd, driftCmd } from './lib/spec-cli.mjs'
|
|
@@ -45,7 +46,8 @@ import { formatAffected, selectedIds } from './lib/affected-core.mjs'
|
|
|
45
46
|
import { healStep, formatHeal } from './lib/heal.mjs'
|
|
46
47
|
import { readSource, stepFor, viewportFor, adhocScenario, writeImported, attachImported, keepVideos, slug } from './lib/import.mjs'
|
|
47
48
|
import { findPreview, pullRequestOf, envLines, writeEnvFile, githubWorkflow, gitlabJob, addPreviewEnvironment } from './lib/ci.mjs'
|
|
48
|
-
import { walkDesign, syncDesign, publishVersion, historyCmd, referenceCmd, tokensCmd, exportLive, localRevisions, branchCmd, uploadBlobs } from './lib/design-cli.mjs'
|
|
49
|
+
import { walkDesign, syncDesign, publishVersion, previewNotes, historyCmd, referenceCmd, tokensCmd, exportLive, localRevisions, branchCmd, uploadBlobs } from './lib/design-cli.mjs'
|
|
50
|
+
import { screensCmd } from './lib/screens-cli.mjs'
|
|
49
51
|
import { designDir, readBase, headKey } from './lib/design.mjs'
|
|
50
52
|
import { lintDesign, formatLint } from './lib/design-lint.mjs'
|
|
51
53
|
import { exportDesign } from './lib/design-export.mjs'
|
|
@@ -73,12 +75,17 @@ Project
|
|
|
73
75
|
Create ${CONFIG_FILE} and install the kaloko skill for your agent; detects the
|
|
74
76
|
framework, dev server URL, locales and main routes, and drafts a first scenario
|
|
75
77
|
(qa/flows/main-pages.yml) when the project has none; asks "Keep a visual changelog?"
|
|
76
|
-
(--docs: changelog adds CHANGELOG.md and the config block, full also docs/ folders
|
|
78
|
+
(--docs: changelog adds CHANGELOG.md and the config block, full also docs/ folders
|
|
79
|
+
with Diátaxis templates: tutorials, how-to, reference, explanation)
|
|
77
80
|
ci github|gitlab [--scenario a.yml,b.yml] [--env preview] [--trigger deployment|pull_request] [--no-affected]
|
|
78
81
|
[--out file] [--print] [--force]
|
|
79
82
|
Write a workflow that walks what each pull request affects on its preview
|
|
80
83
|
deployment (Vercel, Netlify, Cloudflare Pages, review apps) and comments on it;
|
|
81
84
|
adds the preview environment (base_url \${KALOKO_PREVIEW_URL}) to the config
|
|
85
|
+
ci github --release [--snippet] [--print] [--force]
|
|
86
|
+
Write .github/workflows/kaloko-release.yml: when release-please, changesets or
|
|
87
|
+
semantic-release publishes a GitHub release, kaloko release records it on Kaloko
|
|
88
|
+
(--snippet prints the step to add after the tool's own step instead)
|
|
82
89
|
ci preview-url [--wait 600] [--github-env] [--dotenv file]
|
|
83
90
|
In CI: the preview URL (KALOKO_PREVIEW_URL, the deployment event, provider
|
|
84
91
|
variables or the commit's GitHub deployments), the pull request and its base
|
|
@@ -228,21 +235,59 @@ Results
|
|
|
228
235
|
stability --pull [--scenario id] [--json]
|
|
229
236
|
Regions people drew on screenshots on kaloko.app (step detail → Page → Draw a
|
|
230
237
|
region) go into the scenario's ignore: of their steps; walk them again after
|
|
238
|
+
screens [words] [--project p] [--product p] [--module m] [--scenario id] [--locale l] [--viewport v] [--device d]
|
|
239
|
+
[--status pass|partial|fail|skip] [--environment e] [--designs] [--all] [--json] [--cursor c]
|
|
240
|
+
The screen library: the newest screenshot of every step across your flows, searched
|
|
241
|
+
by title, text, URL or id, with the canvas link of each (--designs adds design versions)
|
|
231
242
|
compare [--run id] [--baseline id | --design] [--json]
|
|
232
243
|
Text diff of two runs: step statuses, criterion verdicts, scores and evidence
|
|
233
244
|
(default: current run against the scenario baseline, else the previous run)
|
|
234
245
|
|
|
246
|
+
Jira and Linear (connected by an admin in Settings → Integrations)
|
|
247
|
+
issue create --step <id> --to jira|linear [--criterion AC2] [--title "…"] [--note "…"] [--run id] [--json]
|
|
248
|
+
An issue from a returned step or a failing criterion: the screenshot, the
|
|
249
|
+
criterion, the reviewers' notes and a link back; the step's comment is resolved
|
|
250
|
+
when the issue is done. Branches and PR titles that name a key (WEB-12) link runs
|
|
251
|
+
issue list [--run id] [--scenario id] [--state open|done|canceled] [--json]
|
|
252
|
+
|
|
253
|
+
Hosted runs (read-only scenarios on Kaloko's browsers, Business plan)
|
|
254
|
+
schedule add --scenario <file> --env <name> [--every day|week|manual] [--at 5] [--weekday mon] [--account key]
|
|
255
|
+
Kaloko opens the scenario's pages every day or week (hour in UTC) and shares the
|
|
256
|
+
run as you: public pages, HTTP Basic from the environment, or a saved session
|
|
257
|
+
(--account). Steps that click or type are left out; they need the full walk
|
|
258
|
+
schedule list | run <id> | pause <id> | resume <id> | remove <id> [--json]
|
|
259
|
+
schedule export github --scenario <file> --env <name> [--every day|week] [--at 5] [--out file] [--print]
|
|
260
|
+
A GitHub Actions workflow that walks the scenario on a schedule with the full CLI
|
|
261
|
+
|
|
235
262
|
Changelog (Keep a Changelog in CHANGELOG.md; pictures are step references kaloko:<scenario>/<step>)
|
|
236
263
|
changelog draft [--product p] [--since <tag|version>] [--apply] [--json] [--offline]
|
|
237
264
|
The Unreleased entry since the last release: merged pull requests and commits
|
|
238
265
|
(Conventional Commits when used, gh for titles) grouped Added / Changed / Fixed /
|
|
239
266
|
Removed, plus before/after pictures of the screens accepted runs show differently;
|
|
240
267
|
printed, or --apply writes it into CHANGELOG.md (not when a release tool owns it)
|
|
241
|
-
release [<version>] [--product p] [--date YYYY-MM-DD] [--runs id,id] [--no-write] [--dry-run] [--json]
|
|
268
|
+
release [<version>] [--product p] [--date YYYY-MM-DD] [--runs id,id] [--no-write] [--no-docs] [--dry-run] [--json]
|
|
242
269
|
Record a release on Kaloko: the version (default: from git tags, package.json,
|
|
243
270
|
release-please or changesets), its CHANGELOG.md entry (Unreleased becomes the
|
|
244
271
|
version unless --no-write) and the accepted runs that are its pictures; prints the
|
|
245
|
-
illustrated changelog page
|
|
272
|
+
illustrated changelog page. The docs folder is published as that version's docs
|
|
273
|
+
(Business) unless --no-docs
|
|
274
|
+
|
|
275
|
+
Docs (Markdown in docs/ with step references; Diátaxis folders; translations as guide.cs.md next to guide.md)
|
|
276
|
+
docs publish [--product p] [--version latest|<v>] [--reviewed] [--dry-run] [--json]
|
|
277
|
+
Send the docs folder to Kaloko as the docs of a version (default latest): readers
|
|
278
|
+
get a version selector, language and screen switches, search and training mode;
|
|
279
|
+
--reviewed says the text was checked, which clears its stale notes
|
|
280
|
+
docs check [--product p] [--version v] [--offline] [--strict] [--json]
|
|
281
|
+
Broken or unknown step references, stale sections (the screen changed since the
|
|
282
|
+
text was published), missing or outdated translations and user-visible changelog
|
|
283
|
+
items without a picture; exit 1 on references or stale sections (--strict: on
|
|
284
|
+
anything)
|
|
285
|
+
export --format md|docusaurus|vitepress|mkdocs|html|pdf|json [--product p] [--version v] [--lang l]
|
|
286
|
+
[--what all|docs|changelog] [--local] [--out dir|file]
|
|
287
|
+
The docs and changelog with their pictures as files: Markdown with an assets
|
|
288
|
+
folder, a Docusaurus, VitePress or MkDocs folder with sidebars, offline HTML, PDF
|
|
289
|
+
(printed by the browser) or the changelog as JSON; --local exports the files in
|
|
290
|
+
the repository instead of the published ones
|
|
246
291
|
|
|
247
292
|
Design (environments with kind: design; live HTML steps in a folder)
|
|
248
293
|
start --scenario <file> --env <design env> A run of the design folder (same scenario as development and QA)
|
|
@@ -254,7 +299,15 @@ Design (environments with kind: design; live HTML steps in a folder)
|
|
|
254
299
|
design lint [--env e | --dir design/] [--scenario file] [--all] [--strict] [--json]
|
|
255
300
|
Check the folder against the portable HTML rules (relative URLs, no CDNs, no storage,
|
|
256
301
|
deterministic content, viewport, lang, links that also work from disk); --strict fails on warnings
|
|
257
|
-
share [--name <text>] [--branch b]
|
|
302
|
+
share [--name <text>] [--branch b] [--notes file|-] [--notes-preview]
|
|
303
|
+
Publish the draft as a version of the flow (a run): "v4 · 3 changed, 17 unchanged",
|
|
304
|
+
with "what changed" notes per step (screens per viewport, text, tokens); --notes-preview
|
|
305
|
+
prints them without publishing, --notes replaces them (a summary, then "step: text" lines)
|
|
306
|
+
design import figma <file-url|key|export folder> [--scenario file] [--frames a,b] [--page p] [--map frames.json]
|
|
307
|
+
[--token-env FIGMA_TOKEN] [--tokens auto|write|skip] [--force] [--dry-run] [--publish [--notes file]]
|
|
308
|
+
Figma frames become design steps (frame PNG at 1× and 2×, text layers with their
|
|
309
|
+
typography, variables or styles as tokens.json) matched to steps by name or --map;
|
|
310
|
+
a folder exported from Figma works without a token. One way: no prototype links
|
|
258
311
|
history [--scenario id] [--step id] [--locale l] [--json]
|
|
259
312
|
Versions of the flow; revisions of a step with who (person or agent) and when
|
|
260
313
|
spec [step] [--ref design|draft|vN|<run id>] [--locale l] [--viewport v] [--local] [--json]
|
|
@@ -394,6 +447,49 @@ function stepOf (scenario, id) {
|
|
|
394
447
|
}
|
|
395
448
|
|
|
396
449
|
/** Scenario of --scenario (id or file) or of the current run. */
|
|
450
|
+
/**
|
|
451
|
+
* kaloko design import figma <file-url|key|folder>: Figma frames (or a folder exported from Figma) become the steps of
|
|
452
|
+
* the design folder; --publish then walks them and publishes a version (the design reference to be).
|
|
453
|
+
*/
|
|
454
|
+
async function designImport (args) {
|
|
455
|
+
const [, kind, source] = args._
|
|
456
|
+
if (kind !== 'figma' || !source) throw new UserError('Usage: kaloko design import figma <file-url|key|export folder> [--scenario file] [--env <design env>] [--frames a,b] [--page name] [--map frames.json|yml] [--token-env FIGMA_TOKEN] [--tokens auto|write|skip] [--force] [--dry-run] [--publish [--notes file]]')
|
|
457
|
+
const { config, root } = await context(args)
|
|
458
|
+
let run = null
|
|
459
|
+
try { run = await resolveRun({ root, output: config.output, run: args.run }) } catch {}
|
|
460
|
+
const runEnv = run ? environment(config, run.meta.environment.name) : null
|
|
461
|
+
const env = typeof args.env === 'string' ? environment(config, args.env) : runEnv?.kind === 'design' ? runEnv : Object.keys(config.environments).map((n) => environment(config, n)).find((e) => e.kind === 'design')
|
|
462
|
+
if (!env || env.kind !== 'design') throw new UserError('No design environment: add one with kind: design and serve: design/ to kaloko.config.yml, or pass --env <design env>.')
|
|
463
|
+
const scenarioFile = typeof args.scenario === 'string' ? resolve(args.scenario) : run && runEnv?.kind === 'design' ? resolve(root, run.meta.scenario_file) : null
|
|
464
|
+
if (!scenarioFile) throw new UserError('Name the scenario whose steps the frames are: --scenario <file>.')
|
|
465
|
+
const scenario = await loadScenario(scenarioFile)
|
|
466
|
+
const { importFigma, formatImport, readMap } = await import('./lib/figma.mjs')
|
|
467
|
+
const scope = startScope({ args: {}, scenario, config, env })
|
|
468
|
+
const viewports = Object.fromEntries(baseViewports(scope).map((v) => [v, scope.viewports[v]]))
|
|
469
|
+
const tokenVar = typeof args['token-env'] === 'string' ? args['token-env'] : typeof args.token === 'string' && /^[A-Z_][A-Z0-9_]*$/.test(args.token) ? args.token : 'FIGMA_TOKEN'
|
|
470
|
+
const dir = typeof args.dir === 'string' ? resolve(args.dir) : designDir(env, root)
|
|
471
|
+
const r = await importFigma({
|
|
472
|
+
source, scenario, viewports, locales: scenario.locales || ['default'], dir,
|
|
473
|
+
token: process.env[tokenVar] || (tokenVar === 'FIGMA_TOKEN' ? process.env.FIGMA_ACCESS_TOKEN : null) || null,
|
|
474
|
+
map: typeof args.map === 'string' ? await readMap(resolve(args.map)) : null, only: list(args.frames), page: typeof args.page === 'string' ? args.page : null,
|
|
475
|
+
tokensMode: ['write', 'skip'].includes(args.tokens) ? args.tokens : 'auto', force: !!args.force, dryRun: !!args['dry-run'], say
|
|
476
|
+
})
|
|
477
|
+
if (args.json) say(JSON.stringify(r, null, 2))
|
|
478
|
+
else say(formatImport(r, { dir: relative(process.cwd(), dir) || '.' }))
|
|
479
|
+
if (args['dry-run']) return r
|
|
480
|
+
const lint = await lintDesign({ dir, scenario })
|
|
481
|
+
if (lint.errors) say(`\n${formatLint(lint, { max: 10 })}`)
|
|
482
|
+
if (!args.publish) {
|
|
483
|
+
say(`\nNext: kaloko start --scenario ${relative(process.cwd(), scenarioFile)} --env ${env.name} && kaloko walk && kaloko share (or rerun with --publish).\nAccept the version on the canvas and set it as the design reference (kaloko reference --design --version <n>): implementation runs with packs: [design] are then checked against these frames.`)
|
|
484
|
+
return r
|
|
485
|
+
}
|
|
486
|
+
await commands.start({ ...args, _: [], scenario: scenarioFile, env: env.name })
|
|
487
|
+
const current = await resolveRun({ root, output: config.output })
|
|
488
|
+
await commands.walk({ _: [], run: current.dir })
|
|
489
|
+
await commands.share({ _: [], run: current.dir, ...(typeof args.notes === 'string' ? { notes: args.notes } : {}), ...(typeof args.name === 'string' ? { name: args.name } : {}), ...(typeof args.branch === 'string' ? { branch: args.branch } : {}) })
|
|
490
|
+
return r
|
|
491
|
+
}
|
|
492
|
+
|
|
397
493
|
async function scenarioIdOf (args, config, root) {
|
|
398
494
|
if (typeof args.scenario === 'string') {
|
|
399
495
|
if (/\.ya?ml$/.test(args.scenario)) return (await loadScenario(resolve(args.scenario))).id
|
|
@@ -801,7 +897,7 @@ const commands = {
|
|
|
801
897
|
const cfgFile = existsSync(file) ? file : join(cwd, LEGACY_CONFIG_FILE)
|
|
802
898
|
const cfg = await readFile(cfgFile, 'utf8')
|
|
803
899
|
const products = /^products:/m.test(cfg) ? [] : await detectProducts(cwd)
|
|
804
|
-
if (!/^docs:/m.test(cfg) && !/^products:/m.test(cfg)) { await writeFile(cfgFile, `${cfg.replace(/\n*$/, '\n')}${docsConfigBlock({ products })}`); say(`Added the changelog settings to ${relative(cwd, cfgFile)}${products.length ? ` with ${products.length} products (${products.map((p) => p.slug).join(', ')})` : ''}`) }
|
|
900
|
+
if (!/^docs:/m.test(cfg) && !/^products:/m.test(cfg)) { await writeFile(cfgFile, `${cfg.replace(/\n*$/, '\n')}${docsConfigBlock({ products, full: docsMode === 'full' })}`); say(`Added the changelog settings to ${relative(cwd, cfgFile)}${products.length ? ` with ${products.length} products (${products.map((p) => p.slug).join(', ')})` : ''}`) }
|
|
805
901
|
for (const f of products.length ? products.map((p) => join(cwd, p.dir, 'CHANGELOG.md')) : [join(cwd, 'CHANGELOG.md')]) {
|
|
806
902
|
if (existsSync(f)) continue
|
|
807
903
|
await mkdirp(dirname(f), { recursive: true })
|
|
@@ -815,7 +911,7 @@ const commands = {
|
|
|
815
911
|
await mkdirp(dirname(f), { recursive: true })
|
|
816
912
|
await writeFile(f, text)
|
|
817
913
|
}
|
|
818
|
-
say('Docs folders in docs/ (tutorials, how-to, reference, explanation) with an index')
|
|
914
|
+
say('Docs folders in docs/ (tutorials, how-to, reference, explanation) with an index and a template in each; kaloko docs publish sends them, kaloko docs check checks them')
|
|
819
915
|
}
|
|
820
916
|
} else if (!docsMode) say('Visual changelog: kaloko init --docs=changelog adds CHANGELOG.md and the config block (recommended).')
|
|
821
917
|
const agent = typeof args.agent === 'string' ? args.agent : 'none'
|
|
@@ -920,7 +1016,7 @@ const commands = {
|
|
|
920
1016
|
const vpNames = baseViewports(scope)
|
|
921
1017
|
const git = await gitMeta(root)
|
|
922
1018
|
const executor = { kind: args.executor ? 'agent' : 'cli', name: args.executor || 'kaloko-cli' }
|
|
923
|
-
const { project, issue } = await projectAndIssue({ cwd: root, config, scenario, branch: git?.branch })
|
|
1019
|
+
const { project, issue } = await projectAndIssue({ cwd: root, config, scenario, branch: git?.branch, title: git?.pr?.title || null })
|
|
924
1020
|
const appVer = typeof args['app-version'] === 'string' ? args['app-version'].slice(0, 80) : await appVersion(root)
|
|
925
1021
|
// the product the flow documents (visual changelog): its tag, or the only product of the config
|
|
926
1022
|
const product = scenario.product || (Object.keys(config.products || {}).length === 1 ? Object.keys(config.products)[0] : null)
|
|
@@ -1390,8 +1486,32 @@ const commands = {
|
|
|
1390
1486
|
console.log(preview.url)
|
|
1391
1487
|
return
|
|
1392
1488
|
}
|
|
1393
|
-
if (!['github', 'gitlab'].includes(action)) throw new UserError('Usage: kaloko ci github|gitlab [--scenario a.yml,b.yml] [--env preview] [--trigger deployment|pull_request] [--no-affected] [--out file] [--print] [--force]\n kaloko ci preview-url [--wait 600] [--github-env] [--dotenv file]')
|
|
1489
|
+
if (!['github', 'gitlab'].includes(action)) throw new UserError('Usage: kaloko ci github|gitlab [--scenario a.yml,b.yml] [--env preview] [--trigger deployment|pull_request] [--no-affected] [--out file] [--print] [--force]\n kaloko ci github --release [--snippet] [--print] [--force]\n kaloko ci preview-url [--wait 600] [--github-env] [--dotenv file]')
|
|
1394
1490
|
const { config, root, file } = await context(args)
|
|
1491
|
+
if (args.release) {
|
|
1492
|
+
// release-tool integrations: record each published release on Kaloko (docs/DOCS-CHANGELOG.md)
|
|
1493
|
+
if (action !== 'github') throw new UserError('--release writes a GitHub Actions workflow; on GitLab run kaloko release <version> --no-write in your release job.')
|
|
1494
|
+
const { productsOf, detectVersionSource } = await import('./lib/changelog.mjs')
|
|
1495
|
+
const { releaseWorkflow, releaseSnippet } = await import('./lib/ci.mjs')
|
|
1496
|
+
const products = productsOf(config, root)
|
|
1497
|
+
const sources = await Promise.all(products.map((p) => detectVersionSource(root, p)))
|
|
1498
|
+
const tool = sources.map((s) => s.tool).find(Boolean) || null
|
|
1499
|
+
let pkg = null
|
|
1500
|
+
try { pkg = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')) } catch {}
|
|
1501
|
+
const pm = existsSync(join(root, 'pnpm-lock.yaml')) ? 'pnpm' : existsSync(join(root, 'yarn.lock')) ? 'yarn' : existsSync(join(root, 'bun.lockb')) || existsSync(join(root, 'bun.lock')) ? 'bun' : 'npm'
|
|
1502
|
+
const dependency = !!(pkg && ({ ...pkg.dependencies, ...pkg.devDependencies }).kaloko)
|
|
1503
|
+
const version = `^${JSON.parse(await readFile(join(packageRoot, 'package.json'), 'utf8')).version}`
|
|
1504
|
+
if (args.snippet) { say(releaseSnippet({ tool, pm, dependency, version, product: products.length > 1 ? '<product>' : null })); return }
|
|
1505
|
+
const content = releaseWorkflow({ products: products.map((p, i) => ({ slug: products.length > 1 || !p.implicit ? p.slug : null, pattern: sources[i].pattern || 'v*' })), pm, dependency, version, tool })
|
|
1506
|
+
if (args.print) { say(content); return }
|
|
1507
|
+
const out = resolve(root, typeof args.out === 'string' ? args.out : '.github/workflows/kaloko-release.yml')
|
|
1508
|
+
if (existsSync(out) && !args.force) throw new UserError(`${relative(process.cwd(), out)} exists; pass --force to replace it (or --print to compare).`)
|
|
1509
|
+
await mkdir(dirname(out), { recursive: true })
|
|
1510
|
+
await writeFile(out, content)
|
|
1511
|
+
say(`Wrote ${relative(process.cwd(), out)}: every published GitHub release${tool ? ` (${tool} publishes them)` : ''} is recorded on Kaloko with its changelog entry, pictures and docs.`)
|
|
1512
|
+
say('Next:\n add the repository secret KALOKO_TOKEN (tester role, upload scope)\n publish releases with your release tool as before; nothing else changes in your release workflow')
|
|
1513
|
+
return
|
|
1514
|
+
}
|
|
1395
1515
|
const envName = typeof args.env === 'string' ? args.env : 'preview'
|
|
1396
1516
|
const all = (await scenarioFiles(config, root)).map((f) => relative(root, f))
|
|
1397
1517
|
const scenarios = list(args.scenario) || all
|
|
@@ -1568,6 +1688,7 @@ const commands = {
|
|
|
1568
1688
|
if (env.kind === 'design') {
|
|
1569
1689
|
const r = await syncDesign({ config, root, run, scenario, env, client, args: { ...args, strict: true }, say, pull: false, render: (steps) => walkDesign({ config, root, run, scenario, env, pick: steps, locales: run.meta.scope.locales, viewports: run.meta.scope.viewports, say }).then(() => evaluateRun({ run, scenario, config, root })) })
|
|
1570
1690
|
if (r.conflicts) return
|
|
1691
|
+
if (args['notes-preview']) { await previewNotes({ config, scenario, client, args, say }); return }
|
|
1571
1692
|
const v = await publishVersion({ config, scenario, client, args, say })
|
|
1572
1693
|
const meta = await readJson(join(run.dir, 'meta.json'))
|
|
1573
1694
|
meta.shared = { url: v.url, at: new Date().toISOString(), version: v.version, run: v.id }
|
|
@@ -1663,7 +1784,8 @@ const commands = {
|
|
|
1663
1784
|
},
|
|
1664
1785
|
|
|
1665
1786
|
async design (args) {
|
|
1666
|
-
if (args._[0]
|
|
1787
|
+
if (args._[0] === 'import') return designImport(args)
|
|
1788
|
+
if (args._[0] !== 'lint') throw new UserError('Usage: kaloko design lint [--env <design env> | --dir design/] [--scenario file] [--all] [--strict] [--json]\n kaloko design import figma <file-url|key|export folder> [--frames a,b] [--map frames.json] [--token-env FIGMA_TOKEN] [--publish]')
|
|
1667
1789
|
const { config, root } = await context(args)
|
|
1668
1790
|
let run = null
|
|
1669
1791
|
try { run = await resolveRun({ root, output: config.output, run: args.run }) } catch {}
|
|
@@ -1680,6 +1802,12 @@ const commands = {
|
|
|
1680
1802
|
if (report.errors || (args.strict && report.warnings)) process.exitCode = 1
|
|
1681
1803
|
},
|
|
1682
1804
|
|
|
1805
|
+
/** `kaloko screens [words]`: the screen library of the organization (newest capture of every step). */
|
|
1806
|
+
async screens (args) {
|
|
1807
|
+
const { config } = await context(args)
|
|
1808
|
+
await screensCmd({ config, client: service(config), args, say })
|
|
1809
|
+
},
|
|
1810
|
+
|
|
1683
1811
|
async history (args) {
|
|
1684
1812
|
const { config, root } = await context(args)
|
|
1685
1813
|
const scenarioId = await scenarioIdOf(args, config, root)
|
|
@@ -1781,6 +1909,20 @@ const commands = {
|
|
|
1781
1909
|
* The signed acceptance record of an accepted run: JSON and PDF downloaded and checked against the service's public
|
|
1782
1910
|
* key; --verify checks a record someone handed over (no token, no project needed).
|
|
1783
1911
|
*/
|
|
1912
|
+
/** Jira and Linear (#21): an issue from a step of a shared run, and the issues created so far. */
|
|
1913
|
+
async issue (args) {
|
|
1914
|
+
const { config, root } = await context(args)
|
|
1915
|
+
let runId = null
|
|
1916
|
+
if (args._[0] === 'create' && typeof args.run !== 'string') { try { const r = await resolveRun({ root, output: config.output }); if (r.meta.shared) runId = r.meta.id } catch {} }
|
|
1917
|
+
return issueCmd({ args, config, client: service(config), runId, say })
|
|
1918
|
+
},
|
|
1919
|
+
|
|
1920
|
+
/** Hosted runs (#16): schedules of read-only scenarios on Kaloko's browsers, or a scheduled GitHub workflow. */
|
|
1921
|
+
async schedule (args) {
|
|
1922
|
+
const { config, root } = await context(args)
|
|
1923
|
+
return scheduleCmd({ args, config, root, say, deps: { loadScenario, environment, baseUrl, projectAndIssue, packageRoot, service } })
|
|
1924
|
+
},
|
|
1925
|
+
|
|
1784
1926
|
async signoff (args) {
|
|
1785
1927
|
if (typeof args.verify === 'string') {
|
|
1786
1928
|
const config = await context(args).then((c) => c.config).catch(() => ({}))
|
|
@@ -2039,6 +2181,27 @@ const commands = {
|
|
|
2039
2181
|
return
|
|
2040
2182
|
}
|
|
2041
2183
|
if (typeof args.scenario === 'string') return exportScenario({ args, config, root, client })
|
|
2184
|
+
if (typeof args.format === 'string') {
|
|
2185
|
+
// living docs and the changelog as files (docs/DOCS-CHANGELOG.md → Exports)
|
|
2186
|
+
if (!config.org) throw new UserError('Set org: in the config first.')
|
|
2187
|
+
const { pickProduct } = await import('./lib/changelog.mjs')
|
|
2188
|
+
const { exportDocs } = await import('./lib/docs.mjs')
|
|
2189
|
+
const product = pickProduct(config, root, args.product)
|
|
2190
|
+
const printPdf = async (htmlFile, outFile) => {
|
|
2191
|
+
const { chromium } = await import('playwright')
|
|
2192
|
+
const browser = await chromium.launch(config.browser?.channel ? { channel: config.browser.channel } : {}).catch(() => chromium.launch())
|
|
2193
|
+
try {
|
|
2194
|
+
const page = await browser.newPage()
|
|
2195
|
+
await page.goto(pathToFileURL(htmlFile).href, { waitUntil: 'load', timeout: 60000 })
|
|
2196
|
+
await page.pdf({ path: outFile, format: 'A4', printBackground: true, margin: { top: '16mm', bottom: '16mm', left: '14mm', right: '14mm' } })
|
|
2197
|
+
} finally { await browser.close() }
|
|
2198
|
+
}
|
|
2199
|
+
const r = await exportDocs({ root, config, product, client, format: args.format, version: typeof args.version === 'string' ? args.version : 'latest', lang: typeof args.lang === 'string' ? args.lang : null, what: ['docs', 'changelog'].includes(args.what) ? args.what : 'all', out: typeof args.out === 'string' ? resolve(args.out) : null, local: !!args.local, printPdf: args['server-pdf'] ? null : printPdf, say })
|
|
2200
|
+
if (r.kind === 'file') { say(`Exported ${product.slug} as ${r.format} → ${relative(process.cwd(), r.file) || r.file}`); return }
|
|
2201
|
+
const m = r.manifest
|
|
2202
|
+
say(`Exported ${product.slug} ${m?.version || ''} as ${r.format} → ${relative(process.cwd(), r.dir) || r.dir}\n ${m ? `${m.pages} page(s), ${m.releases} release(s), ${m.pictures} picture(s)` : `${r.files} file(s)`}${m?.missing?.length ? `\n ! no accepted picture for: ${m.missing.slice(0, 8).join(', ')}${m.missing.length > 8 ? ' …' : ''}` : ''}`)
|
|
2203
|
+
return
|
|
2204
|
+
}
|
|
2042
2205
|
let runId = args.run
|
|
2043
2206
|
if (!runId) { try { runId = (await resolveRun({ root, output: config.output })).meta.id } catch {} }
|
|
2044
2207
|
if (!runId || !/^[0-9]{4}-[0-9]{2}-[0-9]{2}-[a-z0-9-]+$/.test(runId)) throw new UserError('--run <id> is required (a shared run id).')
|
|
@@ -2095,6 +2258,37 @@ const commands = {
|
|
|
2095
2258
|
say(draftText(d))
|
|
2096
2259
|
},
|
|
2097
2260
|
|
|
2261
|
+
/** `kaloko docs publish|check`: the docs folder on Kaloko per version, and what in it needs attention. */
|
|
2262
|
+
async docs (args) {
|
|
2263
|
+
const sub = args._[0]
|
|
2264
|
+
if (!['publish', 'check'].includes(sub)) throw new UserError('Usage: kaloko docs publish [--product p] [--version latest|<v>] [--reviewed] [--dry-run] [--json]\n kaloko docs check [--product p] [--version v] [--offline] [--strict] [--json]')
|
|
2265
|
+
const { config, root } = await context(args)
|
|
2266
|
+
const { pickProduct } = await import('./lib/changelog.mjs')
|
|
2267
|
+
const docs = await import('./lib/docs.mjs')
|
|
2268
|
+
const product = pickProduct(config, root, args.product)
|
|
2269
|
+
const version = typeof args.version === 'string' ? args.version : 'latest'
|
|
2270
|
+
if (sub === 'publish') {
|
|
2271
|
+
if (!config.org && !args['dry-run']) throw new UserError('Set org: in the config first.')
|
|
2272
|
+
const r = await docs.publishDocs({ root, config, product, client: args['dry-run'] ? null : service(config), version, reviewed: !!args.reviewed, dryRun: !!args['dry-run'] })
|
|
2273
|
+
if (args.json) { say(JSON.stringify(r, null, 2)); return }
|
|
2274
|
+
if (r.dry_run) { say(`${product.slug}: ${r.pages} page(s) from ${r.dir} would go to Kaloko as ${r.version} (${r.languages.join(', ')}); dry run, nothing sent.${r.broken.length ? `\n ! broken step references: ${r.broken.join('; ')}` : ''}`); return }
|
|
2275
|
+
const lines = [`${product.slug} docs ${r.version}: ${r.pages} page(s) from ${r.dir}, ${r.changed} changed, ${r.unchanged} unchanged${r.removed ? `, ${r.removed} removed` : ''}${args.reviewed ? ' (marked as checked)' : ''}`, ` ${r.url}`]
|
|
2276
|
+
for (const b of r.broken_refs || []) lines.push(` ! broken step reference in ${b.path || 'index'}${b.lang ? ` (${b.lang})` : ''}: ${b.ref}`)
|
|
2277
|
+
for (const u of (r.unresolved || []).slice(0, 10)) lines.push(` ! no accepted picture yet: ${u.ref} (${u.path || 'index'})`)
|
|
2278
|
+
for (const n of r.notes || []) lines.push(` ! ${n}`)
|
|
2279
|
+
say(lines.join('\n'))
|
|
2280
|
+
return
|
|
2281
|
+
}
|
|
2282
|
+
// check: the scenario files say which steps exist
|
|
2283
|
+
const scenarios = new Map()
|
|
2284
|
+
for (const f of await scenarioFiles(config, root).catch(() => [])) { try { const s = yaml.load(await readFile(f, 'utf8')) || {}; if (s.id) scenarios.set(String(s.id), new Set((s.nodes || []).map((n) => String(n.id)))) } catch {} }
|
|
2285
|
+
let client = null
|
|
2286
|
+
if (!args.offline) { try { client = service(config) } catch {} }
|
|
2287
|
+
const r = await docs.docsCheck({ root, config, product, client, scenarios: scenarios.size ? scenarios : null, version })
|
|
2288
|
+
say(args.json ? JSON.stringify(r, null, 2) : docs.formatCheck(r))
|
|
2289
|
+
if (!r.ok || (args.strict && r.warnings)) process.exitCode = 1
|
|
2290
|
+
},
|
|
2291
|
+
|
|
2098
2292
|
/** `kaloko release <version>`: the version's changelog entry and its reference runs on the service. */
|
|
2099
2293
|
async release (args) {
|
|
2100
2294
|
const { config, root } = await context(args)
|
|
@@ -2103,6 +2297,14 @@ const commands = {
|
|
|
2103
2297
|
const product = pickProduct(config, root, args.product)
|
|
2104
2298
|
const client = args['dry-run'] ? null : service(config)
|
|
2105
2299
|
const r = await releaseProduct({ root, config, product, version: args._[0] || null, date: typeof args.date === 'string' ? args.date : null, client, write: !args['no-write'], runs: list(args.runs), dryRun: !!args['dry-run'] })
|
|
2300
|
+
// the docs of this version: what the docs folder says now (Business; latest stays as kaloko docs publish left it)
|
|
2301
|
+
if (r.release && !args['no-docs']) {
|
|
2302
|
+
const docs = await import('./lib/docs.mjs')
|
|
2303
|
+
const pages = await docs.collectDocs(root, product).then((x) => x.pages.length).catch(() => 0)
|
|
2304
|
+
if (pages) {
|
|
2305
|
+
try { r.docs = await docs.publishDocs({ root, config, product, client, version: r.version }) } catch (e) { r.docs = { error: String(e.message || e).split('\n')[0] } }
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2106
2308
|
if (args.json) { say(JSON.stringify(r, null, 2)); return }
|
|
2107
2309
|
const file = relative(process.cwd(), r.file) || r.file
|
|
2108
2310
|
const lines = [`${product.slug} ${r.version} (${r.date}) · version source: ${r.source.tool || r.source.source}${r.source.path ? ` (${r.source.path})` : ''}`]
|
|
@@ -2111,6 +2313,7 @@ const commands = {
|
|
|
2111
2313
|
lines.push(` ${r.release.created ? 'Recorded' : 'Updated'} on the service with ${r.release.runs.length} reference run(s)${r.release.versioned_runs ? `; ${r.release.versioned_runs} accepted run(s) now carry ${r.version}` : ''}`)
|
|
2112
2314
|
lines.push(` ${r.release.url}`)
|
|
2113
2315
|
} else lines.push(' Dry run: nothing sent.')
|
|
2316
|
+
if (r.docs) lines.push(r.docs.error ? ` ! Docs of ${r.version} not published: ${r.docs.error}` : ` Docs of ${r.version}: ${r.docs.pages} page(s)${r.docs.changed ? `, ${r.docs.changed} changed since the last publish of ${r.version}` : ''}`)
|
|
2114
2317
|
for (const n of r.notes) lines.push(` ! ${n}`)
|
|
2115
2318
|
if (r.finalized) lines.push(` Commit ${file} with the release.`)
|
|
2116
2319
|
say(lines.join('\n'))
|
package/src/lib/changelog.mjs
CHANGED
|
@@ -259,7 +259,7 @@ export async function releaseProduct ({ root, config, product, version = null, d
|
|
|
259
259
|
const tag = tagFor(src.pattern, v)
|
|
260
260
|
const commit = (await tagExists(root, tag)) ? ((await out('git', ['rev-list', '-n', '1', tag], root)) || '').trim() : ((await out('git', ['rev-parse', 'HEAD'], root)) || '').trim()
|
|
261
261
|
const payload = {
|
|
262
|
-
product:
|
|
262
|
+
product: await productPayload(root, product, src),
|
|
263
263
|
release: { product: product.slug, version: v, date: section?.date || day, changelog: md, source_hash: sha256(md), tag: (await tagExists(root, tag)) ? tag : null, commit: /^[0-9a-f]{40}$/.test(commit) ? commit : null, ...(runs ? { runs } : {}) }
|
|
264
264
|
}
|
|
265
265
|
if (dryRun) return { version: v, date: payload.release.date, file, section: !!section, finalized, source: src, notes, payload, items: parseEntry(md).groups.reduce((n, g) => n + g.items.length, 0), refs: refs.length }
|
|
@@ -268,6 +268,11 @@ export async function releaseProduct ({ root, config, product, version = null, d
|
|
|
268
268
|
const rec = await client.request('POST', `/orgs/${config.org}/releases`, payload.release)
|
|
269
269
|
return { version: v, date: payload.release.date, file, section: !!section, finalized, source: src, notes, release: rec, product: saved.product, items: parseEntry(md).groups.reduce((n, g) => n + g.items.length, 0), refs: refs.length }
|
|
270
270
|
}
|
|
271
|
+
/** The product as the service records it (POST /products): from the config, with the version source found. */
|
|
272
|
+
export async function productPayload (root, product, src) {
|
|
273
|
+
return { slug: product.slug, name: product.name, projects: product.projects, modules: product.modules, reference_env: product.reference_env, default_scenario: product.default_scenario, implicit: product.implicit, version_source: { source: src.source, ...(src.path ? { path: src.path } : {}), pattern: src.pattern, ...(src.tool ? { tool: src.tool } : {}) }, ...(product.publish ? { publish: product.publish } : {}), ...(await repoUrl(root)) }
|
|
274
|
+
}
|
|
275
|
+
|
|
271
276
|
async function repoUrl (root) {
|
|
272
277
|
const r = await repoSlug(root)
|
|
273
278
|
return r?.host === 'github' ? { repo: `https://github.com/${r.slug}` } : r?.host === 'gitlab' ? { repo: `https://gitlab.com/${r.slug}` } : {}
|
|
@@ -276,8 +281,9 @@ async function repoUrl (root) {
|
|
|
276
281
|
// ---------- init and doctor ----------
|
|
277
282
|
|
|
278
283
|
/** The config block `kaloko init --docs=changelog|full` adds (comments say what each line does). */
|
|
279
|
-
export function docsConfigBlock ({ products = null } = {}) {
|
|
284
|
+
export function docsConfigBlock ({ products = null, full = false } = {}) {
|
|
280
285
|
const lines = ['', '# Visual changelog: kaloko changelog draft writes the Unreleased entry with step references,', '# kaloko release <version> records the release and its pictures (docs/DOCS-CHANGELOG.md on kaloko.app)', 'docs:', ' changelog: CHANGELOG.md', ' version_source: auto # git-tag | package.json | release-please | changesets | semantic-release']
|
|
286
|
+
if (full) lines.push(' dir: docs # guides with step references; kaloko docs publish sends them, kaloko docs check checks them', ' # languages: [en, cs] # the first is the originals\' language; translations sit next to them (guide.cs.md)')
|
|
281
287
|
if (products?.length) {
|
|
282
288
|
lines.push('# one product per app in this repository; scenarios say which one they show (product: <slug>)', 'products:')
|
|
283
289
|
for (const p of products) lines.push(` ${p.slug}:`, ` name: ${JSON.stringify(p.name)}`, ` version: { source: package.json, path: ${p.path} }`, ` docs: { changelog: ${p.dir}/CHANGELOG.md }`)
|
|
@@ -298,13 +304,73 @@ export async function detectProducts (root) {
|
|
|
298
304
|
return out.length > 1 ? out : []
|
|
299
305
|
}
|
|
300
306
|
|
|
301
|
-
/**
|
|
307
|
+
/**
|
|
308
|
+
* Diátaxis folders and an index for `kaloko init --docs=full` (only what is missing). Each folder's README says what
|
|
309
|
+
* belongs there and carries a template in a comment (comments never show on Kaloko or in exports).
|
|
310
|
+
*/
|
|
302
311
|
export const DOCS_SKELETON = {
|
|
303
|
-
'docs/index.md': '# Documentation\n\nGuides for the people who use the product. Pictures are step references\n(``) that always show the newest accepted screen.\n\n- [Tutorials](tutorials/)
|
|
304
|
-
'docs/tutorials/README.md':
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
312
|
+
'docs/index.md': '# Documentation\n\nGuides for the people who use the product. Pictures are step references\n(``) that always show the newest accepted screen.\n\n- [Tutorials](tutorials/README.md): learn by doing, start to finish\n- [How-to guides](how-to/README.md): one task each\n- [Reference](reference/README.md): the facts to look up\n- [Explanation](explanation/README.md): why it works this way\n',
|
|
313
|
+
'docs/tutorials/README.md': `# Tutorials
|
|
314
|
+
|
|
315
|
+
A tutorial takes a newcomer through a first success, start to finish, on a path that always works.
|
|
316
|
+
|
|
317
|
+
<!-- Template (copy into a new file, e.g. first-order.md):
|
|
318
|
+
---
|
|
319
|
+
title: Your first order
|
|
320
|
+
order: 1
|
|
321
|
+
---
|
|
322
|
+
# Your first order
|
|
323
|
+
|
|
324
|
+
In ten minutes you will have placed an order and seen it in your account.
|
|
325
|
+
|
|
326
|
+
1. Open the shop and put a tea in the cart.
|
|
327
|
+

|
|
328
|
+
2. Choose **Checkout** and pay with a test card.
|
|
329
|
+

|
|
330
|
+
|
|
331
|
+
You have placed your first order. Next: [track it](../how-to/track-an-order.md).
|
|
332
|
+
-->
|
|
333
|
+
`,
|
|
334
|
+
'docs/how-to/README.md': `# How-to guides
|
|
335
|
+
|
|
336
|
+
One task per page, for someone who already knows what they want to do.
|
|
337
|
+
|
|
338
|
+
<!-- Template (copy into a new file, e.g. pay-with-apple-pay.md):
|
|
339
|
+
# Pay with Apple Pay
|
|
340
|
+
|
|
341
|
+
1. Open the cart and choose **Checkout**.
|
|
342
|
+

|
|
343
|
+
2. Choose **Apple Pay** above the card form and confirm on your phone or Mac.
|
|
344
|
+

|
|
345
|
+
|
|
346
|
+
If the button is missing, your browser has no Apple Pay set up.
|
|
347
|
+
-->
|
|
348
|
+
`,
|
|
349
|
+
'docs/reference/README.md': `# Reference
|
|
350
|
+
|
|
351
|
+
Facts to look up: fields, limits, settings, keyboard shortcuts. Tables and lists, no story.
|
|
352
|
+
|
|
353
|
+
<!-- Template (copy into a new file, e.g. order-states.md):
|
|
354
|
+
# Order states
|
|
355
|
+
|
|
356
|
+
| State | What it means |
|
|
357
|
+
|---|---|
|
|
358
|
+
| Paid | The payment went through; the order waits for packing. |
|
|
359
|
+
| Shipped | The parcel left the warehouse. |
|
|
360
|
+
|
|
361
|
+

|
|
362
|
+
-->
|
|
363
|
+
`,
|
|
364
|
+
'docs/explanation/README.md': `# Explanation
|
|
365
|
+
|
|
366
|
+
Why things work the way they do: background, choices, trade-offs.
|
|
367
|
+
|
|
368
|
+
<!-- Template (copy into a new file, e.g. why-we-hold-payments.md):
|
|
369
|
+
# Why a payment waits a day
|
|
370
|
+
|
|
371
|
+
Card payments are confirmed the next morning, so a cancelled order costs the customer nothing.
|
|
372
|
+
-->
|
|
373
|
+
`
|
|
308
374
|
}
|
|
309
375
|
|
|
310
376
|
/**
|
package/src/lib/ci.mjs
CHANGED
|
@@ -209,3 +209,74 @@ export function addPreviewEnvironment (text, name = 'preview') {
|
|
|
209
209
|
const at = m.index + m[0].length
|
|
210
210
|
return text.slice(0, at) + '\n' + block + text.slice(at)
|
|
211
211
|
}
|
|
212
|
+
|
|
213
|
+
// ---------- releases (docs/DOCS-CHANGELOG.md → release-tool integrations) ----------
|
|
214
|
+
|
|
215
|
+
const TOOL_SNIPPETS = {
|
|
216
|
+
'release-please': { after: 'googleapis/release-please-action (id: release)', when: '${{ steps.release.outputs.release_created }}', version: '${{ steps.release.outputs.version }}' },
|
|
217
|
+
changesets: { after: 'changesets/action (id: changesets)', when: "${{ steps.changesets.outputs.published == 'true' }}", version: '' },
|
|
218
|
+
'semantic-release': { after: 'npx semantic-release', when: null, version: '' }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* .github/workflows/kaloko-release.yml: when a GitHub release is published (release-please, semantic-release with
|
|
223
|
+
* @semantic-release/github and changesets all publish one) or by hand, `kaloko release <version> --no-write` records
|
|
224
|
+
* it on Kaloko: the entry the release tool wrote, the accepted runs that are its pictures, and the docs of that
|
|
225
|
+
* version. `products`: [{ slug, pattern }] (tag patterns such as v* or shop-v*); several products get one step each.
|
|
226
|
+
*/
|
|
227
|
+
export function releaseWorkflow ({ products = [{ slug: null, pattern: 'v*' }], pm = 'npm', dependency = true, version = 'latest', node = 22, tool = null }) {
|
|
228
|
+
const { install, kaloko: k } = runner({ pm, dependency, version })
|
|
229
|
+
const several = products.length > 1
|
|
230
|
+
const steps = products.map((p) => {
|
|
231
|
+
const prefix = String(p.pattern || 'v*').split('*')[0]
|
|
232
|
+
const flag = several || p.slug ? ` --product ${q(p.slug)}` : ''
|
|
233
|
+
return ` - name: Record ${p.slug || 'the release'} on Kaloko
|
|
234
|
+
${several ? ` if: startsWith(env.TAG, '${prefix}')\n` : ''} run: ${k} release "\${TAG#${prefix}}"${flag} --no-write`
|
|
235
|
+
}).join('\n')
|
|
236
|
+
return `# Kaloko: record every release on Kaloko once your release tool has published it: its changelog entry with
|
|
237
|
+
# before/after pictures, the accepted runs that are its reference, and the docs of that version.
|
|
238
|
+
# Written by kaloko ci github --release${tool ? ` (${tool} found)` : ''}. Secret: KALOKO_TOKEN (tester role, upload scope).
|
|
239
|
+
# kaloko release --no-write only reads CHANGELOG.md; the release tool keeps writing it.
|
|
240
|
+
name: Kaloko release
|
|
241
|
+
on:
|
|
242
|
+
release:
|
|
243
|
+
types: [published]
|
|
244
|
+
workflow_dispatch:
|
|
245
|
+
inputs:
|
|
246
|
+
tag:
|
|
247
|
+
description: Tag of the release to record (e.g. v2.4.0)
|
|
248
|
+
required: true
|
|
249
|
+
permissions:
|
|
250
|
+
contents: read
|
|
251
|
+
jobs:
|
|
252
|
+
record:
|
|
253
|
+
runs-on: ubuntu-latest
|
|
254
|
+
timeout-minutes: 10
|
|
255
|
+
env:
|
|
256
|
+
KALOKO_TOKEN: \${{ secrets.KALOKO_TOKEN }}
|
|
257
|
+
GH_TOKEN: \${{ github.token }}
|
|
258
|
+
KALOKO_AGENT: github-actions
|
|
259
|
+
TAG: \${{ github.event.release.tag_name || inputs.tag }}
|
|
260
|
+
steps:
|
|
261
|
+
- uses: actions/checkout@v4
|
|
262
|
+
with:
|
|
263
|
+
ref: \${{ github.event.release.tag_name || inputs.tag }}
|
|
264
|
+
fetch-depth: 0 # the tags and the history since the previous release
|
|
265
|
+
- uses: actions/setup-node@v4
|
|
266
|
+
with:
|
|
267
|
+
node-version: ${node}
|
|
268
|
+
${install ? `${['pnpm', 'yarn'].includes(pm) ? ' - run: corepack enable\n' : ''} - run: ${install}\n` : ''}${steps}
|
|
269
|
+
`
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** The step to paste into an existing release workflow, right after the release tool's own step. */
|
|
273
|
+
export function releaseSnippet ({ tool = null, pm = 'npm', dependency = true, version = 'latest', product = null }) {
|
|
274
|
+
const { kaloko: k } = runner({ pm, dependency, version })
|
|
275
|
+
const s = TOOL_SNIPPETS[tool] || { after: 'the step that publishes the release', when: null, version: '' }
|
|
276
|
+
return `# after ${s.after}:
|
|
277
|
+
- name: Record the release on Kaloko
|
|
278
|
+
${s.when ? ` if: ${s.when}\n` : ''} run: ${k} release${s.version ? ` ${s.version}` : ''}${product ? ` --product ${q(product)}` : ''} --no-write
|
|
279
|
+
env:
|
|
280
|
+
KALOKO_TOKEN: \${{ secrets.KALOKO_TOKEN }}
|
|
281
|
+
`
|
|
282
|
+
}
|
package/src/lib/design-cli.mjs
CHANGED
|
@@ -297,11 +297,31 @@ export async function syncDesign ({ config, root, run, scenario, env, client, ar
|
|
|
297
297
|
return { pushed: toPush.length, pulled: toPull.length, conflicts: conflicts.length ? conflicts : null, seq: after.seq }
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
-
/**
|
|
300
|
+
/** The notes an agent or person wrote for a version: --notes <file> or --notes - (stdin); null without the option. */
|
|
301
|
+
export async function readNotesArg (value, stdin = process.stdin) {
|
|
302
|
+
if (value == null || value === false) return null
|
|
303
|
+
if (value === true) throw new UserError('--notes takes a file (Markdown, text or JSON) or - for standard input.')
|
|
304
|
+
if (value === '-') { const chunks = []; for await (const c of stdin) chunks.push(c); return Buffer.concat(chunks.map((c) => Buffer.from(c))).toString('utf8') }
|
|
305
|
+
try { return await readFile(value, 'utf8') } catch { throw new UserError(`--notes: cannot read ${value}.`) }
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** What publishing the draft now would write as "what changed" (kaloko share --notes-preview). */
|
|
309
|
+
export async function previewNotes ({ config, scenario, client, args, say }) {
|
|
310
|
+
const branch = typeof args.branch === 'string' ? args.branch : 'main'
|
|
311
|
+
const r = await client.request('GET', `/orgs/${config.org}/scenarios/${enc(scenario.id)}/versions/draft/notes?branch=${enc(branch)}`)
|
|
312
|
+
if (args.json) { say(JSON.stringify(r, null, 2)); return r }
|
|
313
|
+
const { notesText } = await import('./design-notes.mjs')
|
|
314
|
+
say(`What publishing now would say (${r.changed} changed, ${r.unchanged} unchanged${r.notes?.from ? ` since v${r.notes.from.version}` : ''}):\n\n${notesText(r.notes, { lang: args.lang === 'cs' ? 'cs' : 'en' })}\n\nWrite your own in a file (a summary, then "step-id: what changed" lines) and publish with kaloko share --notes <file>.`)
|
|
315
|
+
return r
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Publishes the draft head as a version (a run of the design environment), with "what changed" notes. */
|
|
301
319
|
export async function publishVersion ({ config, scenario, client, args, say }) {
|
|
302
320
|
const branch = typeof args.branch === 'string' ? args.branch : 'main'
|
|
303
|
-
const
|
|
321
|
+
const notes = await readNotesArg(args.notes)
|
|
322
|
+
const r = await client.request('POST', `/orgs/${config.org}/scenarios/${enc(scenario.id)}/versions`, { branch, ...(typeof args.name === 'string' ? { name: args.name } : {}), ...(notes ? { notes } : {}) })
|
|
304
323
|
say(`Version v${r.version}${r.name ? ` "${r.name}"` : ''} · ${r.changed} changed, ${r.unchanged} unchanged${r.changed_steps.length ? ` (${r.changed_steps.join(', ')})` : ''}\n${r.url}`)
|
|
324
|
+
if (r.notes) { const { notesText } = await import('./design-notes.mjs'); say(`\nWhat changed${notes ? ' (your notes over the generated ones)' : ''}:\n${notesText(r.notes)}`) }
|
|
305
325
|
return r
|
|
306
326
|
}
|
|
307
327
|
|