Repository navigation
628 lines (559 loc) · 32.6 KB
/
Copy pathupdate-cli-coverage.yml
File metadata and controls
628 lines (559 loc) · 32.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
name: Update CLI Coverage
on:
push:
branches: [main]
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to use for context (leave empty to use most recent merged PR)'
required: false
type: string
# Or trigger on releases:
# release:
# types: [published]
permissions:
contents: read
pull-requests: read
concurrency:
group: update-cli-coverage
cancel-in-progress: false
jobs:
generate:
if: github.repository == 'kernel/kernel-go-sdk'
runs-on: ubuntu-latest
timeout-minutes: 55
outputs:
changed: ${{ steps.snapshot.outputs.changed }}
main_sha: ${{ steps.cli-clone.outputs.main_sha }}
update_sha: ${{ steps.cli-clone.outputs.update_sha }}
rebased: ${{ steps.cli-clone.outputs.rebased }}
agent_base_tree: ${{ steps.cli-clone.outputs.agent_base_tree }}
version: ${{ steps.sdk-version.outputs.version }}
steps:
- name: Generate API read token
id: api-read-token
uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1
with:
app-id: ${{ secrets.ADMIN_APP_ID }}
private-key: ${{ secrets.ADMIN_APP_PRIVATE_KEY }}
owner: kernel
repositories: kernel
permission-contents: read
- name: Get PR info for manual dispatch
id: pr-info
if: github.event_name == 'workflow_dispatch'
env:
GH_TOKEN: ${{ github.token }}
run: |
if [ -n "${{ inputs.pr_number }}" ]; then
# Use provided PR number
PR_NUMBER="${{ inputs.pr_number }}"
echo "Using provided PR number: $PR_NUMBER"
else
# Get most recent merged PR
PR_NUMBER=$(gh pr list --repo ${{ github.repository }} --state merged --limit 1 --json number --jq '.[0].number')
echo "Using most recent merged PR: $PR_NUMBER"
fi
if [ -z "$PR_NUMBER" ]; then
echo "No PR found, will use HEAD commit"
echo "has_pr=false" >> $GITHUB_OUTPUT
else
# Get PR details
PR_DATA=$(gh pr view "$PR_NUMBER" --repo ${{ github.repository }} --json mergeCommit,author,title)
MERGE_SHA=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid // empty')
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login // empty')
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title // empty')
echo "PR #$PR_NUMBER: $PR_TITLE"
echo "Merge commit: $MERGE_SHA"
echo "Author: $PR_AUTHOR"
echo "has_pr=true" >> $GITHUB_OUTPUT
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
echo "merge_sha=$MERGE_SHA" >> $GITHUB_OUTPUT
echo "pr_author=$PR_AUTHOR" >> $GITHUB_OUTPUT
fi
- name: Checkout SDK repo
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false
fetch-depth: 2
fetch-tags: true
# For manual dispatch with a specific PR, checkout the merge commit
ref: ${{ steps.pr-info.outputs.merge_sha || github.sha }}
- name: Checkout updater scripts
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
path: updater
sparse-checkout: .github/scripts
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: '22'
- name: Install Pi
env:
PI_TELEMETRY: '0'
run: |
npm install -g --ignore-scripts @earendil-works/pi-coding-agent
pi --version
- name: Configure git identity
run: |
git config --global user.name "kernel-internal[bot]"
git config --global user.email "260533166+kernel-internal[bot]@users.noreply.github.com"
- name: Setup Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
with:
go-version: 'stable'
- name: Clone CLI repo
id: cli-clone
run: |
git clone https://github.com/kernel/cli.git /tmp/kernel-cli
cd /tmp/kernel-cli
git config user.name "kernel-internal[bot]"
git config user.email "260533166+kernel-internal[bot]@users.noreply.github.com"
echo "main_sha=$(git rev-parse origin/main)" >> "$GITHUB_OUTPUT"
if git show-ref --verify --quiet refs/remotes/origin/cli-coverage-update; then
echo "update_sha=$(git rev-parse origin/cli-coverage-update)" >> "$GITHUB_OUTPUT"
git switch --track origin/cli-coverage-update
if ! git rebase origin/main; then
git rebase --abort
# The agent covers everything since main's SDK version, so a bot-only
# branch can be regenerated from main. Human commits must be kept.
if git log --no-merges --format=%ae origin/main..origin/cli-coverage-update \
| grep -qvxF '260533166+kernel-internal[bot]@users.noreply.github.com'; then
echo "::error::cli-coverage-update has human commits that conflict with main. Resolve the conflicts on that branch, then rerun this workflow."
exit 1
fi
echo "::warning::cli-coverage-update conflicts with main; regenerating it from main."
git switch -C cli-coverage-update origin/main
else
echo "rebased=true" >> "$GITHUB_OUTPUT"
fi
else
git switch -c cli-coverage-update origin/main
fi
# The agent's changes are published on top of this head, so human
# commits on the update branch keep their content and authorship.
echo "agent_base=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
echo "agent_base_tree=$(git rev-parse 'HEAD^{tree}')" >> "$GITHUB_OUTPUT"
- name: Get SDK version info
id: sdk-version
run: |
# Get the latest tag if available, otherwise use commit SHA
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -n "$LATEST_TAG" ]; then
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
echo "SDK version: $LATEST_TAG"
else
CURRENT_SHA="${{ steps.pr-info.outputs.merge_sha || github.sha }}"
echo "version=$CURRENT_SHA" >> $GITHUB_OUTPUT
echo "SDK version: $CURRENT_SHA (no tag)"
fi
# Get the module path from go.mod
MODULE_PATH=$(head -1 go.mod | awk '{print $2}')
echo "module=$MODULE_PATH" >> $GITHUB_OUTPUT
echo "SDK module: $MODULE_PATH"
# Determine the commit author (from PR info for manual dispatch, or from push event)
if [ -n "${{ steps.pr-info.outputs.pr_author }}" ]; then
echo "author=${{ steps.pr-info.outputs.pr_author }}" >> $GITHUB_OUTPUT
else
echo "author=${{ github.event.head_commit.author.username || github.actor }}" >> $GITHUB_OUTPUT
fi
- name: Compute SDK diff since CLI's current version
id: sdk-diff
run: |
# Extract the SDK version currently used by the CLI
# Prefer go list (parses go.mod properly); fall back to grep, taking the
# first non-conflict-marker match so a merge conflict can't yield a
# multi-line value that breaks $GITHUB_OUTPUT.
OLD_SDK_VERSION=$(cd /tmp/kernel-cli && go list -m github.com/kernel/kernel-go-sdk 2>/dev/null | awk '{print $2}')
if [ -z "$OLD_SDK_VERSION" ]; then
OLD_SDK_VERSION=$(grep 'kernel/kernel-go-sdk' /tmp/kernel-cli/go.mod | grep -v '^[<>=]' | head -1 | awk '{print $2}')
fi
echo "CLI currently uses SDK version: $OLD_SDK_VERSION"
echo "old_version=$OLD_SDK_VERSION" >> $GITHUB_OUTPUT
NEW_SDK_VERSION="${{ steps.sdk-version.outputs.version }}"
echo "New SDK version: $NEW_SDK_VERSION"
# Compute the diff between old and new SDK versions
# This gives the agent a focused view of what changed
if git diff "$OLD_SDK_VERSION".."$NEW_SDK_VERSION" -- api.md '*.go' ':!*_test.go' ':!internal/*' > /tmp/sdk-diff.patch 2>/dev/null; then
DIFF_LINES=$(wc -l < /tmp/sdk-diff.patch)
echo "SDK diff: $DIFF_LINES lines"
if [ "$DIFF_LINES" -eq 0 ]; then
echo "No SDK changes between versions (diff is empty)"
fi
else
echo "Could not compute SDK diff (old version tag may not exist), creating empty diff"
echo "" > /tmp/sdk-diff.patch
fi
utc_date() {
TZ=UTC git log -1 --date=format-local:%Y-%m-%dT%H:%M:%SZ --format=%cd "$1"
}
echo "old_date=$(utc_date "$OLD_SDK_VERSION" 2>/dev/null || true)" >> $GITHUB_OUTPUT
echo "new_date=$(utc_date HEAD)" >> $GITHUB_OUTPUT
- name: Fetch API spec
env:
GH_TOKEN: ${{ steps.api-read-token.outputs.token }}
OLD_DATE: ${{ steps.sdk-diff.outputs.old_date }}
NEW_DATE: ${{ steps.sdk-diff.outputs.new_date }}
run: |
main_at() {
gh api -X GET repos/kernel/kernel/commits -f sha=main -f until="$1" -f per_page=1 --jq '.[0].sha // empty'
}
file_at() {
gh api -H 'Accept: application/vnd.github.raw' "repos/kernel/kernel/contents/$1?ref=$2"
}
head=$(main_at "$NEW_DATE")
test -n "$head"
mkdir -p /tmp/kernel-api/packages/api
for file in openapi.yaml stainless.yaml; do
file_at "packages/api/$file" "$head" > "/tmp/kernel-api/packages/api/$file"
done
base=""
if [[ -n "$OLD_DATE" ]]; then
base=$(main_at "$OLD_DATE")
fi
if [[ -n "$base" ]]; then
file_at packages/api/openapi.yaml "$base" > "$RUNNER_TEMP/openapi-base.yaml"
diff -u --label "openapi.yaml@$base" --label "openapi.yaml@$head" \
"$RUNNER_TEMP/openapi-base.yaml" /tmp/kernel-api/packages/api/openapi.yaml \
> /tmp/openapi-diff.patch || [[ $? -eq 1 ]]
rm "$RUNNER_TEMP/openapi-base.yaml"
else
printf 'Spec baseline unavailable; compare all relevant descriptions in openapi.yaml.\n' > /tmp/openapi-diff.patch
fi
gh api --method DELETE /installation/token
- name: Update CLI coverage
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
PI_PREFERRED_MODEL: ${{ vars.PI_PREFERRED_MODEL }}
PI_SKIP_VERSION_CHECK: '1'
PI_TELEMETRY: '0'
KERNEL_API_KEY: ${{ secrets.KERNEL_API_KEY }}
BRANCH_PREFIX: cli-coverage-update
run: |
: "${PI_PREFERRED_MODEL:?Set the PI_PREFERRED_MODEL organization variable}"
mkdir -p "$RUNNER_TEMP/cli-scratch"
pi --print --no-session --approve \
--model "$PI_PREFERRED_MODEL" --thinking max \
"You are a CLI updater that implements missing CLI commands based on SDK updates.
Git is available for local diffs only. You have no GitHub publishing credentials.
Do not commit, push, open, or merge PRs. The workflow validates and publishes your changes.
# Context
- SDK Repo: ${{ github.repository }} (current directory)
- SDK Module: ${{ steps.sdk-version.outputs.module }}
- SDK Version: ${{ steps.sdk-version.outputs.version }}
- Commit SHA: ${{ steps.pr-info.outputs.merge_sha || github.sha }}
- Commit Author: ${{ steps.sdk-version.outputs.author }}
- Trigger: ${{ github.event_name }} ${{ inputs.pr_number && format('(PR #{0})', inputs.pr_number) || '' }}
- API Spec Location: /tmp/kernel-api/packages/api (openapi.yaml and stainless.yaml as of this SDK release)
- CLI Repo Location: /tmp/kernel-cli
- Update Branch Prefix: cli-coverage-update
# Background
The Go SDK (this repo) was just updated by Stainless, and may contain new API methods. The CLI (kernel/cli) needs to be updated to expose these new methods as CLI commands.
The CLI was previously on SDK version ${{ steps.sdk-diff.outputs.old_version }}, and we are updating it to ${{ steps.sdk-version.outputs.version }}.
# Source Files
- SDK api.md: Current directory - READ THIS FILE FIRST. This is the authoritative list of all SDK methods and their signatures.
- SDK *.go files: Current directory - Contains param structs (e.g., BrowserNewParams, DeploymentListParams) with all available options/fields.
- SDK diff: /tmp/sdk-diff.patch - IMPORTANT: This is the diff of api.md and Go source files between the CLI's current SDK version (${{ steps.sdk-diff.outputs.old_version }}) and the new version (${{ steps.sdk-version.outputs.version }}). Read this file to see exactly what methods, params, and fields were added or changed. Any additions here MUST be reflected in the CLI.
- API Spec: /tmp/kernel-api/packages/api/stainless.yaml - SDK configuration with resources and methods
- API Spec: /tmp/kernel-api/packages/api/openapi.yaml - Full OpenAPI specification. CHECK for x-cli-skip: true on endpoints - skip those from CLI coverage.
- API Spec diff: /tmp/openapi-diff.patch - Spec changes between the CLI's current SDK version and the new one, including endpoint, parameter, and field description changes.
- CLI: /tmp/kernel-cli - Existing CLI commands
# Task
## Step 1: Update SDK Version (ALWAYS DO THIS FIRST)
- Go to /tmp/kernel-cli
- Update go.mod to require the latest SDK: ${{ steps.sdk-version.outputs.module }}@${{ steps.sdk-version.outputs.version }}
- Run: go get ${{ steps.sdk-version.outputs.module }}@${{ steps.sdk-version.outputs.version }}
- Run: go mod tidy
- This ensures the CLI always uses the latest SDK, even if no new commands are added
## Step 2: Review SDK Diff (CRITICAL - DO THIS BEFORE THE FULL ENUMERATION)
Read /tmp/sdk-diff.patch to see exactly what changed in the SDK between the CLI's current version and the new version.
This diff shows new methods, new param fields, and changed signatures — these are the changes most likely to need CLI coverage.
Treat every addition in this diff as a checklist item: each new method needs a CLI command, each new param field needs a CLI flag.
After verifying these are covered, proceed to the full enumeration below to catch anything else.
## Step 3: Full SDK Method Enumeration (CRITICAL - DO NOT SKIP)
You MUST perform a complete enumeration of ALL SDK methods and their parameters. Do NOT rely only on the diff from Step 2.
3a. Read the api.md file in the SDK repo root. This file lists EVERY SDK method in the format:
- \`client.Resource.Method(ctx, params)\` with links to param types
Extract a complete list of all methods.
3b. For EACH SDK method, read the corresponding param type from the Go source files.
For example:
- BrowserNewParams in browser.go -> lists all fields like \`Proxy\`, \`Profile\`, \`Viewport\`, etc.
- DeploymentNewParams in deployment.go -> lists all fields like \`AppName\`, \`Region\`, \`EnvVars\`, etc.
Each field in a Params struct represents an option that could be a CLI flag.
3c. Build a complete SDK coverage matrix:
| SDK Method | SDK Param Type | SDK Param Fields |
|------------|----------------|------------------|
| client.Browsers.New | BrowserNewParams | Proxy, Profile, Viewport, Extensions, ... |
| client.Browsers.List | BrowserListParams | Page, PerPage, IncludeDeleted, ... |
| client.Deployments.New | DeploymentNewParams | AppName, Region, EnvVars, ... |
| ... | ... | ... |
## Step 4: Full CLI Command Enumeration (CRITICAL - DO NOT SKIP)
Enumerate ALL existing CLI commands and their flags.
4a. Look at cmd/ directory structure for existing commands
4b. For each command file, extract:
- The command name/path (e.g., \`kernel browser create\`)
- All flags defined for that command
4c. Build a CLI coverage matrix:
| CLI Command | CLI Flags |
|-------------|-----------|
| kernel browser create | --proxy, --profile, --viewport, ... |
| kernel browser list | --page, --per-page, ... |
| ... | ... |
## Step 5: Gap Analysis (CRITICAL - DO NOT SKIP)
Compare the SDK matrix (Step 3) with the CLI matrix (Step 4) to identify:
5a. Missing commands: SDK methods with NO corresponding CLI command
5b. Missing flags: SDK param fields with NO corresponding CLI flag
5c. Cross-check against the SDK diff from Step 2: every addition in /tmp/sdk-diff.patch MUST appear as a gap to address (new method = missing command, new param field = missing flag) unless it already exists in the CLI.
5d. Create a gap report:
## Missing Commands
- client.Browsers.LoadExtensions -> needs \`kernel browser load-extensions\`
- client.Proxies.Check -> needs \`kernel proxy check\`
## Missing Flags
- BrowserNewParams.SomeNewField -> \`kernel browser create\` needs --some-new-field
- DeploymentListParams.Status -> \`kernel deployment list\` needs --status
## Step 6: Implement Missing Coverage
For each gap identified in Step 5:
- Implement missing commands following existing patterns
- Add missing flags to existing commands
- Run \`go build ./...\` to verify the code compiles
## Step 6b: Sync Help Text With the API Spec
The SDK is generated from openapi.yaml, so its endpoint, parameter, and schema descriptions are the source of truth for what the API does.
- For every changed summary or description in /tmp/openapi-diff.patch that belongs to an endpoint, parameter, or field the CLI exposes, update the matching command Short/Long text and flag usage string. Use the spec text for anything you add in this run too.
- Adapt the text; do not copy it verbatim. API descriptions are written for HTTP callers and often reference other endpoints, HTTP methods, status codes, headers, or JSON body paths that mean nothing to a CLI user. Rewrite those references in CLI terms: name the kernel command and flags that do the equivalent. For example, \"List connections with \`GET /auth/connections\`\" becomes \"List connections with \`kernel auth connections list\`\", and a body field like \`max_concurrent_sessions\` becomes its flag, \`--max-concurrent-sessions\`. Drop API details that do not apply to the command.
- Follow existing help text style: keep Short to one concise line and put caveats in Long.
- Preserve meaning exactly: limits, defaults, units, accepted values, deprecations, and caveats must match the spec. Do not invent behavior the spec does not state.
- Change only text whose meaning the spec change affects, or existing text the spec now shows to be wrong. Do not restyle unrelated help text.
## Step 7: Test Your Changes (ALWAYS DO THIS)
After implementing, build the CLI and smoke test the new commands/flags you just added against the real API.
The KERNEL_API_KEY environment variable is available for authentication.
Keep every smoke-test artifact outside the checkout: write binaries, command output, JSON responses, and logs under $RUNNER_TEMP/cli-scratch, never under /tmp/kernel-cli.
7a. Build the CLI:
cd /tmp/kernel-cli && go build -o $RUNNER_TEMP/cli-scratch/kernel ./cmd/kernel
7b. For each new command or flag you added:
- Run the command against the real API and verify it succeeds
- For create/write commands: create a resource, verify it works, then clean up (delete it)
- For read commands: call them and verify the output format is correct
- For flag additions: create a resource with the new flag and verify it shows up in the get output
Example test flow for a new browser flag:
$RUNNER_TEMP/cli-scratch/kernel browsers create --new-flag value -t 30
$RUNNER_TEMP/cli-scratch/kernel browsers get <id> # verify new-flag appears
$RUNNER_TEMP/cli-scratch/kernel browsers delete <id> # clean up
7c. If any command fails or produces unexpected output, fix the implementation and re-test before proceeding.
7d. If you added no new commands or flags (SDK version bump only), skip this step.
## Step 8: Review and Report
- Read the full diff against origin/main and remove unrelated changes or unnecessary wrappers/casts.
- Write $RUNNER_TEMP/cli-coverage-report.md with the new commands/flags, help text updated to match spec changes (with the spec operation or field each follows), intentional exclusions, and test results or limitations.
- The report becomes part of a public PR. Include no credentials, customer information, private discussions, or internal URLs.
- Do not commit, push, change git authentication, or open PRs. Leave the changes for the workflow to validate and publish.
- Only go.mod, go.sum, and .go files under cmd/ and pkg/ are published. The workflow discards every other change before validation.
# SDK Method -> CLI Command Mapping Guide
- client.Resource.New() -> kernel resource create
- client.Resource.List() -> kernel resource list
- client.Resource.Get() -> kernel resource get
- client.Resource.Delete() -> kernel resource delete
- client.Resource.Update() -> kernel resource update
- client.Resource.Sub.Action() -> kernel resource sub action
- client.Resource.CustomAction() -> kernel resource custom-action
# SDK Param Field -> CLI Flag Mapping Guide
- CamelCaseField -> --camel-case-field
- TimeoutSeconds -> --timeout-seconds
- IncludeDeleted -> --include-deleted
- Nested structs: Viewport.Width -> --viewport-width or separate flags
# Implementation Guidelines
- Follow the existing CLI code patterns in /tmp/kernel-cli
- Use cobra for command definitions
- Use the Kernel Go SDK (this repo) for API calls
- Include proper flag definitions with descriptions matching SDK field comments
- Add help text for commands matching SDK method comments
- Handle errors appropriately
- Match the style of existing commands
# CLI Pagination UX for Paginated List Endpoints
When an SDK update introduces pagination on a list endpoint (e.g., the return type
changes to \`*pagination.OffsetPagination[T]\` and the params gain \`Limit\`/\`Offset\`
fields), do NOT naively expose \`--limit\` and \`--offset\` flags. Instead, follow the
page-based UX pattern used by \`kernel app list\` (see cmd/app.go):
## Flags
- Add \`--page\` (int, default 1): 1-based page number.
- Add \`--per-page\` (int, default 20): items per page.
- Do NOT add \`--limit\` or \`--offset\` flags. These are implementation details the
user should not need to think about.
- Keep any filter flags the endpoint supports (e.g., \`--query\`, \`--name\`, \`--status\`).
## Pagination Logic (the \"+1 trick\")
The SDK's \`OffsetPagination\` struct stores response headers (\`X-Has-More\`,
\`X-Next-Offset\`) in private fields that the CLI cannot access. To detect whether
more pages exist without an extra API call, request one extra item:
1. Set \`params.Limit = perPage + 1\`
2. Set \`params.Offset = (page - 1) * perPage\`
3. If the API returns more than \`perPage\` items, \`hasMore = true\`; truncate to
\`perPage\` items for display.
4. Otherwise \`hasMore = false\`.
## Pagination Footer
After rendering the table, always print a pagination footer (using \`pterm.Printf\`
so test output capture works):
Page: 1 Per-page: 20 Items this page: 20 Has more: yes
Next: kernel <resource> list --page 2 --per-page 20
- The footer line is always printed (even when \`hasMore\` is false).
- The \"Next:\" line is only printed when \`hasMore\` is true.
- Preserve all filter flags in the \"Next:\" hint. Always quote string values
(e.g., \`--query \"value\"\`) to handle multi-word values safely.
- Use \`lo.Ternary(hasMore, \"yes\", \"no\")\` for the Has more value
(import \`github.com/samber/lo\`, already a dependency).
- Do NOT print the footer when \`--output json\` is used.
## Input Struct
Use \`Page int\` and \`PerPage int\` in the list input struct, not \`Limit\`/\`Offset\`.
Default both defensively in the List method body (\`page <= 0\` -> 1, \`perPage <= 0\` -> 20).
## Example Reference
See \`cmd/profiles.go\` ProfilesCmd.List and \`cmd/app.go\` runAppList for working
implementations of this pattern.
# Constraints
- ALWAYS update the SDK version in go.mod - this is the primary purpose
- ALWAYS perform the full enumeration (Steps 2-4) - this is critical for finding gaps
- ALL SDK methods in api.md MUST have corresponding CLI commands, EXCEPT those marked with x-cli-skip in openapi.yaml
- SKIP endpoints marked with x-cli-skip: true in openapi.yaml - these are internal endpoints (e.g., hosted UI auth flows) not suitable for CLI
- Streaming methods may have different CLI implementations (e.g., follow flags)
- Even if no coverage gaps are found, still leave the SDK version bump and write the coverage report
- Ensure code compiles before finishing
"
- name: Snapshot publishable changes
id: snapshot
working-directory: /tmp/kernel-cli
env:
AGENT_BASE: ${{ steps.cli-clone.outputs.agent_base }}
run: |
git reset -q --soft "$AGENT_BASE"
git add -A
git diff --cached --name-only --no-renames -z \
| bash "$GITHUB_WORKSPACE/updater/.github/scripts/cli-update-disallowed-paths.sh" > "$RUNNER_TEMP/excluded-paths"
if [[ -s "$RUNNER_TEMP/excluded-paths" ]]; then
echo "::warning::Discarding changes outside the CLI updater allowlist:"
tr '\0' '\n' < "$RUNNER_TEMP/excluded-paths"
GIT_LITERAL_PATHSPECS=1 git reset -q --pathspec-from-file="$RUNNER_TEMP/excluded-paths" --pathspec-file-nul
fi
git diff --cached --binary --no-renames --no-ext-diff --no-textconv > "$RUNNER_TEMP/changes.patch"
git reset -q --hard
git clean -fdq
if [[ -s "$RUNNER_TEMP/changes.patch" ]]; then
git apply --index "$RUNNER_TEMP/changes.patch"
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- name: Verify CLI changes
working-directory: /tmp/kernel-cli
env:
SDK_MODULE: ${{ steps.sdk-version.outputs.module }}
SDK_VERSION: ${{ steps.sdk-version.outputs.version }}
run: |
test -s "$RUNNER_TEMP/cli-coverage-report.md"
expected=$(go list -m -f '{{.Version}}' "$SDK_MODULE@$SDK_VERSION")
test "$(go list -m -f '{{.Version}}' "$SDK_MODULE")" = "$expected"
# Validation must build against published modules, not local replacements.
test -z "$(go list -m -f '{{with .Replace}}{{.Path}}{{end}}' "$SDK_MODULE")"
go mod edit -json | jq -e '[.Replace[]? | select(.New.Version == null)] | length == 0'
go build ./...
go vet ./...
go test ./...
git diff --cached --check
- name: Upload validated changes
if: steps.snapshot.outputs.changed == 'true'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: cli-coverage-update
path: |
${{ runner.temp }}/changes.patch
${{ runner.temp }}/cli-coverage-report.md
if-no-files-found: error
retention-days: 7
publish:
needs: generate
if: needs.generate.outputs.changed == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
env:
SDK_VERSION: ${{ needs.generate.outputs.version }}
MAIN_SHA: ${{ needs.generate.outputs.main_sha }}
UPDATE_SHA: ${{ needs.generate.outputs.update_sha }}
REBASED: ${{ needs.generate.outputs.rebased }}
AGENT_BASE_TREE: ${{ needs.generate.outputs.agent_base_tree }}
steps:
- name: Checkout updater scripts
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false
- name: Download validated changes
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: cli-coverage-update
path: ${{ runner.temp }}/cli-coverage-update
- name: Checkout CLI
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
repository: kernel/cli
path: cli
fetch-depth: 0
persist-credentials: false
- name: Apply validated changes
working-directory: cli
run: |
git config user.name "kernel-internal[bot]"
git config user.email "260533166+kernel-internal[bot]@users.noreply.github.com"
if [[ "$REBASED" == true ]]; then
git switch -c cli-coverage-update "$UPDATE_SHA"
git rebase -q "$MAIN_SHA"
else
git switch -c cli-coverage-update "$MAIN_SHA"
fi
if [[ "$(git rev-parse 'HEAD^{tree}')" != "$AGENT_BASE_TREE" ]]; then
echo "::error::The rebuilt update branch differs from the one the changes were validated on."
exit 1
fi
git apply --index "$RUNNER_TEMP/cli-coverage-update/changes.patch"
git diff --cached --name-only --no-renames -z \
| bash ../.github/scripts/cli-update-disallowed-paths.sh > "$RUNNER_TEMP/disallowed-paths"
if [[ -s "$RUNNER_TEMP/disallowed-paths" ]]; then
echo "::error::Validated changes include paths outside the CLI updater allowlist:"
tr '\0' '\n' < "$RUNNER_TEMP/disallowed-paths"
exit 1
fi
if git diff --cached --raw --no-renames | awk '$2 !~ /^(100644|100755|000000)$/ { found = 1 } END { exit !found }'; then
echo "::error::Validated changes may only contain regular files."
exit 1
fi
git commit -q -m "Update Go SDK to $SDK_VERSION and CLI coverage"
- name: Generate CLI publishing token
id: publish-token
uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1
with:
app-id: ${{ secrets.ADMIN_APP_ID }}
private-key: ${{ secrets.ADMIN_APP_PRIVATE_KEY }}
owner: kernel
repositories: cli
permission-contents: write
permission-pull-requests: write
- name: Open or update CLI coverage PR
working-directory: cli
env:
GH_TOKEN: ${{ steps.publish-token.outputs.token }}
run: |
if git diff --quiet origin/main HEAD; then
echo "CLI coverage is already up to date."
exit 0
fi
gh auth setup-git
git push --force-with-lease="cli-coverage-update:$UPDATE_SHA" origin cli-coverage-update
body="$RUNNER_TEMP/cli-coverage-pr.md"
{
printf '## Summary\n\n- Update the Go SDK to `%s`.\n' "$SDK_VERSION"
printf '\n## Coverage\n\n'
head -c 60000 "$RUNNER_TEMP/cli-coverage-update/cli-coverage-report.md"
printf '\n## Validation\n\n- Go build\n- Go vet\n- Go tests\n'
} > "$body"
pr=$(gh pr list --repo kernel/cli --head cli-coverage-update --base main \
--state open --json number --jq '.[0].number // empty')
title="CLI: Update Go SDK to $SDK_VERSION"
if [[ -n "$pr" ]]; then
gh pr edit "$pr" --repo kernel/cli --title "$title" --body-file "$body"
else
gh pr create --repo kernel/cli --head cli-coverage-update --base main \
--title "$title" --body-file "$body"
fi