Skip to content

feat: add upgrade command and --upgradable flag#31

Merged
pavelanni merged 9 commits into
mainfrom
feature/upgrade-command
May 1, 2026
Merged

feat: add upgrade command and --upgradable flag#31
pavelanni merged 9 commits into
mainfrom
feature/upgrade-command

Conversation

@pavelanni
Copy link
Copy Markdown
Collaborator

@pavelanni pavelanni commented Apr 30, 2026

Summary

  • Add skillctl upgrade <skill-name> --target <agent> to pull and re-install the latest published version from the source registry
  • Add skillctl upgrade --all --target <agent> for batch upgrades
  • Add list --installed --upgradable to preview available upgrades
  • Add CheckUpgrades function in pkg/installed/ with semver comparison and injectable tag lister for testability

Part of #27

Design decisions

  • Published versions only — tags with -draft or -testing suffix are excluded
  • Semver ordering only — no date-based comparison
  • Remote refs only — source must have a registry host (contains . or : in first segment); local store refs are skipped
  • TagLister function type injected via CheckOptions for unit test isolation (no real registry needed)
  • Reuses existing writeProvenance from install flow to update skill.yaml after upgrade

Test plan

  • TestCheckUpgrades_HasUpgrade — installed 1.0.0, remote has 2.0.0 published
  • TestCheckUpgrades_AlreadyLatest — installed 2.0.0, no newer published
  • TestCheckUpgrades_NoProvenance — skill without source skipped
  • TestCheckUpgrades_OnlyDraftTags — newer versions only in draft/testing
  • TestCheckUpgrades_LocalRef — local store ref (no registry host) skipped
  • TestCheckUpgrades_InvalidSemver — non-semver version skipped
  • go test ./... — all tests pass
  • make lint — 0 issues

Summary by CodeRabbit

  • New Features

    • Added skillctl upgrade command to upgrade installed skills to the latest published version, with support for upgrading individual skills or all installed skills at once.
    • Added --upgradable flag to list command to display only installed skills with available updates.
  • Tests

    • Added comprehensive test suite for upgrade availability checking.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Pavel Anni <panni@redhat.com>
Implements CheckUpgrades function that queries remote registries
to find available upgrades for installed skills. The function:
- Filters skills by remote source refs and valid semver versions
- Uses injected TagLister for testability
- Excludes draft/testing tags from upgrade candidates
- Returns skills with newer published versions

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Pavel Anni <panni@redhat.com>
Add --upgradable flag to list --installed command to show only skills
with available updates. The flag requires --installed and displays
current and latest versions side-by-side.

Add ListTagsForRepo wrapper function to pkg/oci/catalog.go that splits
a repository reference (registry/repo) into components and calls the
existing ListRemoteTags function.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Pavel Anni <panni@redhat.com>
Part of #27

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Pavel Anni <panni@redhat.com>
Signed-off-by: Pavel Anni <panni@redhat.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

Warning

Rate limit exceeded

@pavelanni has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 47 minutes and 21 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Enterprise

Run ID: 79c04daa-39cf-4cf2-9678-3e74b2ecb007

📥 Commits

Reviewing files that changed from the base of the PR and between 5198268 and 586caf6.

📒 Files selected for processing (3)
  • internal/cli/install.go
  • internal/cli/install_test.go
  • internal/cli/upgrade.go
📝 Walkthrough

Walkthrough

The changes implement upgrade functionality for a CLI tool called skillctl. A new upgrade command is introduced to upgrade installed skills to published versions, the list command gains an --upgradable filter, and core upgrade-checking APIs are added to the installed and oci packages. Installation flows are updated to pull remote OCI references before unpacking. Planning and design documents outline the feature specification.

Changes

Cohort / File(s) Summary
Planning & Documentation
dev/plans/2026-04-30-upgrade-command.md, dev/specs/2026-04-30-upgrade-command-design.md
Implementation plan and design specification for upgrade command and upgradable listing, defining APIs, CLI flags, workflows, and test targets.
Installed Skills APIs
pkg/installed/check.go, pkg/installed/check_test.go
New CheckUpgrades function with CheckOptions, UpgradeCandidate, and TagLister type; handles semver parsing, tag filtering (excludes -draft/-testing), version comparison, and returns upgrade candidates; includes comprehensive test suite covering tag selection, filtering, and edge cases.
OCI Registry Access
pkg/oci/catalog.go
Adds ListTagsForRepo wrapper function that parses repository references and delegates to existing remote tag listing.
CLI Commands
internal/cli/install.go
Updates install flow to pull remote OCI refs before unpacking; adds tagFromRef helper for consistent version extraction; updates provenance to align installed version with ref tag.
CLI Commands
internal/cli/list.go
Adds --upgradable/-u flag (requires --installed); extends output table with LATEST columns when upgradable skills are requested; replaces local target resolution logic with delegation to shared helper.
CLI Command Registration
internal/cli/root.go
Registers new upgrade subcommand in root command.
CLI Command Implementation
internal/cli/upgrade.go
New upgrade command supporting single-skill and --all batch upgrades; validates mutually-exclusive flags (skill-name vs --all, --target vs --output); scans installed skills, checks upgrade availability, pulls latest refs, unpacks, and writes provenance; prints status per-skill with final count on batch success.
CLI Helper Functions
internal/cli/helpers.go
New resolveTargetDirs function converts CLI target/output arguments to absolute paths; performs home-dir expansion, agent target lookup, and normalization; provides descriptive errors for unknown targets.

Sequence Diagram(s)

sequenceDiagram
    participant User as User/CLI
    participant Install as install.runInstall
    participant OCI as oci.Client
    participant LocalStore as Local Skill Store
    participant Provenance as skill.yaml

    User->>Install: upgrade [skill-name] or --all
    Install->>Install: Scan installed skills
    Install->>Install: Check upgrades via installed.CheckUpgrades
    Install->>OCI: ListTagsForRepo(repo) for each candidate
    OCI-->>Install: Latest version tag
    Install->>Install: Filter draft/testing, parse semver
    Install->>OCI: Pull latest ref
    OCI-->>LocalStore: Download OCI artifact
    Install->>LocalStore: Unpack into skill directory
    Install->>Provenance: Update skill.yaml with new version
    Provenance-->>User: Upgrade complete
Loading
sequenceDiagram
    participant User as User/CLI
    participant List as list.runListInstalled
    participant Installed as installed.Scan
    participant CheckUpgrade as installed.CheckUpgrades
    participant OCI as oci.ListTagsForRepo
    participant Output as Output Table

    User->>List: list --installed --upgradable
    List->>Installed: Scan installed skills
    Installed-->>List: []InstalledSkill
    List->>CheckUpgrade: CheckUpgrades(skills, opts)
    CheckUpgrade->>OCI: Query tags for each skill repo
    OCI-->>CheckUpgrade: Available tags
    CheckUpgrade->>CheckUpgrade: Filter & compare semver
    CheckUpgrade-->>List: []UpgradeCandidate
    List->>Output: Render table with NAME/VERSION/LATEST/SOURCE/TARGET
    Output-->>User: Display upgradable skills
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main changes: adding an upgrade command and a related --upgradable flag for listing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/upgrade-command

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 47 minutes and 21 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

When the ref looks remote (has a registry host) and isn't in the
local store, install now pulls it automatically before unpacking.
Eliminates the need for a separate pull step.

Signed-off-by: Pavel Anni <panni@redhat.com>
After unpacking, the skill.yaml version now reflects the tag
that was actually installed, not whatever was baked into the
image. Fixes stale version after upgrade when the image's
embedded version doesn't match its tag.

Signed-off-by: Pavel Anni <panni@redhat.com>
- Fix looksRemote false positive on relative paths (./foo, ../bar)
- Add warning on registry errors instead of silent skip
- Extract shared resolveTargetDirs helper, remove duplication
- Add TestCheckUpgrades_RelativePath test case

Signed-off-by: Pavel Anni <panni@redhat.com>
@pavelanni pavelanni marked this pull request as ready for review April 30, 2026 23:59
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@dev/plans/2026-04-30-upgrade-command.md`:
- Around line 300-303: The loop silently ignores errors from opts.TagLister
(tags, err := opts.TagLister(ctx, repo, opts.SkipTLSVerify)) by using continue,
which can hide outages and yield false “up to date” results; change the handling
to return or propagate the error (or aggregate it) to the caller instead of
continuing — e.g., when err != nil, wrap/contextualize the error with repo
information and return it (or add it to an error accumulator) so callers can
surface failures explicitly.
- Around line 667-681: The loop over candidates currently logs errors and
continues, then always returns nil which causes a non-all single-skill failure
to exit 0; modify the logic in the function containing
candidates/upgradeSkill/all/upgraded so that when an upgrade attempt fails and
the flag all is false you immediately return a non-nil error (include context
like the skill name and underlying err), and if all is true but upgraded == 0
after processing return an error indicating no skills were upgraded; update any
callers to handle the returned error and keep the existing fmt.Fprintf lines for
stdout/stderr for logging.

In `@internal/cli/install.go`:
- Around line 158-176: The tagFromRef function currently returns the digest
portion when a ref contains '@' (e.g., "quay.io/acme/skill@sha256:..."); change
its behavior so that when an '@' is present it returns an empty string instead
of the digest. Update the tagFromRef function to detect '@' (the existing if idx
:= strings.Index(ref, "@") branch) and immediately return "" for digest-based
refs; leave the rest of the logic (handling ":" after the last "/") unchanged so
normal tag refs like "repo/name:1.2.3" still return the tag.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Enterprise

Run ID: fbae9c5f-f146-497d-9b09-121020f5bca6

📥 Commits

Reviewing files that changed from the base of the PR and between 0494ebf and 5198268.

📒 Files selected for processing (10)
  • dev/plans/2026-04-30-upgrade-command.md
  • dev/specs/2026-04-30-upgrade-command-design.md
  • internal/cli/helpers.go
  • internal/cli/install.go
  • internal/cli/list.go
  • internal/cli/root.go
  • internal/cli/upgrade.go
  • pkg/installed/check.go
  • pkg/installed/check_test.go
  • pkg/oci/catalog.go

Comment on lines +300 to +303
tags, err := opts.TagLister(ctx, repo, opts.SkipTLSVerify)
if err != nil {
continue
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not silently ignore registry tag-list failures.

continue on TagLister error hides real outages and can produce false “up to date” results. Return (or aggregate) the error so callers can surface it explicitly.

Suggested adjustment
-        tags, err := opts.TagLister(ctx, repo, opts.SkipTLSVerify)
-        if err != nil {
-            continue
-        }
+        tags, err := opts.TagLister(ctx, repo, opts.SkipTLSVerify)
+        if err != nil {
+            return nil, fmt.Errorf("listing tags for %s: %w", repo, err)
+        }

As per coding guidelines, "Focus on major issues impacting performance, readability, maintainability and security. Avoid nitpicks and avoid verbosity."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
tags, err := opts.TagLister(ctx, repo, opts.SkipTLSVerify)
if err != nil {
continue
}
tags, err := opts.TagLister(ctx, repo, opts.SkipTLSVerify)
if err != nil {
return nil, fmt.Errorf("listing tags for %s: %w", repo, err)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dev/plans/2026-04-30-upgrade-command.md` around lines 300 - 303, The loop
silently ignores errors from opts.TagLister (tags, err := opts.TagLister(ctx,
repo, opts.SkipTLSVerify)) by using continue, which can hide outages and yield
false “up to date” results; change the handling to return or propagate the error
(or aggregate it) to the caller instead of continuing — e.g., when err != nil,
wrap/contextualize the error with repo information and return it (or add it to
an error accumulator) so callers can surface failures explicitly.

Comment on lines +667 to +681
for _, c := range candidates {
if err := upgradeSkill(ctx, client, c, skipTLSVerify); err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "Error upgrading %s: %v\n", c.Installed.Name, err)
continue
}
fmt.Fprintf(cmd.OutOrStdout(), "Upgraded %s %s → %s (%s)\n",
c.Installed.Name, c.Installed.Version, c.LatestVersion, c.Installed.Target)
upgraded++
}

if all && upgraded > 0 {
fmt.Fprintf(cmd.OutOrStdout(), "\nUpgraded %d skill(s).\n", upgraded)
}

return nil
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Single-skill upgrade can fail but still return success.

The loop logs errors and continues, then returns nil. With one candidate, a failed upgrade still exits 0. Return an error when !all and an upgrade attempt fails (or when upgraded == 0 after failures).

Suggested adjustment
     for _, c := range candidates {
         if err := upgradeSkill(ctx, client, c, skipTLSVerify); err != nil {
-            fmt.Fprintf(cmd.ErrOrStderr(), "Error upgrading %s: %v\n", c.Installed.Name, err)
-            continue
+            if !all {
+                return fmt.Errorf("upgrading %s: %w", c.Installed.Name, err)
+            }
+            fmt.Fprintf(cmd.ErrOrStderr(), "Error upgrading %s: %v\n", c.Installed.Name, err)
+            continue
         }
         fmt.Fprintf(cmd.OutOrStdout(), "Upgraded %s %s → %s (%s)\n",
             c.Installed.Name, c.Installed.Version, c.LatestVersion, c.Installed.Target)
         upgraded++
     }

As per coding guidelines, "Focus on major issues impacting performance, readability, maintainability and security. Avoid nitpicks and avoid verbosity."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for _, c := range candidates {
if err := upgradeSkill(ctx, client, c, skipTLSVerify); err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "Error upgrading %s: %v\n", c.Installed.Name, err)
continue
}
fmt.Fprintf(cmd.OutOrStdout(), "Upgraded %s %s → %s (%s)\n",
c.Installed.Name, c.Installed.Version, c.LatestVersion, c.Installed.Target)
upgraded++
}
if all && upgraded > 0 {
fmt.Fprintf(cmd.OutOrStdout(), "\nUpgraded %d skill(s).\n", upgraded)
}
return nil
for _, c := range candidates {
if err := upgradeSkill(ctx, client, c, skipTLSVerify); err != nil {
if !all {
return fmt.Errorf("upgrading %s: %w", c.Installed.Name, err)
}
fmt.Fprintf(cmd.ErrOrStderr(), "Error upgrading %s: %v\n", c.Installed.Name, err)
continue
}
fmt.Fprintf(cmd.OutOrStdout(), "Upgraded %s %s → %s (%s)\n",
c.Installed.Name, c.Installed.Version, c.LatestVersion, c.Installed.Target)
upgraded++
}
if all && upgraded > 0 {
fmt.Fprintf(cmd.OutOrStdout(), "\nUpgraded %d skill(s).\n", upgraded)
}
return nil
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dev/plans/2026-04-30-upgrade-command.md` around lines 667 - 681, The loop
over candidates currently logs errors and continues, then always returns nil
which causes a non-all single-skill failure to exit 0; modify the logic in the
function containing candidates/upgradeSkill/all/upgraded so that when an upgrade
attempt fails and the flag all is false you immediately return a non-nil error
(include context like the skill name and underlying err), and if all is true but
upgraded == 0 after processing return an error indicating no skills were
upgraded; update any callers to handle the returned error and keep the existing
fmt.Fprintf lines for stdout/stderr for logging.

Comment thread internal/cli/install.go
- Return error on single-skill upgrade failure instead of exit 0
- Return error when --all upgrades nothing due to errors
- Skip version sync for digest-based refs (sha256:...)

Signed-off-by: Pavel Anni <panni@redhat.com>
@pavelanni pavelanni merged commit a1d0d12 into main May 1, 2026
6 checks passed
@pavelanni pavelanni deleted the feature/upgrade-command branch May 1, 2026 00:20
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