Skip to content

Commit 92e812b

Browse files
Merge pull request cli#13236 from cli/sammorrowdrums/skill-install-upstream-provenance
2 parents fe90f9a + 50f0f8f commit 92e812b

2 files changed

Lines changed: 352 additions & 1 deletion

File tree

pkg/cmd/skills/install/install.go

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package install
22

33
import (
4+
"encoding/base64"
45
"errors"
56
"fmt"
67
"io"
@@ -57,6 +58,7 @@ type InstallOptions struct {
5758
Force bool
5859
FromLocal bool // treat SkillSource as a local directory path
5960
AllowHiddenDirs bool // include skills in dot-prefixed directories
61+
Upstream bool // install from upstream when re-published skill detected
6062

6163
repo ghrepo.Interface // set when SkillSource is a GitHub repository
6264
localPath string // set when FromLocal is true
@@ -193,6 +195,10 @@ func NewCmdInstall(f *cmdutil.Factory, telemetry ghtelemetry.CommandRecorder, ru
193195
return err
194196
}
195197

198+
if err := cmdutil.MutuallyExclusive("--from-local and --upstream cannot be used together", opts.FromLocal, opts.Upstream); err != nil {
199+
return err
200+
}
201+
196202
if opts.Pin != "" && opts.SkillName != "" && strings.Contains(opts.SkillName, "@") {
197203
return cmdutil.FlagErrorf("cannot use --pin with an inline @version in the skill name")
198204
}
@@ -212,6 +218,7 @@ func NewCmdInstall(f *cmdutil.Factory, telemetry ghtelemetry.CommandRecorder, ru
212218
cmd.Flags().BoolVarP(&opts.Force, "force", "f", false, "Overwrite existing skills without prompting")
213219
cmd.Flags().BoolVar(&opts.FromLocal, "from-local", false, "Treat the argument as a local directory path instead of a repository")
214220
cmd.Flags().BoolVar(&opts.AllowHiddenDirs, "allow-hidden-dirs", false, "Include skills in hidden directories (e.g. .claude/skills/, .agents/skills/)")
221+
cmd.Flags().BoolVar(&opts.Upstream, "upstream", false, "Install from the upstream source when a re-published skill is detected")
215222
cmdutil.DisableAuthCheckFlag(cmd.Flags().Lookup("from-local"))
216223

217224
return cmd
@@ -248,13 +255,17 @@ func installRun(opts *InstallOptions) error {
248255
// Kick off the visibility fetch in parallel with the install work so
249256
// the extra API roundtrip doesn't add latency on the critical path.
250257
// The result is consumed when the telemetry event is emitted below.
258+
// Capture repo fields now to avoid a data race if opts.repo is
259+
// swapped during an upstream redirect.
251260
type visResult struct {
252261
vis discovery.RepoVisibility
253262
err error
254263
}
255264
visCh := make(chan visResult, 1)
265+
visOwner := opts.repo.RepoOwner()
266+
visRepo := opts.repo.RepoName()
256267
go func() {
257-
vis, err := discovery.FetchRepoVisibility(apiClient, hostname, opts.repo.RepoOwner(), opts.repo.RepoName())
268+
vis, err := discovery.FetchRepoVisibility(apiClient, hostname, visOwner, visRepo)
258269
visCh <- visResult{vis: vis, err: err}
259270
}()
260271

@@ -293,6 +304,43 @@ func installRun(opts *InstallOptions) error {
293304
}
294305
}
295306

307+
// Track upstream provenance detection result for telemetry.
308+
upstreamSource := "none"
309+
310+
// Check if the selected skill was re-published from an upstream source.
311+
// The re-publisher's SKILL.md will have github-repo metadata pointing
312+
// to the original source repo. If detected, offer to install directly
313+
// from upstream instead.
314+
if len(selectedSkills) == 1 && selectedSkills[0].BlobSHA != "" {
315+
upstreamRepo, detected, err := checkUpstreamProvenance(opts, apiClient, hostname, selectedSkills[0], resolved.SHA)
316+
if err != nil {
317+
return err
318+
}
319+
if upstreamRepo != nil {
320+
redirectDims := map[string]string{}
321+
select {
322+
case r := <-visCh:
323+
if r.err == nil && r.vis == discovery.RepoVisibilityPublic {
324+
redirectDims["from_owner"] = visOwner
325+
redirectDims["from_repo"] = visRepo
326+
}
327+
case <-time.After(visibilityWaitTimeout):
328+
}
329+
opts.Telemetry.Record(ghtelemetry.Event{
330+
Type: "skill_upstream_redirect",
331+
Dimensions: redirectDims,
332+
})
333+
opts.repo = upstreamRepo
334+
opts.SkillSource = ghrepo.FullName(upstreamRepo)
335+
opts.version = ""
336+
opts.Pin = ""
337+
return installRun(opts)
338+
}
339+
if detected {
340+
upstreamSource = "republisher"
341+
}
342+
}
343+
296344
printPreInstallDisclaimer(opts.IO.ErrOut, cs)
297345

298346
selectedHosts, err := resolveHosts(opts, canPrompt)
@@ -355,6 +403,7 @@ func installRun(opts *InstallOptions) error {
355403
dims := map[string]string{
356404
"agent_hosts": mapAgentHostsToIDs(selectedHosts),
357405
"skill_host_type": ghinstance.CategorizeHost(opts.repo.RepoHost()),
406+
"upstream_source": upstreamSource,
358407
}
359408
select {
360409
case r := <-visCh:
@@ -1169,3 +1218,85 @@ func filterHiddenDirSkills(opts *InstallOptions, allSkills []discovery.Skill) ([
11691218

11701219
return standard, nil
11711220
}
1221+
1222+
// checkUpstreamProvenance fetches the skill's SKILL.md via the contents API
1223+
// to check if it contains github-repo metadata pointing to a different
1224+
// repository, indicating the skill was re-published from an upstream source.
1225+
// In interactive mode, the user is asked whether to install from the
1226+
// re-publisher or redirect to the upstream. Non-interactive mode always
1227+
// installs from the re-publisher.
1228+
// Returns (repo to redirect to, whether upstream was detected, error).
1229+
func checkUpstreamProvenance(opts *InstallOptions, client *api.Client, hostname string, skill discovery.Skill, commitSHA string) (ghrepo.Interface, bool, error) {
1230+
apiPath := fmt.Sprintf("repos/%s/%s/contents/%s?ref=%s",
1231+
opts.repo.RepoOwner(), opts.repo.RepoName(),
1232+
skill.Path+"/SKILL.md", commitSHA)
1233+
var fileResp struct {
1234+
Content string `json:"content"`
1235+
Encoding string `json:"encoding"`
1236+
}
1237+
if err := client.REST(hostname, "GET", apiPath, nil, &fileResp); err != nil {
1238+
return nil, false, nil //nolint:nilerr // best-effort check; failing to fetch is not fatal
1239+
}
1240+
if fileResp.Encoding != "base64" {
1241+
return nil, false, nil
1242+
}
1243+
decoded, decodeErr := io.ReadAll(base64.NewDecoder(base64.StdEncoding, strings.NewReader(fileResp.Content)))
1244+
if decodeErr != nil {
1245+
return nil, false, nil //nolint:nilerr // best-effort; decode failure is not fatal
1246+
}
1247+
content := string(decoded)
1248+
1249+
result, parseErr := frontmatter.Parse(content)
1250+
if parseErr != nil || result.Metadata.Meta == nil {
1251+
//nolint:nilerr // unparseable frontmatter means no upstream to detect
1252+
return nil, false, nil
1253+
}
1254+
1255+
existingRepo, _ := result.Metadata.Meta["github-repo"].(string)
1256+
if existingRepo == "" {
1257+
return nil, false, nil
1258+
}
1259+
1260+
currentRepoURL := source.BuildRepoURL(hostname, opts.repo.RepoOwner(), opts.repo.RepoName())
1261+
if existingRepo == currentRepoURL {
1262+
return nil, false, nil
1263+
}
1264+
1265+
upstreamRepo, parseErr := source.ParseRepoURL(existingRepo)
1266+
if parseErr != nil {
1267+
//nolint:nilerr // invalid repo URL means we can't redirect; install normally
1268+
return nil, false, nil
1269+
}
1270+
1271+
cs := opts.IO.ColorScheme()
1272+
upstreamLabel := ghrepo.FullName(upstreamRepo)
1273+
repoSource := ghrepo.FullName(opts.repo)
1274+
1275+
fmt.Fprintf(opts.IO.ErrOut, "%s This skill was originally published in %s\n", cs.WarningIcon(), upstreamLabel)
1276+
1277+
if opts.Upstream {
1278+
fmt.Fprintf(opts.IO.ErrOut, "Redirecting install to %s...\n", upstreamLabel)
1279+
return upstreamRepo, true, nil
1280+
}
1281+
1282+
if !opts.IO.CanPrompt() {
1283+
fmt.Fprintf(opts.IO.ErrOut, " Installing from %s (use --upstream or interactive mode to choose upstream)\n", repoSource)
1284+
return nil, true, nil
1285+
}
1286+
1287+
choices := []string{
1288+
fmt.Sprintf("%s (re-publisher, recommended)", repoSource),
1289+
fmt.Sprintf("%s (upstream)", upstreamLabel),
1290+
}
1291+
idx, err := opts.Prompter.Select("Install from:", "", choices)
1292+
if err != nil {
1293+
return nil, true, err
1294+
}
1295+
1296+
if idx == 1 {
1297+
fmt.Fprintf(opts.IO.ErrOut, "Redirecting install to %s...\n", upstreamLabel)
1298+
return upstreamRepo, true, nil
1299+
}
1300+
1301+
return nil, true, nil
1302+
}

0 commit comments

Comments
 (0)