Skip to content

Commit b99ed48

Browse files
authored
Merge pull request #22 from kernel/cli-updater
Add CLI updater CI job
2 parents 160d52f + 9a1afd5 commit b99ed48

1 file changed

Lines changed: 368 additions & 0 deletions

File tree

Lines changed: 368 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,368 @@
1+
name: Update CLI Coverage
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
workflow_dispatch:
8+
inputs:
9+
pr_number:
10+
description: 'PR number to use for context (leave empty to use most recent merged PR)'
11+
required: false
12+
type: string
13+
14+
# Only run one instance at a time; cancel older runs when a new push arrives
15+
concurrency:
16+
group: update-cli-coverage
17+
cancel-in-progress: true
18+
19+
permissions:
20+
contents: read
21+
22+
jobs:
23+
update-cli-coverage:
24+
runs-on: ubuntu-latest
25+
steps:
26+
- name: Generate app token
27+
id: app-token
28+
uses: actions/create-github-app-token@v1
29+
with:
30+
app-id: ${{ secrets.ADMIN_APP_ID }}
31+
private-key: ${{ secrets.ADMIN_APP_PRIVATE_KEY }}
32+
owner: kernel
33+
34+
- name: Get PR info for manual dispatch
35+
id: pr-info
36+
if: github.event_name == 'workflow_dispatch'
37+
env:
38+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
39+
run: |
40+
if [ -n "${{ inputs.pr_number }}" ]; then
41+
# Use provided PR number
42+
PR_NUMBER="${{ inputs.pr_number }}"
43+
echo "Using provided PR number: $PR_NUMBER"
44+
else
45+
# Get most recent merged PR
46+
PR_NUMBER=$(gh pr list --repo ${{ github.repository }} --state merged --limit 1 --json number --jq '.[0].number')
47+
echo "Using most recent merged PR: $PR_NUMBER"
48+
fi
49+
50+
if [ -z "$PR_NUMBER" ]; then
51+
echo "No PR found, will use HEAD commit"
52+
echo "has_pr=false" >> $GITHUB_OUTPUT
53+
else
54+
# Get PR details
55+
PR_DATA=$(gh pr view "$PR_NUMBER" --repo ${{ github.repository }} --json mergeCommit,author,title)
56+
MERGE_SHA=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid // empty')
57+
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login // empty')
58+
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title // empty')
59+
60+
echo "PR #$PR_NUMBER: $PR_TITLE"
61+
echo "Merge commit: $MERGE_SHA"
62+
echo "Author: $PR_AUTHOR"
63+
64+
echo "has_pr=true" >> $GITHUB_OUTPUT
65+
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
66+
echo "merge_sha=$MERGE_SHA" >> $GITHUB_OUTPUT
67+
echo "pr_author=$PR_AUTHOR" >> $GITHUB_OUTPUT
68+
fi
69+
70+
- name: Checkout SDK repo
71+
uses: actions/checkout@v4
72+
with:
73+
fetch-depth: 2
74+
fetch-tags: true
75+
# For manual dispatch with a specific PR, checkout the merge commit
76+
ref: ${{ steps.pr-info.outputs.merge_sha || github.sha }}
77+
78+
- name: Install Cursor CLI
79+
run: |
80+
curl https://cursor.com/install -fsS | bash
81+
echo "$HOME/.cursor/bin" >> $GITHUB_PATH
82+
83+
- name: Configure git identity
84+
run: |
85+
git config --global user.name "kernel-internal[bot]"
86+
git config --global user.email "260533166+kernel-internal[bot]@users.noreply.github.com"
87+
88+
- name: Setup Go
89+
uses: actions/setup-go@v6
90+
with:
91+
go-version: 'stable'
92+
93+
- name: Clone API repo
94+
env:
95+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
96+
run: |
97+
gh repo clone kernel/hypeman /tmp/hypeman -- --depth=1
98+
99+
- name: Clone CLI repo and checkout existing branch
100+
env:
101+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
102+
run: |
103+
gh repo clone kernel/hypeman-cli /tmp/hypeman-cli
104+
cd /tmp/hypeman-cli
105+
106+
# Try to fetch the cli-coverage-update branch from remote
107+
if git fetch origin cli-coverage-update 2>/dev/null; then
108+
echo "Branch cli-coverage-update exists, checking it out..."
109+
git checkout cli-coverage-update
110+
# Merge latest main to keep it up to date
111+
git merge origin/main -m "Merge main into cli-coverage-update" --no-edit || true
112+
else
113+
echo "Branch cli-coverage-update does not exist, creating from main..."
114+
git checkout -b cli-coverage-update
115+
fi
116+
117+
- name: Get SDK version info
118+
id: sdk-version
119+
run: |
120+
# Get the latest tag if available, otherwise use commit SHA
121+
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
122+
if [ -n "$LATEST_TAG" ]; then
123+
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
124+
echo "SDK version: $LATEST_TAG"
125+
else
126+
CURRENT_SHA="${{ steps.pr-info.outputs.merge_sha || github.sha }}"
127+
echo "version=$CURRENT_SHA" >> $GITHUB_OUTPUT
128+
echo "SDK version: $CURRENT_SHA (no tag)"
129+
fi
130+
131+
# Get the module path from go.mod
132+
MODULE_PATH=$(head -1 go.mod | awk '{print $2}')
133+
echo "module=$MODULE_PATH" >> $GITHUB_OUTPUT
134+
echo "SDK module: $MODULE_PATH"
135+
136+
# Determine the commit author (from PR info for manual dispatch, or from push event)
137+
if [ -n "${{ steps.pr-info.outputs.pr_author }}" ]; then
138+
echo "author=${{ steps.pr-info.outputs.pr_author }}" >> $GITHUB_OUTPUT
139+
else
140+
echo "author=${{ github.event.head_commit.author.username || github.actor }}" >> $GITHUB_OUTPUT
141+
fi
142+
143+
- name: Update CLI coverage
144+
env:
145+
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
146+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
147+
BRANCH_PREFIX: cli-coverage-update
148+
run: |
149+
cursor-agent -p "You are a CLI updater that implements missing CLI commands based on SDK updates.
150+
151+
The GitHub CLI is available as \`gh\` and authenticated via GH_TOKEN. Git is available. You have write access to the CLI repository (kernel/hypeman-cli).
152+
153+
# Context
154+
- SDK Repo: ${{ github.repository }} (current directory)
155+
- SDK Module: ${{ steps.sdk-version.outputs.module }}
156+
- SDK Version: ${{ steps.sdk-version.outputs.version }}
157+
- Commit SHA: ${{ steps.pr-info.outputs.merge_sha || github.sha }}
158+
- Commit Author: ${{ steps.sdk-version.outputs.author }}
159+
- Trigger: ${{ github.event_name }} ${{ inputs.pr_number && format('(PR #{0})', inputs.pr_number) || '' }}
160+
- API Repo Location: /tmp/hypeman
161+
- CLI Repo Location: /tmp/hypeman-cli
162+
- Update Branch Prefix: cli-coverage-update
163+
164+
# Background
165+
The Go SDK (this repo) was just updated by Stainless, and may contain new API methods. The CLI (kernel/hypeman-cli) needs to be updated to expose these new methods as CLI commands.
166+
167+
# Source Files
168+
- SDK api.md: Current directory - READ THIS FILE FIRST. This is the authoritative list of all SDK methods and their signatures.
169+
- SDK *.go files: Current directory - Contains param structs (e.g., InstanceNewParams, ImageNewParams) with all available options/fields.
170+
- API Spec: /tmp/hypeman/stainless.yaml - SDK configuration with resources and methods
171+
- API Spec: /tmp/hypeman/openapi.yaml - Full OpenAPI specification. CHECK for x-cli-skip: true on endpoints - skip those from CLI coverage.
172+
- CLI: /tmp/hypeman-cli - Existing CLI commands (in pkg/cmd/ directory)
173+
174+
# CLI Architecture
175+
The CLI uses urfave/cli/v3 (NOT cobra). Commands are defined in /tmp/hypeman-cli/pkg/cmd/:
176+
- Root command: pkg/cmd/cmd.go
177+
- Resource commands: pkg/cmd/{resource}cmd.go (e.g., imagecmd.go, volumecmd.go, devicecmd.go, ingresscmd.go)
178+
- Top-level commands: pkg/cmd/{command}.go (e.g., run.go, ps.go, rm.go, logs.go, exec.go, cp.go)
179+
- Lifecycle commands: pkg/cmd/lifecycle.go (stop, start, standby, restore)
180+
- Build commands: pkg/cmd/build.go
181+
- Utilities: pkg/cmd/cmdutil.go, pkg/cmd/format.go
182+
- Entry point: cmd/hypeman/main.go
183+
184+
# Task
185+
186+
## Step 1: Update SDK Version (ALWAYS DO THIS FIRST)
187+
- Go to /tmp/hypeman-cli
188+
- Update go.mod to require the latest SDK: ${{ steps.sdk-version.outputs.module }}@${{ steps.sdk-version.outputs.version }}
189+
- Run: go get ${{ steps.sdk-version.outputs.module }}@${{ steps.sdk-version.outputs.version }}
190+
- Run: go mod tidy
191+
- This ensures the CLI always uses the latest SDK, even if no new commands are added
192+
193+
## Step 2: Full SDK Method Enumeration (CRITICAL - DO NOT SKIP)
194+
You MUST perform a complete enumeration of ALL SDK methods and their parameters. Do NOT rely only on recent commits.
195+
196+
2a. Read the api.md file in the SDK repo root. This file lists EVERY SDK method in the format:
197+
- \`client.Resource.Method(ctx, params)\` with links to param types
198+
Extract a complete list of all methods.
199+
200+
2b. For EACH SDK method, read the corresponding param type from the Go source files.
201+
For example:
202+
- InstanceNewParams in instance.go -> lists all fields like \`Image\`, \`Region\`, \`VolumeMounts\`, etc.
203+
- ImageNewParams in image.go -> lists all fields like \`Name\`, \`Tag\`, etc.
204+
- VolumeNewParams in volume.go -> lists all fields like \`Name\`, \`Size\`, etc.
205+
Each field in a Params struct represents an option that could be a CLI flag.
206+
207+
2c. Build a complete SDK coverage matrix:
208+
| SDK Method | SDK Param Type | SDK Param Fields |
209+
|------------|----------------|------------------|
210+
| client.Instances.New | InstanceNewParams | Image, Region, VolumeMounts, ... |
211+
| client.Instances.List | (none) | |
212+
| client.Images.New | ImageNewParams | Name, Tag, ... |
213+
| client.Volumes.New | VolumeNewParams | Name, Size, ... |
214+
| ... | ... | ... |
215+
216+
## Step 3: Full CLI Command Enumeration (CRITICAL - DO NOT SKIP)
217+
Enumerate ALL existing CLI commands and their flags.
218+
219+
3a. Look at pkg/cmd/ directory for existing command files
220+
3b. For each command file, extract:
221+
- The command name/path (e.g., \`hypeman run\`, \`hypeman image list\`)
222+
- All flags defined for that command
223+
3c. Build a CLI coverage matrix:
224+
| CLI Command | CLI Flags |
225+
|-------------|-----------|
226+
| hypeman run | --name, --image, --quiet, ... |
227+
| hypeman ps | --quiet, --format, ... |
228+
| hypeman image list | --format, ... |
229+
| hypeman volume create | --name, ... |
230+
| ... | ... |
231+
232+
## Step 4: Gap Analysis (CRITICAL - DO NOT SKIP)
233+
Compare the SDK matrix (Step 2) with the CLI matrix (Step 3) to identify:
234+
235+
4a. Missing commands: SDK methods with NO corresponding CLI command
236+
4b. Missing flags: SDK param fields with NO corresponding CLI flag
237+
4c. Create a gap report:
238+
## Missing Commands
239+
- client.SomeResource.SomeMethod -> needs new CLI command
240+
241+
## Missing Flags
242+
- InstanceNewParams.SomeNewField -> \`hypeman run\` needs --some-new-field
243+
- VolumeNewParams.Region -> \`hypeman volume create\` needs --region
244+
245+
## Step 5: Implement Missing Coverage
246+
For each gap identified in Step 4:
247+
- Implement missing commands following existing patterns in pkg/cmd/
248+
- Add missing flags to existing commands
249+
- Use urfave/cli/v3 for command and flag definitions (NOT cobra)
250+
- Run \`go build ./...\` to verify the code compiles
251+
252+
## Step 6: Commit and Push
253+
- You should already be on the cli-coverage-update branch (it was checked out during setup if it existed)
254+
- If you're on main, create/switch to the cli-coverage-update branch
255+
- Commit with message describing SDK version bump and any new commands/flags
256+
- IMPORTANT: Do NOT force push! Use regular \`git push origin cli-coverage-update\` to preserve existing work on the branch
257+
- If push fails due to divergence, pull and rebase first: \`git pull --rebase origin cli-coverage-update\`
258+
- Create or update the PR in kernel/hypeman-cli
259+
260+
# SDK Method -> CLI Command Mapping Guide
261+
Instance operations use Docker-style top-level commands:
262+
- client.Instances.New() -> hypeman run
263+
- client.Instances.List() -> hypeman ps
264+
- client.Instances.Delete() -> hypeman rm
265+
- client.Instances.Get() -> (used internally by other commands)
266+
- client.Instances.Logs() -> hypeman logs
267+
- client.Instances.Stop() -> hypeman stop
268+
- client.Instances.Start() -> hypeman start
269+
- client.Instances.Standby() -> hypeman standby
270+
- client.Instances.Restore() -> hypeman restore
271+
- client.Instances.Stat() -> (used internally by cp)
272+
273+
Resource group commands use subcommands:
274+
- client.Images.New() -> hypeman image create
275+
- client.Images.List() -> hypeman image list
276+
- client.Images.Get() -> hypeman image get
277+
- client.Images.Delete() -> hypeman image delete
278+
- client.Volumes.New() -> hypeman volume create
279+
- client.Volumes.List() -> hypeman volume list
280+
- client.Volumes.Get() -> hypeman volume get
281+
- client.Volumes.Delete() -> hypeman volume delete
282+
- client.Volumes.NewFromArchive() -> (used internally by push)
283+
- client.Instances.Volumes.Attach() -> hypeman volume attach
284+
- client.Instances.Volumes.Detach() -> hypeman volume detach
285+
- client.Devices.New() -> hypeman device register
286+
- client.Devices.List() -> hypeman device list
287+
- client.Devices.Get() -> hypeman device get
288+
- client.Devices.Delete() -> hypeman device delete
289+
- client.Devices.ListAvailable() -> hypeman device available
290+
- client.Ingresses.New() -> hypeman ingress create
291+
- client.Ingresses.List() -> hypeman ingress list
292+
- client.Ingresses.Get() -> hypeman ingress get
293+
- client.Ingresses.Delete() -> hypeman ingress delete
294+
- client.Builds.New() -> hypeman build create (or used internally)
295+
- client.Builds.List() -> hypeman build list
296+
- client.Builds.Get() -> hypeman build get
297+
- client.Builds.Cancel() -> hypeman build cancel
298+
- client.Builds.Events() -> (used internally for streaming build output)
299+
- client.Resources.Get() -> hypeman resources
300+
- client.Health.Check() -> (internal, no CLI needed)
301+
302+
# SDK Param Field -> CLI Flag Mapping Guide
303+
- CamelCaseField -> --camel-case-field
304+
- TimeoutSeconds -> --timeout-seconds
305+
- IncludeDeleted -> --include-deleted
306+
- Optional fields use hypeman.Opt() wrapper in SDK calls
307+
308+
# Implementation Guidelines
309+
- Follow the existing CLI code patterns in /tmp/hypeman-cli/pkg/cmd/
310+
- Use urfave/cli/v3 for command definitions (cli.Command struct with Flags, Action, etc.)
311+
- Use the Hypeman Go SDK (this repo) for API calls: hypeman.NewClient(getDefaultRequestOptions(cmd)...)
312+
- Use existing helpers: ShowJSON() for output, ResolveInstance() for name resolution, FormatTimeAgo() for timestamps
313+
- Include proper flag definitions with descriptions matching SDK field comments
314+
- Add help text for commands matching SDK method comments
315+
- Handle errors appropriately
316+
- Match the style of existing commands
317+
318+
# Output Format
319+
After pushing changes, create or update an evergreen PR using gh:
320+
321+
1. Check if a PR already exists for the cli-coverage-update branch:
322+
gh pr list --repo kernel/hypeman-cli --head cli-coverage-update --json number
323+
324+
2. If PR exists, update it. If not, create a new one.
325+
326+
If new commands or flags were added:
327+
Title: 'CLI: Update hypeman SDK to <version> and add new commands/flags'
328+
Body:
329+
'This PR updates the Hypeman Go SDK to ${{ steps.sdk-version.outputs.version }} and adds CLI commands/flags for new SDK methods.
330+
331+
## SDK Update
332+
- Updated hypeman-go to ${{ steps.sdk-version.outputs.version }}
333+
334+
## Coverage Analysis
335+
This PR was generated by performing a full enumeration of SDK methods and CLI commands.
336+
337+
## New Commands
338+
- \`hypeman <command>\` for \`client.Resource.Action()\`
339+
340+
## New Flags
341+
- \`--flag-name\` on \`hypeman <command>\` for \`ResourceParams.FieldName\`
342+
343+
Triggered by: kernel/hypeman-go@${{ steps.pr-info.outputs.merge_sha || github.sha }}
344+
Reviewer: @<commit_author>'
345+
346+
If only SDK version update (no coverage gaps found):
347+
Title: 'CLI: Update Hypeman Go SDK to ${{ steps.sdk-version.outputs.version }}'
348+
Body:
349+
'This PR updates the Hypeman Go SDK dependency to the latest version.
350+
351+
## SDK Update
352+
- Updated hypeman-go to ${{ steps.sdk-version.outputs.version }}
353+
354+
## Coverage Analysis
355+
A full enumeration of SDK methods and CLI commands was performed. No coverage gaps were found.
356+
357+
Triggered by: kernel/hypeman-go@${{ steps.pr-info.outputs.merge_sha || github.sha }}
358+
Reviewer: @<commit_author>'
359+
360+
# Constraints
361+
- ALWAYS update the SDK version in go.mod - this is the primary purpose
362+
- ALWAYS perform the full enumeration (Steps 2-4) - this is critical for finding gaps
363+
- ALL SDK methods in api.md MUST have corresponding CLI commands, EXCEPT those marked with x-cli-skip in openapi.yaml or noted as internal-only in the mapping guide above
364+
- SKIP endpoints marked with x-cli-skip: true in openapi.yaml - these are internal endpoints not suitable for CLI
365+
- Streaming methods may have different CLI implementations (e.g., build events are streamed internally)
366+
- Even if no coverage gaps are found, still create a PR for the SDK version bump
367+
- Ensure code compiles before pushing
368+
" --model opus-4.6 --force --output-format=text

0 commit comments

Comments
 (0)