@ultimat3/cli 20.1.0 → 20.1.2
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/package.json +29 -29
- package/src/e2e-driver.ts +2 -1
- package/src/messages.ts +8 -0
- package/src/output.ts +29 -1
- package/src/templates/github/ci.yml.ts +18 -0
- package/src/templates/scaffold-repo.ts +3 -1
- package/src/verify-run.ts +13 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/cli",
|
|
3
|
-
"version": "20.1.
|
|
3
|
+
"version": "20.1.2",
|
|
4
4
|
"description": "The `x` binary: new, dev, build, verify, generate, db, mcp, doctor, deploy",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,34 +37,34 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@babel/core": "^7.28.4",
|
|
40
|
-
"@ultimat3/action": "20.1.
|
|
41
|
-
"@ultimat3/admin": "20.1.
|
|
42
|
-
"@ultimat3/ai": "20.1.
|
|
43
|
-
"@ultimat3/auth": "20.1.
|
|
44
|
-
"@ultimat3/cache": "20.1.
|
|
45
|
-
"@ultimat3/core": "20.1.
|
|
46
|
-
"@ultimat3/db": "20.1.
|
|
47
|
-
"@ultimat3/entity": "20.1.
|
|
48
|
-
"@ultimat3/flags": "20.1.
|
|
49
|
-
"@ultimat3/http": "20.1.
|
|
50
|
-
"@ultimat3/i18n": "20.1.
|
|
51
|
-
"@ultimat3/jobs": "20.1.
|
|
52
|
-
"@ultimat3/mail": "20.1.
|
|
53
|
-
"@ultimat3/manifest": "20.1.
|
|
54
|
-
"@ultimat3/mcp": "20.1.
|
|
55
|
-
"@ultimat3/money": "20.1.
|
|
56
|
-
"@ultimat3/notify": "20.1.
|
|
57
|
-
"@ultimat3/policy": "20.1.
|
|
58
|
-
"@ultimat3/pwa": "20.1.
|
|
59
|
-
"@ultimat3/query": "20.1.
|
|
60
|
-
"@ultimat3/realtime": "20.1.
|
|
61
|
-
"@ultimat3/render": "20.1.
|
|
62
|
-
"@ultimat3/schema": "20.1.
|
|
63
|
-
"@ultimat3/scraping": "20.1.
|
|
64
|
-
"@ultimat3/seo": "20.1.
|
|
65
|
-
"@ultimat3/storage": "20.1.
|
|
66
|
-
"@ultimat3/testing": "20.1.
|
|
67
|
-
"@ultimat3/time": "20.1.
|
|
40
|
+
"@ultimat3/action": "20.1.2",
|
|
41
|
+
"@ultimat3/admin": "20.1.2",
|
|
42
|
+
"@ultimat3/ai": "20.1.2",
|
|
43
|
+
"@ultimat3/auth": "20.1.2",
|
|
44
|
+
"@ultimat3/cache": "20.1.2",
|
|
45
|
+
"@ultimat3/core": "20.1.2",
|
|
46
|
+
"@ultimat3/db": "20.1.2",
|
|
47
|
+
"@ultimat3/entity": "20.1.2",
|
|
48
|
+
"@ultimat3/flags": "20.1.2",
|
|
49
|
+
"@ultimat3/http": "20.1.2",
|
|
50
|
+
"@ultimat3/i18n": "20.1.2",
|
|
51
|
+
"@ultimat3/jobs": "20.1.2",
|
|
52
|
+
"@ultimat3/mail": "20.1.2",
|
|
53
|
+
"@ultimat3/manifest": "20.1.2",
|
|
54
|
+
"@ultimat3/mcp": "20.1.2",
|
|
55
|
+
"@ultimat3/money": "20.1.2",
|
|
56
|
+
"@ultimat3/notify": "20.1.2",
|
|
57
|
+
"@ultimat3/policy": "20.1.2",
|
|
58
|
+
"@ultimat3/pwa": "20.1.2",
|
|
59
|
+
"@ultimat3/query": "20.1.2",
|
|
60
|
+
"@ultimat3/realtime": "20.1.2",
|
|
61
|
+
"@ultimat3/render": "20.1.2",
|
|
62
|
+
"@ultimat3/schema": "20.1.2",
|
|
63
|
+
"@ultimat3/scraping": "20.1.2",
|
|
64
|
+
"@ultimat3/seo": "20.1.2",
|
|
65
|
+
"@ultimat3/storage": "20.1.2",
|
|
66
|
+
"@ultimat3/testing": "20.1.2",
|
|
67
|
+
"@ultimat3/time": "20.1.2",
|
|
68
68
|
"babel-preset-solid": "^1.9.15"
|
|
69
69
|
}
|
|
70
70
|
}
|
package/src/e2e-driver.ts
CHANGED
|
@@ -70,7 +70,8 @@ export const e2eFixtures = (page: PageLike, browser: E2eBrowserPage): E2eFixture
|
|
|
70
70
|
* declaration `driverFixtures()` registered — the ordinary way a driver arrives, last registration
|
|
71
71
|
* wins — so every `test('…', async ({ page }) => …)` in the suite gets a browser. `useE2eDriver`
|
|
72
72
|
* is the other half: it is what makes `hasE2eDriver()` answer true and stops `e2eTest` becoming a
|
|
73
|
-
* `test.skip`
|
|
73
|
+
* `test.skip` — which the gate now reports as a SKIPPED step rather than the green check it
|
|
74
|
+
* printed until #434, and which a repo whose `x.verify.json` names `e2e` gets red for.
|
|
74
75
|
*
|
|
75
76
|
* `budget`, `signIn` and `deploy` are deliberately NOT registered here. Each needs something a
|
|
76
77
|
* page cannot supply — byte counts off a built `dist/`, an app's own sign-in route, a second build
|
package/src/messages.ts
CHANGED
|
@@ -279,6 +279,14 @@ const CATALOG = {
|
|
|
279
279
|
// The `errors` step's own coverage, in `output`: a scan without a parser reads most fix lines
|
|
280
280
|
// and not all of them, and a step that reports findings alone claims a completeness it lacks.
|
|
281
281
|
'cli.verify.fixCoverage': 'checked {checked} fix line(s), could not read {unreadable}',
|
|
282
|
+
// Why a skipped step is a dash when the step can say so: the suite exists and not one of its
|
|
283
|
+
// tests ran. Each test's own name carries the reason it skipped, and `bun test` prints only the
|
|
284
|
+
// counts — so the line says how many and `x test <type>` prints the names.
|
|
285
|
+
'cli.verify.allSkipped': 'found {skipped} test(s) and every one skipped itself',
|
|
286
|
+
// The other shape the same counts can take: a suite that reported no test at all, skipped ones
|
|
287
|
+
// included. Its own line and not `allSkipped` with a zero, because the repair differs — there is
|
|
288
|
+
// no skipped test to read a reason off, so the files the step selected hold no test.
|
|
289
|
+
'cli.verify.ranNothing': 'found no test to run in the file(s) it selected',
|
|
282
290
|
'cli.verify.serial': 'serial',
|
|
283
291
|
'cli.verify.workers': '{workers} workers',
|
|
284
292
|
'cli.env.checked': '{count} declared variable(s), all present and valid',
|
package/src/output.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { ERROR_DOCS_URL, renderThrowable, singleLine, stringField } from '@ultimat3/core';
|
|
6
6
|
import { msg } from './messages';
|
|
7
|
+
import type { TestCounts } from './test-counts';
|
|
7
8
|
|
|
8
9
|
export interface Finding {
|
|
9
10
|
readonly code: string;
|
|
@@ -24,6 +25,13 @@ export interface StepResult {
|
|
|
24
25
|
readonly output?: string;
|
|
25
26
|
/** Worker processes the step used; `1` means it ran serially. Absent for a non-test step. */
|
|
26
27
|
readonly workers?: number;
|
|
28
|
+
/**
|
|
29
|
+
* What the step's suite executed. Absent for a step that spawned no test process, which is NOT
|
|
30
|
+
* the same state as `{ ran: 0 }` — a step with no suite and a suite whose every test skipped
|
|
31
|
+
* itself both report `skipped`, and this is the only thing that tells a reader which one it is
|
|
32
|
+
* looking at (#434).
|
|
33
|
+
*/
|
|
34
|
+
readonly tests?: TestCounts;
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
export type JsonValue =
|
|
@@ -164,10 +172,27 @@ const width = (step: StepResult): string => {
|
|
|
164
172
|
return ` ${step.workers === 1 ? msg('cli.verify.serial') : msg('cli.verify.workers', { workers: step.workers })}`;
|
|
165
173
|
};
|
|
166
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Why a step is a dash, when the step itself can say. `- roadmap` is "there is nothing here to
|
|
177
|
+
* check"; a step that spawned a suite and executed none of it is a different state, which is what
|
|
178
|
+
* issue #434 reported as a green check — so a step carrying counts always says which it is, in one
|
|
179
|
+
* of the two shapes its counts can take. The reason each test skipped lives in that test's own
|
|
180
|
+
* NAME — `bun test` prints only the counts — so the line points at the suite and `x test <type>`
|
|
181
|
+
* is what prints the names.
|
|
182
|
+
*/
|
|
183
|
+
const why = (step: StepResult): string => {
|
|
184
|
+
const tests = step.tests;
|
|
185
|
+
if (step.skipped !== true || tests === undefined) return '';
|
|
186
|
+
if (tests.skipped === 0) return ` ${msg('cli.verify.ranNothing')}`;
|
|
187
|
+
return ` ${msg('cli.verify.allSkipped', { skipped: tests.skipped })}`;
|
|
188
|
+
};
|
|
189
|
+
|
|
167
190
|
export function renderHuman(result: CommandResult, verbose = false): string {
|
|
168
191
|
const out: string[] = [];
|
|
169
192
|
for (const step of result.steps ?? []) {
|
|
170
|
-
out.push(
|
|
193
|
+
out.push(
|
|
194
|
+
` ${mark(step)} ${step.name.padEnd(18)} ${step.durationMs}ms${width(step)}${why(step)}`,
|
|
195
|
+
);
|
|
171
196
|
for (const finding of step.findings) out.push(renderFinding(finding, ' '));
|
|
172
197
|
// NOT escaped, and that is the one exception: `output` is this process's own captured
|
|
173
198
|
// subprocess stdout — `bun test`'s colour is the reason a human reads it at all, and it is
|
|
@@ -196,6 +221,9 @@ export function renderJson(result: CommandResult): string {
|
|
|
196
221
|
skipped: step.skipped === true,
|
|
197
222
|
findings: step.findings,
|
|
198
223
|
...(step.workers === undefined ? {} : { workers: step.workers }),
|
|
224
|
+
// The counts the human line's `why()` renders, as numbers: a `--json` reader deciding whether
|
|
225
|
+
// a skipped lane is missing a suite or missing a prerequisite needs the same fact CI's log has.
|
|
226
|
+
...(step.tests === undefined ? {} : { tests: step.tests }),
|
|
199
227
|
// A FAILED step carries its captured stdout, exactly as the human renderer prints it. CI runs
|
|
200
228
|
// `--json`, and without this the log said only "one or more unit tests failed" with a generic
|
|
201
229
|
// fix line — the failing test's name and its assertion diff existed and were thrown away, so
|
|
@@ -45,11 +45,22 @@ on:
|
|
|
45
45
|
push:
|
|
46
46
|
pull_request:
|
|
47
47
|
|
|
48
|
+
# A newer push to a branch makes the run already in flight for it worthless, so it is cancelled —
|
|
49
|
+
# except on the default branch, where every commit keeps its own verdict: that group is keyed by
|
|
50
|
+
# SHA, so nothing there can supersede anything.
|
|
51
|
+
concurrency:
|
|
52
|
+
group: ci-\${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && github.sha || github.ref }}
|
|
53
|
+
cancel-in-progress: true
|
|
54
|
+
|
|
48
55
|
permissions:
|
|
49
56
|
contents: read
|
|
50
57
|
|
|
51
58
|
jobs:
|
|
52
59
|
check:
|
|
60
|
+
# A branch in THIS repository with an open pull request fires both events for one commit, and
|
|
61
|
+
# the push run already gates that exact tree — so the pull_request run is skipped, not paid for
|
|
62
|
+
# twice. A pull request from a FORK fires no push here, and it still runs.
|
|
63
|
+
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
|
|
53
64
|
runs-on: ubuntu-latest
|
|
54
65
|
# A bound on a HANG, not on cost: a step that never returns holds a runner until GitHub's
|
|
55
66
|
# six-hour default expires, and the failure is invisible for all six of them.
|
|
@@ -63,6 +74,13 @@ jobs:
|
|
|
63
74
|
# whoever took the app at its word. Never \`latest\`: a Bun minor landing unannounced is a
|
|
64
75
|
# runtime change nobody chose.
|
|
65
76
|
bun-version: '${REQUIRED_BUN}'
|
|
77
|
+
# Bun's download cache, keyed on the lockfile: \`bin/setup\`'s install then links from disk
|
|
78
|
+
# instead of fetching every package again on every run.
|
|
79
|
+
- uses: actions/cache@v6
|
|
80
|
+
with:
|
|
81
|
+
path: ~/.bun/install/cache
|
|
82
|
+
key: bun-\${{ runner.os }}-\${{ hashFiles('bun.lock') }}
|
|
83
|
+
restore-keys: bun-\${{ runner.os }}-
|
|
66
84
|
# Two steps rather than one \`&&\`, so the log names which half failed and times each.
|
|
67
85
|
- run: bin/setup
|
|
68
86
|
- run: bin/check
|
|
@@ -295,7 +295,9 @@ const biome = (): string => `{
|
|
|
295
295
|
*
|
|
296
296
|
* Two remain absent. `e2e` has a scaffolded file and it is an `e2eTest` — `test.skip` until the app
|
|
297
297
|
* registers a browser driver, so the step would run zero tests and fail the ratchet on the
|
|
298
|
-
* scaffold's own placeholder.
|
|
298
|
+
* scaffold's own placeholder. Its absence is visible rather than silent: a suite that executed
|
|
299
|
+
* nothing reports as a SKIPPED step, not the green check it printed until #434, so the app's own
|
|
300
|
+
* gate line says `- e2e` and names it in the summary until a driver makes it run. `contract-diff` needs a committed `x.manifest.json`, which
|
|
299
301
|
* `x manifest` writes later. Each joins the list in the commit that makes the app's gate run it.
|
|
300
302
|
*/
|
|
301
303
|
const SCAFFOLD_FLOOR: readonly VerifyStepName[] = [
|
package/src/verify-run.ts
CHANGED
|
@@ -53,20 +53,29 @@ export async function runVerify(
|
|
|
53
53
|
findings: [findingOf(error, step.name)],
|
|
54
54
|
}),
|
|
55
55
|
);
|
|
56
|
+
// A suite that executed nothing did not run, whatever its exit code says: `bun test` exits 0
|
|
57
|
+
// over an all-skipped file, so the counts are the only channel that can tell the two apart.
|
|
58
|
+
// ONE definition of "nothing ran", read twice, because the floor decides which of the two
|
|
59
|
+
// things it means — exactly as it already does for a step whose `applies` said no.
|
|
60
|
+
const tests = outcome.tests;
|
|
61
|
+
const nothingRan = tests !== undefined && tests.ran === 0;
|
|
62
|
+
const required = floorRequires(floor, step.name);
|
|
56
63
|
// A step the floor requires whose suite executed nothing is the same vanished suite as a step
|
|
57
64
|
// with no files at all — the run just had to finish before it could be seen. Appended to the
|
|
58
65
|
// step's own findings so `data.failed`, the counts and every gate reading this table carry it.
|
|
59
|
-
const vanished =
|
|
60
|
-
floorRequires(floor, step.name) && outcome.tests !== undefined && outcome.tests.ran === 0
|
|
61
|
-
? [skippedSuiteFinding(step.name, outcome.tests.skipped)]
|
|
62
|
-
: [];
|
|
66
|
+
const vanished = nothingRan && required ? [skippedSuiteFinding(step.name, tests.skipped)] : [];
|
|
63
67
|
results.push({
|
|
64
68
|
name: step.name,
|
|
65
69
|
ok: outcome.ok && vanished.length === 0,
|
|
66
70
|
durationMs: Math.round(performance.now() - started),
|
|
71
|
+
// Without a floor to require it, a suite that ran nothing is a SKIP and not a pass (#434):
|
|
72
|
+
// the `e2e` step printed `✓ e2e 46ms` over its one skipped test, which is the one thing a
|
|
73
|
+
// step table may never do — a reader cannot tell a lane that ran from a lane that did not.
|
|
74
|
+
skipped: nothingRan && !required,
|
|
67
75
|
findings: [...outcome.findings, ...vanished],
|
|
68
76
|
...(outcome.output === undefined ? {} : { output: outcome.output }),
|
|
69
77
|
...(outcome.workers === undefined ? {} : { workers: outcome.workers }),
|
|
78
|
+
...(tests === undefined ? {} : { tests }),
|
|
70
79
|
});
|
|
71
80
|
}
|
|
72
81
|
const failedSteps = results.filter((step) => !step.ok).map((step) => step.name);
|