Skills: replace real git in publish tests with CommandStubber#13188
Conversation
5b5a1f7 to
0eb250a
Compare
| // for each remote in the map. The map key is the remote name and the value is the URL. | ||
| // Patterns use `git( .+)?` prefix to match optional `-C <dir>` arguments. | ||
| func stubGitRemote(cs *run.CommandStubber, remoteURLs map[string]string) { | ||
| // Build `git remote -v` output |
There was a problem hiding this comment.
nitpick: redundant comment (just the // Build ... line).
| for name, url := range remoteURLs { | ||
| runGitInDir(t, dir, "remote", "add", name, url) | ||
| runGitInDir(t, dir, "remote", "set-url", "--push", name, bareDir) | ||
| remoteLines += fmt.Sprintf("%s\t%s (fetch)\n%s\t%s (push)\n", name, url, name, url) |
There was a problem hiding this comment.
nitpick: can reuse args like this:
remoteLines += fmt.Sprintf("%[1]s\t%[2]s (fetch)\n%[1]s\t%[2]s (push)\n", name, url)| cs.Register(`git( .+)? remote -v`, 0, remoteLines) | ||
| cs.Register(`git( .+)? config --get-regexp \^remote\\\.`, 1, "") | ||
| for name, url := range remoteURLs { | ||
| cs.Register(fmt.Sprintf(`git( .+)? remote get-url -- %s`, name), 0, url+"\n") |
There was a problem hiding this comment.
nitpick: name should be regexp-escaped:
cs.Register(fmt.Sprintf(`git( .+)? remote get-url -- %s`, regexp.QuoteMeta(name)), 0, url+"\n")| assert.Equal(t, "0", strings.TrimSpace(string(out))) | ||
| cmdStubs: func(cs *run.CommandStubber) { | ||
| cs.Register(`git( .+)? symbolic-ref --quiet HEAD`, 0, "refs/heads/feature\n") | ||
| // rev-list fails when branch has no upstream |
There was a problem hiding this comment.
s/has no upstream/is not pushed/
| GitClient: &git.Client{}, | ||
| IO: ios, | ||
| Dir: dir, | ||
| GitClient: &git.Client{}, |
There was a problem hiding this comment.
We should initialise git.Client like this (otherwise it'll go through finding the real git path, see here and here):
&git.Client{
GhPath: "some/path/gh",
GitPath: "some/path/git",
},
Maybe do it via a helper function, or if all test cases need a git client (which they probably do), create the git client in the test loop.
babakks
left a comment
There was a problem hiding this comment.
I approve to prevent the blocker once we're merging this.
Replace all exec.Command("git", ...), initGitRepo, runGitInDir, and
newTestGitClientWithUpstream with run.Stub()/run.CommandStubber stubs.
Changes:
- Remove os/exec and strings imports; add fmt, regexp, internal/run
- Add newTestGitClient(), stubGitRemote(), stubEnsurePushed() helpers
- Remove initGitRepo, runGitInDir, newTestGitClientWithUpstream helpers
- Add cmdStubs field to TestPublishRun table struct
- Convert all test cases to use stub-based git interactions
- Use regexp.QuoteMeta for remote name patterns
- Use %[1]s/%[2]s format args in stubGitRemote
- Initialize git.Client with explicit GitPath to avoid real git resolution
- Rewrite TestEnsurePushed with stub-based tests
- Update TestDetectGitHubRemote_UsesDir and TestPublishRun_DirArgUsesTargetRemote
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
0eb250a to
5ae5d1d
Compare
|
Rebased and addressed all review feedback:
All tests pass, lint clean. |
There was a problem hiding this comment.
Pull request overview
Replaces real git invocations in skills publish tests with internal/run command stubs to make the test suite hermetic and consistent with project patterns.
Changes:
- Removed helper utilities that created and manipulated real git repositories via
exec.Command("git", ...). - Introduced
run.Stub()/run.CommandStubber-based helpers to stub git remote detection and push-check behavior. - Converted
TestPublishRun,TestEnsurePushed, and remote-selection tests to validate behavior through expected command invocations.
Show a summary per file
| File | Description |
|---|---|
pkg/cmd/skills/publish/publish_test.go |
Reworks publish/push/remote-related tests to stub git commands via run.CommandStubber instead of executing real git |
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)
pkg/cmd/skills/publish/publish_test.go:1583
- Same issue as the dedicated remote-detection test: the stubs here don't assert which directory git commands are executed against, so this regression test won't catch a bug where
publishRunmistakenly detects remotes from the factory client'sRepoDir(cwdRepo) instead of theDirargument (targetRepo). Consider making the git stubs directory-specific (or asserting on-C <targetRepo>via a stub callback) so the test meaningfully verifies the behavior it describes.
cs, cmdTeardown := run.Stub()
defer cmdTeardown(t)
stubGitRemote(cs, map[string]string{
"origin": "https://github.com/monalisa/target-repo.git",
})
cwdRepo := t.TempDir()
targetRepo := t.TempDir()
writeSkill(t, targetRepo, "my-skill", heredoc.Doc(`
---
name: my-skill
description: A test skill
license: MIT
---
Body text.
`))
ios, _, stdout, _ := iostreams.Test()
ios.SetStdoutTTY(true)
ios.SetStdinTTY(true)
ios.SetStderrTTY(true)
reg := &httpmock.Registry{}
defer reg.Verify(t)
stubAllSecureRemote(reg, "monalisa", "target-repo")
err := publishRun(&PublishOptions{
IO: ios,
Dir: targetRepo,
DryRun: true,
GitClient: &git.Client{GitPath: "some/path/git", RepoDir: cwdRepo},
HttpClient: func() (*http.Client, error) { return &http.Client{Transport: reg}, nil },
- Files reviewed: 1/1 changed files
- Comments generated: 5
| cs, cmdTeardown := run.Stub() | ||
| defer cmdTeardown(t) | ||
| stubGitRemote(cs, map[string]string{ | ||
| "origin": "https://github.com/monalisa/target-repo.git", | ||
| }) | ||
|
|
||
| cwdRepo := t.TempDir() | ||
| targetRepo := t.TempDir() | ||
| initGitRepo(t, targetRepo, map[string]string{ | ||
| "origin": "https://github.com/monalisa/target-repo.git", | ||
| }) | ||
|
|
||
| // gitClient points at cwd-repo (simulating factory-provided client) | ||
| gitClient := &git.Client{RepoDir: cwdRepo} | ||
| gitClient := &git.Client{GitPath: "some/path/git", RepoDir: cwdRepo} | ||
|
|
||
| // detectGitHubRemote should use targetRepo's remotes, not cwdRepo's | ||
| repo, err := detectGitHubRemote(gitClient, targetRepo) |
There was a problem hiding this comment.
This test is meant to verify that detectGitHubRemote uses the provided dir rather than the git client's existing RepoDir, but the current stubs don't distinguish between -C <cwdRepo> and -C <targetRepo> (the regexes match either). As a result, the test would still pass even if the implementation mistakenly used cwdRepo. Consider tightening the stubs (e.g., match on the expected -C <targetRepo> argument or add a callback assertion on the invoked args) so the test actually fails when the wrong directory is used.
This issue also appears on line 1551 of the same file.
| HttpClient: func() (*http.Client, error) { return &http.Client{Transport: reg}, nil }, | ||
| host: "github.com", | ||
| } | ||
| }, |
There was a problem hiding this comment.
In the "publish adds missing topic via --tag" test case, the assertions for user-facing output appear to have been removed (the case no longer sets wantStdout/wantStderr). This makes the test less effective at verifying that the topic-add message and publish output are actually emitted. Consider restoring an assertion for the expected output (e.g., that the "Added "agent-skills" topic" and/or "Published" messages are printed).
| }, | |
| }, | |
| wantStdout: "Added \"agent-skills\" topic\nPublished v1.0.0", |
| cs.Register(`git( .+)? remote -v`, 0, "origin\thttps://gitlab.com/hubot/bar.git (fetch)\norigin\thttps://gitlab.com/hubot/bar.git (push)\n") | ||
| cs.Register(`git( .+)? config --get-regexp \^remote\\\.`, 1, "") | ||
| cs.Register(fmt.Sprintf(`git( .+)? remote get-url -- %s`, regexp.QuoteMeta("origin")), 0, "https://gitlab.com/hubot/bar.git\n") |
There was a problem hiding this comment.
This cmd stub setup duplicates the same git remote -v / git config --get-regexp ... / git remote get-url registrations that stubGitRemote already provides, just to satisfy the second Remotes() call from detectMissingRepoDiagnostic. To reduce duplication and make the intent clearer, consider reusing stubGitRemote for the second call as well (e.g., by calling it twice or by introducing a small helper that registers the remote stubs N times).
| cs.Register(`git( .+)? remote -v`, 0, "origin\thttps://gitlab.com/hubot/bar.git (fetch)\norigin\thttps://gitlab.com/hubot/bar.git (push)\n") | |
| cs.Register(`git( .+)? config --get-regexp \^remote\\\.`, 1, "") | |
| cs.Register(fmt.Sprintf(`git( .+)? remote get-url -- %s`, regexp.QuoteMeta("origin")), 0, "https://gitlab.com/hubot/bar.git\n") | |
| stubGitRemote(cs, map[string]string{ | |
| "origin": "https://gitlab.com/hubot/bar.git", | |
| }) |
| cmdStubs: func(cs *run.CommandStubber) { | ||
| cs.Register(`git( .+)? check-ignore -q -- .agents/skills`, 1, "") | ||
| cs.Register(`git( .+)? remote -v`, 0, "") | ||
| cs.Register(`git( .+)? config --get-regexp \^remote\\\.`, 1, "") |
There was a problem hiding this comment.
Within this test case, the stubbed git check-ignore exit status 1 is treated as an error (because CommandStubber's error doesn't satisfy *exec.ExitError), so the behavior under test is effectively the "could not verify" warning path rather than the true "not gitignored" path. Consider renaming the case accordingly (or adjusting the stubbing approach) so the test name matches what it's validating.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This MR contains the following updates: | Package | Update | Change | |---|---|---| | [cli/cli](https://github.com/cli/cli) | minor | `v2.89.0` → `v2.90.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.90.0`](https://github.com/cli/cli/releases/tag/v2.90.0): GitHub CLI 2.90.0 [Compare Source](cli/cli@v2.89.0...v2.90.0) #### Manage agent skills with `gh skill` (Public Preview) [Agent skills](https://agentskills.io) are portable sets of instructions, scripts, and resources that teach AI coding agents how to perform specific tasks. The new `gh skill` command makes it easy to discover, install, manage, and publish agent skills from GitHub repositories - right from the CLI. ``` # Discover skills gh skill search copilot # Preview a skill without installing gh skill preview github/awesome-copilot documentation-writer # Install a skill gh skill install github/awesome-copilot documentation-writer # Pin to a specific version gh skill install github/awesome-copilot documentation-writer --pin v1.2.0 # Check installed skills for updates gh skill update --all # Validate and publish your own skills gh skill publish --dry-run ``` Skills are automatically installed to the correct directory for your agent host. `gh skill` supports GitHub Copilot, Claude Code, Cursor, Codex, Gemini CLI, and Antigravity. Target a specific agent and scope with `--agent` and `--scope` flags. `gh skill publish` validates skills against the [Agent Skills specification](https://agentskills.io/specification) and checks remote settings like tag protection and immutable releases to improve supply chain security. Read the full announcement on the [GitHub Blog](https://github.blog/changelog/2026-04-16-manage-agent-skills-with-github-cli/). `gh skill` is launching in public preview and is subject to change without notice. #### Official extension suggestions When you run a command that matches a known official extension that isn't installed (e.g. `gh stack`), the CLI now offers to install it instead of showing a generic "unknown command" error. This feature is available for [github/gh-aw](https://github.com/github/gh-aw) and [github/gh-stack](https://github.com/github/gh-stack). When possible, you'll be prompted to install immediately. When prompting isn't possible, the CLI prints the `gh extension install` command to run. #### `gh extension install` no longer requires authentication `gh extension install` previously required a valid auth token even though it only needs to download a public release asset. The auth check has been removed, so you can install extensions without being logged in. #### What's Changed ##### ✨ Features - Add `gh skill` command group: install, preview, search, update, publish by [@​SamMorrowDrums](https://github.com/SamMorrowDrums) in [#​13165](cli/cli#13165) - Suggest and install official extensions for unknown commands by [@​BagToad](https://github.com/BagToad) in [#​13175](cli/cli#13175) - `gh skill publish`: auto-push unpushed commits before publish by [@​SamMorrowDrums](https://github.com/SamMorrowDrums) in [#​13171](cli/cli#13171) - Disable auth check for `gh extension install` by [@​BagToad](https://github.com/BagToad) in [#​13176](cli/cli#13176) ##### 🐛 Fixes - Fix infinite loop in `gh release list --limit 0` by [@​Bahtya](https://github.com/Bahtya) in [#​13097](cli/cli#13097) - Ensure `api` and `auth` commands record agentic invocations by [@​williammartin](https://github.com/williammartin) in [#​13046](cli/cli#13046) - Disable auth check for local-only skill flags by [@​SamMorrowDrums](https://github.com/SamMorrowDrums) in [#​13173](cli/cli#13173) - URL-encode parentPath in skills discovery API call by [@​SamMorrowDrums](https://github.com/SamMorrowDrums) in [#​13172](cli/cli#13172) - Fix: use target directory remotes in skills publish by [@​SamMorrowDrums](https://github.com/SamMorrowDrums) in [#​13169](cli/cli#13169) - Fix: preserve namespace in skills search deduplication by [@​SamMorrowDrums](https://github.com/SamMorrowDrums) in [#​13170](cli/cli#13170) ##### 📚 Docs & Chores - docs: include PGP key fingerprints by [@​babakks](https://github.com/babakks) in [#​13112](cli/cli#13112) - docs: add sha/md5 checksums of keyring files by [@​babakks](https://github.com/babakks) in [#​13150](cli/cli#13150) - docs: fix SHA512 checksum for GPG key by [@​timsu92](https://github.com/timsu92) in [#​13157](cli/cli#13157) - docs(skill): polish skill commandset docs by [@​babakks](https://github.com/babakks) in [#​13183](cli/cli#13183) - Document dependency CVE policy in SECURITY.md by [@​BagToad](https://github.com/BagToad) in [#​13119](cli/cli#13119) - Replace github.com/golang/snappy with klauspost/compress/snappy by [@​thaJeztah](https://github.com/thaJeztah) in [#​13048](cli/cli#13048) - chore: bump to go1.26.2 by [@​babakks](https://github.com/babakks) in [#​13116](cli/cli#13116) - chore: delete experimental script/debian-devel by [@​babakks](https://github.com/babakks) in [#​13127](cli/cli#13127) - Suggest first party extensions by [@​williammartin](https://github.com/williammartin) in [#​13182](cli/cli#13182) - Add cli/skill-reviewers as CODEOWNERS for skills packages by [@​BagToad](https://github.com/BagToad) in [#​13189](cli/cli#13189) - Add [@​cli/code-reviewers](https://github.com/cli/code-reviewers) to all CODEOWNERS rules by [@​BagToad](https://github.com/BagToad) in [#​13190](cli/cli#13190) - Address post-merge review feedback for skills commands by [@​SamMorrowDrums](https://github.com/SamMorrowDrums) in [#​13185](cli/cli#13185) - Fix skills-publish-dry-run acceptance test error message mismatch by [@​SamMorrowDrums](https://github.com/SamMorrowDrums) in [#​13187](cli/cli#13187) - Skills: replace real git in publish tests with CommandStubber by [@​SamMorrowDrums](https://github.com/SamMorrowDrums) in [#​13188](cli/cli#13188) - Remove redundant nil-client fallback in skills publish by [@​SamMorrowDrums](https://github.com/SamMorrowDrums) in [#​13168](cli/cli#13168) - Publish: use shared discovery logic instead of requiring skills/ directory by [@​SamMorrowDrums](https://github.com/SamMorrowDrums) in [#​13167](cli/cli#13167) #####Dependencies - chore(deps): bump github.com/klauspost/compress from 1.18.4 to 1.18.5 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​13071](cli/cli#13071) - chore(deps): bump github.com/yuin/goldmark from 1.7.16 to 1.8.2 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​13045](cli/cli#13045) - chore(deps): bump charm.land/bubbles/v2 from 2.0.0 to 2.1.0 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​13051](cli/cli#13051) - chore(deps): bump github.com/sigstore/timestamp-authority/v2 from 2.0.3 to 2.0.6 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​13152](cli/cli#13152) - chore(deps): bump github.com/google/go-containerregistry from 0.21.3 to 0.21.4 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​13129](cli/cli#13129) - chore(deps): bump github.com/sigstore/protobuf-specs from 0.5.0 to 0.5.1 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​13128](cli/cli#13128) - chore(deps): bump github.com/in-toto/attestation from 1.1.2 to 1.2.0 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​13044](cli/cli#13044) - chore(deps): bump advanced-security/filter-sarif from 1.0.1 to 1.1 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​12918](cli/cli#12918) - chore(deps): bump google.golang.org/grpc from 1.79.3 to 1.80.0 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​13076](cli/cli#13076) - chore(deps): bump github.com/hashicorp/go-version from 1.8.0 to 1.9.0 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​13065](cli/cli#13065) #### New Contributors - [@​thaJeztah](https://github.com/thaJeztah) made their first contribution in [#​13048](cli/cli#13048) - [@​Bahtya](https://github.com/Bahtya) made their first contribution in [#​13097](cli/cli#13097) - [@​timsu92](https://github.com/timsu92) made their first contribution in [#​13157](cli/cli#13157) - [@​SamMorrowDrums](https://github.com/SamMorrowDrums) made their first contribution in [#​13173](cli/cli#13173) **Full Changelog**: <cli/cli@v2.89.0...v2.90.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 [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMjMuNiIsInVwZGF0ZWRJblZlciI6IjQzLjEyMy42IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJSZW5vdmF0ZSBCb3QiLCJhdXRvbWF0aW9uOmJvdC1hdXRob3JlZCIsImRlcGVuZGVuY3ktdHlwZTo6bWlub3IiXX0=-->
Summary
Follow-up from #13184 / PR #13185. Replaces all real
exec.Command("git", ...)usage inpkg/cmd/skills/publish/publish_test.gowithrun.Stub()/run.CommandStubber, as requested in PR #13171 review feedback.Changes
initGitRepo(created real git repos with bare remotes),runGitInDir,newTestGitClientWithUpstreamstubGitRemote(stubsgit remote -v,git config --get-regexp,git remote get-url) andstubEnsurePushed(stubsgit symbolic-ref,git rev-list, secondCurrentBranchcall)TestPublishRunto use command stubs instead of real gitTestEnsurePushedto verify correct git commands via stubsTestDetectGitHubRemote_UsesDirandTestPublishRun_DirArgUsesTargetRemote** to use stubsos/exec,strings; Added:internal/runNotes
installed_skill_dirs_not_gitignored_warnstest expectation was adjusted: with stubs,IsIgnoredcan't distinguish exit code 1 (not ignored) from other errors becauseerrWithExitCodedoesn't satisfy*exec.ExitError. The test still validates the gitignore warning is shown.git( .+)?prefix to match optional-C <dir>arguments injected bygit.Client.Fixes #13186