feat(skills): detect re-published skills and offer upstream install#13236
Conversation
6a54097 to
8f184f2
Compare
8c278cc to
696ddf9
Compare
696ddf9 to
326fbaa
Compare
There was a problem hiding this comment.
Pull request overview
Adds end-to-end upstream provenance tracking for re-published skills so installs/updates can preserve original-source metadata and (optionally) fetch future updates from upstream.
Changes:
- Extend frontmatter metadata injection to record upstream repo/tree/path and a track-upstream flag, with cleanup when upstream is nil.
- Add installer-side detection of upstream metadata from SKILL.md and a command-layer callback to choose re-publisher vs upstream tracking.
- Update update/install/publish commands and tests to preserve provenance across forced updates and improve publish-time guidance.
Show a summary per file
| File | Description |
|---|---|
| pkg/cmd/skills/update/update_test.go | Adds unit coverage for parsing upstream metadata and preserving provenance on update. |
| pkg/cmd/skills/update/update.go | Parses upstream fields, resolves updates from upstream when tracking, and preserves provenance through re-installs. |
| pkg/cmd/skills/publish/publish.go | Enhances publish error messaging with re-publisher guidance when github metadata is present. |
| pkg/cmd/skills/install/install.go | Wires upstream-detection callback and adds interactive prompting/warnings for tracking choice. |
| internal/skills/installer/installer_test.go | Adds unit tests for detection, verification, self-referential handling, and preset upstream behavior. |
| internal/skills/installer/installer.go | Implements upstream detection, optional best-effort SHA verification, and injects upstream metadata during install. |
| internal/skills/frontmatter/frontmatter_test.go | Adds tests for upstream metadata injection/cleanup and track-upstream flag. |
| internal/skills/frontmatter/frontmatter.go | Introduces UpstreamInfo and extends InjectGitHubMetadata to write upstream fields. |
| acceptance/testdata/skills/skills-install-republished.txtar | Acceptance test ensuring upstream provenance survives forced update. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comments suppressed due to low confidence (1)
internal/skills/installer/installer.go:377
- In the
existingRepo == currentRepoURLbranch,detectAndResolveUpstreamcallsOnUpstreamDetected(...)but then returns(nil, false)unconditionally, meaning any user choice returned by the callback is ignored. With the current CLI callback implementation, this can lead to an interactive prompt that has no effect. Suggestion: avoid invokingOnUpstreamDetectedin this branch unless you can guarantee the callback is “warning-only”, or extend the callback contract (e.g., an explicitstale boolargument) so the command layer can suppress prompting for this case.
// Check whether the existing metadata points to a different repo
currentRepoURL := source.BuildRepoURL(opts.Host, opts.Owner, opts.Repo)
if existingRepo == currentRepoURL {
// github-repo points to this repo, but if github-upstream-repo is
// also present this looks like a previously installed skill that was
// committed back into the source repo. Warn and install normally,
// clearing the stale upstream fields.
if upstreamURL, _ := result.Metadata.Meta["github-upstream-repo"].(string); upstreamURL != "" {
opts.OnUpstreamDetected(frontmatter.UpstreamInfo{RepoURL: upstreamURL}, false)
}
return nil, false
- Files reviewed: 9/9 changed files
- Comments generated: 4
| // Self-referential: github-repo points to this repo but | ||
| // github-upstream-repo is also present. This is a previously | ||
| // installed skill committed back into the source repo. The | ||
| // installer ignores the return value in this case - we just warn. | ||
| if upstreamLabel == repoSource { | ||
| fmt.Fprintf(opts.IO.ErrOut, "%s This skill contains stale install metadata (upstream points to itself). Upstream tracking will be cleared.\n", cs.WarningIcon()) | ||
| return nil | ||
| } | ||
|
|
||
| fmt.Fprintf(opts.IO.ErrOut, "%s This skill was originally published in %s\n", cs.WarningIcon(), upstreamLabel) | ||
|
|
||
| if !opts.IO.CanPrompt() { | ||
| fmt.Fprintf(opts.IO.ErrOut, " Tracking %s for updates (use interactive mode to choose upstream)\n", repoSource) | ||
| return &installer.UpstreamDecision{ | ||
| Upstream: &upstream, | ||
| TrackUpstream: false, | ||
| } | ||
| } | ||
|
|
||
| choices := []string{ | ||
| fmt.Sprintf("%s (re-publisher, recommended)", repoSource), | ||
| fmt.Sprintf("%s (upstream)", upstreamLabel), | ||
| } | ||
| idx, err := opts.Prompter.Select("Track updates from:", "", choices) | ||
| if err != nil { | ||
| // On prompt error, default to re-publisher | ||
| return &installer.UpstreamDecision{ | ||
| Upstream: &upstream, | ||
| TrackUpstream: false, | ||
| } | ||
| } | ||
|
|
||
| trackUpstream := idx == 1 | ||
| pinOrVersion := opts.Pin | ||
| if pinOrVersion == "" { |
326fbaa to
e8f50f4
Compare
BagToad
left a comment
There was a problem hiding this comment.
Looks pretty good, but I think there's some error handling we should fix. I'm continuing to review, but figured I should post that comment first 😁
| - Each skill name matches its directory name | ||
| - Required frontmatter fields (name, description) are present | ||
| - allowed-tools is a string, not an array | ||
| - Install metadata (%[1]smetadata.github-*%[1]s) is stripped if present |
There was a problem hiding this comment.
❓ Do you think it's worth updating this with new upstream info? I'm not sure it is, but here's a copilot suggested edit:
- - Install metadata (%[1]smetadata.github-*%[1]s) is stripped if present
+ - Install metadata (%[1]smetadata.github-*%[1]s) is reported as an
+ error if present; use %[1]s--fix%[1]s to strip it. When
+ %[1]smetadata.github-upstream-repo%[1]s is also present (i.e. a
+ re-published skill), the error includes guidance on preserving
+ upstream provenance by committing the files and releasing
+ directly instead of stripping with %[1]s--fix%[1]s.|
@BagToad I decided in last 10 mins with @tommaso-moro that I should simplify and give user option of tracking from one or other, and then just using the standard front-matter and removing much complexity. Does that sound better to you? To me it makes it clear while preserving the simple format. |
| } | ||
| } | ||
|
|
||
| dims := map[string]string{ |
There was a problem hiding this comment.
💭 Wondering if you'd like telemetry for this upstream vs publisher?
| --- | ||
| name: code-review | ||
| metadata: | ||
| github-repo: https://github.com/curator/curated-skills |
There was a problem hiding this comment.
❗ let's use official or fake names like monalisa to prevent any name-squatting problems. I don't think it would actually cause any problems but better to be consistent with rest of the tests anyway.
|
@SamMorrowDrums sorry just saw your question 👀
That sounds good! Definitely much simpler. And if they want to switch to a different source they can always reinstall from that other source. |
e8f50f4 to
31c30f0
Compare
ddb8ea4 to
a4a9d80
Compare
a4a9d80 to
131c0d0
Compare
babakks
left a comment
There was a problem hiding this comment.
Thanks for the PR! A few comments we need to fix.
When installing a skill whose SKILL.md contains github-repo metadata pointing to a different repository, the CLI detects it as a re-published skill and offers to redirect the install to the upstream source. In interactive mode, the user is prompted to choose between the re-publisher (default) and the upstream. In non-interactive mode, the install proceeds from the re-publisher with a notice. The --upstream flag skips the prompt and redirects to upstream directly, enabling non-interactive upstream installs in CI/scripts. If the user chooses upstream, the install restarts from that repo, resolving the latest version and discovering skills fresh. A skill_upstream_redirect telemetry event is emitted to track redirects. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
131c0d0 to
50f0f8f
Compare
This MR contains the following updates: | Package | Update | Change | |---|---|---| | [cli/cli](https://github.com/cli/cli) | minor | `v2.90.0` → `v2.92.0` | MR created with the help of [el-capitano/tools/renovate-bot](https://gitlab.com/el-capitano/tools/renovate-bot). **Proposed changes to behavior should be submitted there as MRs.** --- ### Release Notes <details> <summary>cli/cli (cli/cli)</summary> ### [`v2.92.0`](https://github.com/cli/cli/releases/tag/v2.92.0): GitHub CLI 2.92.0 [Compare Source](cli/cli@v2.91.0...v2.92.0) #### Support GitHub Enterprise Cloud (GHEC) in `skill` commandset Now `gh skill` subcommands (`install`, `preview`, `publish`, `search`, `update`) are able to work with [GHEC](https://docs.github.com/en/enterprise-cloud@latest/admin/overview/about-github-enterprise-cloud) hosts with data residency. #### Add `--allow-hidden-dirs` flag to `skill preview` Following the addition of `--allow-hidden-dirs` to `skill install` in the previous release, now the flag is also supported in `skill preview`, allowing users to preview skills located in hidden (dot-prefixed) directories such as `.claude/skills/`, `.agents/skills/`, and `.github/skills/`. #### What's Changed ##### ✨ Features - feat(skills): add --allow-hidden-dirs flag to preview command by [@​SamMorrowDrums](https://github.com/SamMorrowDrums) in [#​13265](cli/cli#13265) - feat(skills): support GHEC with data residency hosts by [@​SamMorrowDrums](https://github.com/SamMorrowDrums) in [#​13264](cli/cli#13264) ##### 🐛 Fixes - Fix SetSampleRate not updating sample\_rate dimension by [@​williammartin](https://github.com/williammartin) in [#​13259](cli/cli#13259) - Fix log terminal injection by [@​williammartin](https://github.com/williammartin) in [#​13272](cli/cli#13272) - Add "Resource not accessible" to ProjectsV2IgnorableError by [@​maxbeizer](https://github.com/maxbeizer) in [#​13281](cli/cli#13281) ##### 📚 Docs & Chores - fix: using variable interpolation \`${{ in deployment.yml... by [@​orbisai0security](https://github.com/orbisai0security) in [#​13258](cli/cli#13258) - docs: correct typo in Linux Homebrew copy by [@​cassidyjames](https://github.com/cassidyjames) in [#​13273](cli/cli#13273) - Install skills flat by Name, not namespaced InstallName by [@​SamMorrowDrums](https://github.com/SamMorrowDrums) in [#​13266](cli/cli#13266) - chore: fix zsh completion on debian by [@​babakks](https://github.com/babakks) in [#​13274](cli/cli#13274) - Add trust disclaimer to extension help text by [@​travellertales](https://github.com/travellertales) in [#​13296](cli/cli#13296) - Bump Go to 1.26.2 by [@​github-actions](https://github.com/github-actions)\[bot] in [#​13301](cli/cli#13301) #####Dependencies - chore(deps): bump github.com/mattn/go-isatty from 0.0.20 to 0.0.21 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​13161](cli/cli#13161) - chore(deps): bump github.com/google/go-containerregistry from 0.21.4 to 0.21.5 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​13162](cli/cli#13162) - chore(deps): bump charm.land/lipgloss/v2 from 2.0.2 to 2.0.3 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​13163](cli/cli#13163) - chore(deps): bump charm.land/bubbletea/v2 from 2.0.2 to 2.0.6 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​13206](cli/cli#13206) - chore(deps): bump github.com/gdamore/tcell/v2 from 2.13.8 to 2.13.9 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​13241](cli/cli#13241) - chore(deps): bump github.com/mattn/go-isatty from 0.0.21 to 0.0.22 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​13298](cli/cli#13298) #### New Contributors - [@​orbisai0security](https://github.com/orbisai0security) made their first contribution in [#​13258](cli/cli#13258) - [@​cassidyjames](https://github.com/cassidyjames) made their first contribution in [#​13273](cli/cli#13273) - [@​travellertales](https://github.com/travellertales) made their first contribution in [#​13296](cli/cli#13296) **Full Changelog**: <cli/cli@v2.91.0...v2.92.0> ### [`v2.91.0`](https://github.com/cli/cli/releases/tag/v2.91.0): GitHub CLI 2.91.0 [Compare Source](cli/cli@v2.90.0...v2.91.0) #### GitHub CLI now collects pseudonymous telemetry To better understand how features are used in practice, especially as agentic adoption grows, GitHub CLI now sends pseudonymous telemetry. See [Telemetry](https://cli.github.com/telemetry) for more details on what's collected, why, and how to opt out. #### Support more agents in `gh skill` Thanks to community feedback, `gh` now supports a large number of agent hosts. Run `gh skill install --help` for the list of available agents. #### Improve skill discovery `gh skill install` now adds the `--allow-hidden-dirs` flag to support discovering skills in hidden (dot-prefixed) directories such as `.claude/skills/`, `.agents/skills/`, and `.github/skills/`. #### Detect skills re-published from other sources GitHut CLI now detects if the skill to be installed is re-published from an upstream source and offers the option to install it from there. The `--upstream` flag is also added for non-interactive use cases. #### What's Changed ##### ✨ Features - Add support for installation in multiple agent hosts in `gh skills install` by [@​tommaso-moro](https://github.com/tommaso-moro) in [#​13209](cli/cli#13209) - Add --allow-hidden-dirs flag to gh skill install by [@​SamMorrowDrums](https://github.com/SamMorrowDrums) in [#​13213](cli/cli#13213) - Make skill discovery less strict: support nested `skills/` directories by [@​SamMorrowDrums](https://github.com/SamMorrowDrums) in [#​13235](cli/cli#13235) - feat(skills): detect re-published skills and offer upstream install by [@​SamMorrowDrums](https://github.com/SamMorrowDrums) in [#​13236](cli/cli#13236) ##### 🐛 Fixes - Fix `skills publish --fix` to not publish by [@​SamMorrowDrums](https://github.com/SamMorrowDrums) in [#​13237](cli/cli#13237) - fix(skills): match skills by install name in preview command by [@​SamMorrowDrums](https://github.com/SamMorrowDrums) in [#​13249](cli/cli#13249) ##### 📚 Docs & Chores - Remove misleading text by [@​tommaso-moro](https://github.com/tommaso-moro) in [#​13203](cli/cli#13203) - Add sampled command telemetry by [@​williammartin](https://github.com/williammartin) in [#​13191](cli/cli#13191) - Do not send telemetry for aliases by [@​williammartin](https://github.com/williammartin) in [#​13192](cli/cli#13192) - Add skills specific telemetry by [@​williammartin](https://github.com/williammartin) in [#​13204](cli/cli#13204) - Record CI context in telemetry by [@​williammartin](https://github.com/williammartin) in [#​13210](cli/cli#13210) - Record official extension telemetry by [@​williammartin](https://github.com/williammartin) in [#​13205](cli/cli#13205) - Add telemetry command by [@​williammartin](https://github.com/williammartin) in [#​13253](cli/cli#13253) - Log when there is no telemetry by [@​williammartin](https://github.com/williammartin) in [#​13255](cli/cli#13255) - docs(skills): add gh and gh-skill agent skills by [@​BagToad](https://github.com/BagToad) in [#​13244](cli/cli#13244) - Enable telemetry without env var by [@​williammartin](https://github.com/williammartin) in [#​13254](cli/cli#13254) **Full Changelog**: <cli/cli@v2.90.0...v2.91.0> </details> --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever MR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this MR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this MR, check this box --- This MR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xNjkuNCIsInVwZGF0ZWRJblZlciI6IjQzLjE2OS40IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJSZW5vdmF0ZSBCb3QiLCJhdXRvbWF0aW9uOmJvdC1hdXRob3JlZCIsImRlcGVuZGVuY3ktdHlwZTo6bWlub3IiXX0=-->
Summary
When installing a skill from a repo that re-published it from an upstream source, the SKILL.md contains
github-repometadata pointing to the original publisher. This PR detects that and offers the user a choice: install from the re-publisher (default) or redirect to the upstream.Changes
pkg/cmd/skills/install/install.gocheckUpstreamProvenance- after skill selection, fetches SKILL.md via contents API, parses frontmatter, checks ifgithub-repopoints elsewhere--upstreamflag: skip prompt, redirect to upstream directly (enables CI/script use)installRunwith the upstream repo (fresh version resolution, discovery)skill_upstream_redirecttelemetry event emitted on redirect--from-localand--upstreamare mutually exclusivepkg/cmd/skills/install/install_test.goTestInstallRun_UpstreamDetectioncases: interactive re-publisher, interactive upstream, non-interactive default,--upstreamflagTestNewCmdInstallcases:--upstreamflag parsing,--from-local --upstreammutual exclusionBehavior
github-repometadatagithub-repopoints to same repogithub-repopoints elsewhere (interactive)github-repopoints elsewhere (non-interactive)--upstreamflag set--from-local --upstreamWhat this does NOT change
InjectGitHubMetadataoverwritesgithub-repoto the chosen source as beforeEnd-to-end testing
Manually verified against real repos using the built CLI binary:
Setup: Installed
git-commitfromgithub/awesome-copilotinto a private repo (sammorrowdrums/test-republished-skills), committed the SKILL.md with itsgithub-repometadata pointing to the upstream, pushed.1. Normal install (no upstream metadata) - works unchanged:
2. Local install - works unchanged:
3. Re-published install (non-interactive) - detects upstream, installs from re-publisher:
4. Re-published install with --upstream flag - redirects to upstream:
5. Interactive prompt (user picks upstream) - redirects:
6. Mutual exclusion: