Merge branch 'trunk' into sammorrowdrums/skills-replace-git-in-publish-tests

This commit is contained in:
Sam Morrow 2026-04-16 19:06:38 +02:00 committed by GitHub
commit bce04e3245
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 258 additions and 10 deletions

21
.github/CODEOWNERS vendored
View file

@ -1,15 +1,18 @@
* @cli/code-reviewers
pkg/cmd/codespace/ @cli/codespaces
internal/codespaces/ @cli/codespaces
pkg/cmd/codespace/ @cli/codespaces @cli/code-reviewers
internal/codespaces/ @cli/codespaces @cli/code-reviewers
# Limit Package Security team ownership to the attestation command package and related integration tests
pkg/cmd/attestation/ @cli/package-security
pkg/cmd/release/attestation/ @cli/package-security
pkg/cmd/release/verify/ @cli/package-security
pkg/cmd/release/verify-asset/ @cli/package-security
pkg/cmd/release/shared/ @cli/package-security
pkg/cmd/attestation/ @cli/package-security @cli/code-reviewers
pkg/cmd/release/attestation/ @cli/package-security @cli/code-reviewers
pkg/cmd/release/verify/ @cli/package-security @cli/code-reviewers
pkg/cmd/release/verify-asset/ @cli/package-security @cli/code-reviewers
pkg/cmd/release/shared/ @cli/package-security @cli/code-reviewers
test/integration/attestation-cmd @cli/package-security
test/integration/attestation-cmd @cli/package-security @cli/code-reviewers
pkg/cmd/attestation/verification/embed/tuf-repo.github.com/ @cli/tuf-root-reviewers
pkg/cmd/attestation/verification/embed/tuf-repo.github.com/ @cli/tuf-root-reviewers @cli/code-reviewers
pkg/cmd/skills/ @cli/skill-reviewers @cli/code-reviewers
internal/skills/ @cli/skill-reviewers @cli/code-reviewers

View file

@ -135,6 +135,7 @@ for _, tt := range tests {
- Add godoc comments to all exported functions, types, and constants
- Avoid unnecessary code comments — only comment when the *why* isn't obvious from the code
- Do not comment just to restate what the code does
- Never use em dashes (—) in code, comments, or documentation; use regular dashes (-) or rewrite the sentence instead
## Error Handling

View file

@ -1,6 +1,6 @@
# Publish dry-run from a directory with no skills/ should fail gracefully
! exec gh skill publish --dry-run $WORK
stderr 'no skills/ directory found'
stderr 'no skills found in'
# Publish dry-run against a valid skill directory should succeed
exec gh skill publish --dry-run $WORK/test-repo

View file

@ -0,0 +1,72 @@
package root
import (
"fmt"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/prompter"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/extensions"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/spf13/cobra"
)
// NewCmdOfficialExtensionStub creates a hidden stub command for an official
// extension that has not yet been installed. When invoked, it suggests
// installing the extension and, in interactive sessions, offers to do so
// immediately. After a successful install, the extension is dispatched with
// the original arguments.
func NewCmdOfficialExtensionStub(io *iostreams.IOStreams, p prompter.Prompter, em extensions.ExtensionManager, ext *extensions.OfficialExtension) *cobra.Command {
cmd := &cobra.Command{
Use: ext.Name,
Short: fmt.Sprintf("Install the official %s extension", ext.Name),
Hidden: true,
GroupID: "extension",
// Accept any args/flags the user may have passed so we don't get
// cobra validation errors before reaching RunE.
DisableFlagParsing: true,
RunE: func(cmd *cobra.Command, args []string) error {
return officialExtensionStubRun(io, p, em, ext)
},
}
cmdutil.DisableAuthCheck(cmd)
return cmd
}
func officialExtensionStubRun(io *iostreams.IOStreams, p prompter.Prompter, em extensions.ExtensionManager, ext *extensions.OfficialExtension) error {
stderr := io.ErrOut
if !io.CanPrompt() {
fmt.Fprint(stderr, heredoc.Docf(`
%[1]s is available as an official extension.
To install it, run:
gh extension install %[2]s/%[3]s
`, fmt.Sprintf("gh %s", ext.Name), ext.Owner, ext.Repo))
return nil
}
prompt := heredoc.Docf(`
%[1]s is available as an official extension.
Would you like to install it now?
`, fmt.Sprintf("gh %s", ext.Name))
confirmed, err := p.Confirm(prompt, true)
if err != nil {
return err
}
if !confirmed {
return nil
}
repo := ext.Repository()
io.StartProgressIndicatorWithLabel(fmt.Sprintf("Installing %s/%s...", ext.Owner, ext.Repo))
installErr := em.Install(repo, "")
io.StopProgressIndicator()
if installErr != nil {
return fmt.Errorf("failed to install extension: %w", installErr)
}
fmt.Fprintf(stderr, "Successfully installed %s/%s\n", ext.Owner, ext.Repo)
return nil
}

View file

@ -0,0 +1,119 @@
package root
import (
"fmt"
"testing"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/prompter"
"github.com/cli/cli/v2/pkg/extensions"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestOfficialExtensionStubRun(t *testing.T) {
ext := &extensions.OfficialExtension{Name: "cool", Owner: "github", Repo: "gh-cool"}
tests := []struct {
name string
isTTY bool
confirmResult bool
confirmErr error
installErr error
wantErr string
wantStderr string
wantInstalled bool
}{
{
name: "non-TTY prints install instructions",
isTTY: false,
wantStderr: "gh extension install github/gh-cool",
},
{
name: "TTY confirmed installs",
isTTY: true,
confirmResult: true,
wantStderr: "Successfully installed github/gh-cool",
wantInstalled: true,
},
{
name: "TTY declined does not install",
isTTY: true,
confirmResult: false,
},
{
name: "TTY prompt error is propagated",
isTTY: true,
confirmErr: fmt.Errorf("prompt interrupted"),
wantErr: "prompt interrupted",
},
{
name: "TTY install error is propagated",
isTTY: true,
confirmResult: true,
installErr: fmt.Errorf("network error"),
wantErr: "network error",
wantInstalled: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ios, _, _, stderr := iostreams.Test()
if tt.isTTY {
ios.SetStdinTTY(true)
ios.SetStdoutTTY(true)
ios.SetStderrTTY(true)
}
em := &extensions.ExtensionManagerMock{
InstallFunc: func(_ ghrepo.Interface, _ string) error {
return tt.installErr
},
}
p := &prompter.PrompterMock{
ConfirmFunc: func(_ string, _ bool) (bool, error) {
return tt.confirmResult, tt.confirmErr
},
}
err := officialExtensionStubRun(ios, p, em, ext)
if tt.wantErr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
} else {
require.NoError(t, err)
}
if tt.wantStderr != "" {
assert.Contains(t, stderr.String(), tt.wantStderr)
}
if tt.wantInstalled {
require.NotEmpty(t, em.InstallCalls())
repo := em.InstallCalls()[0].InterfaceMoqParam
assert.Equal(t, "github", repo.RepoOwner())
assert.Equal(t, "gh-cool", repo.RepoName())
assert.Equal(t, "github.com", repo.RepoHost())
} else if tt.isTTY && !tt.confirmResult && tt.confirmErr == nil {
assert.Empty(t, em.InstallCalls())
}
})
}
}
func TestNewCmdOfficialExtensionStub_Properties(t *testing.T) {
ios, _, _, _ := iostreams.Test()
ext := &extensions.OfficialExtension{Name: "cool", Owner: "github", Repo: "gh-cool"}
em := &extensions.ExtensionManagerMock{}
p := &prompter.PrompterMock{}
cmd := NewCmdOfficialExtensionStub(ios, p, em, ext)
assert.Equal(t, "cool", cmd.Use)
assert.True(t, cmd.Hidden)
assert.Equal(t, "extension", cmd.GroupID)
assert.True(t, cmd.DisableFlagParsing)
}

View file

@ -45,6 +45,7 @@ import (
versionCmd "github.com/cli/cli/v2/pkg/cmd/version"
workflowCmd "github.com/cli/cli/v2/pkg/cmd/workflow"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/extensions"
"github.com/google/shlex"
"github.com/spf13/cobra"
)
@ -231,6 +232,17 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) (*cobra.Command,
}
}
// Official extension stubs: hidden commands that suggest installing
// GitHub-owned extensions when invoked. Registered after real extensions
// and aliases so that both take priority over stubs.
for i := range extensions.OfficialExtensions {
ext := &extensions.OfficialExtensions[i]
if _, _, err := cmd.Find([]string{ext.Name}); err == nil {
continue
}
cmd.AddCommand(NewCmdOfficialExtensionStub(io, f.Prompter, em, ext))
}
cmdutil.DisableAuthCheck(cmd)
// The reference command produces paged output that displays information on every other command.

View file

@ -0,0 +1,26 @@
package extensions
import (
"github.com/cli/cli/v2/internal/ghrepo"
)
// OfficialExtension describes a GitHub-owned CLI extension that can be
// suggested to users when they invoke an unknown command.
type OfficialExtension struct {
Name string
Owner string
Repo string
}
// Repository returns a ghrepo.Interface pinned to github.com so that GHES
// users install from github.com rather than their enterprise host.
func (e *OfficialExtension) Repository() ghrepo.Interface {
return ghrepo.NewWithHost(e.Owner, e.Repo, "github.com")
}
// OfficialExtensions is the registry of GitHub-owned extensions that gh will
// offer to install when the user invokes the corresponding command name.
var OfficialExtensions = []OfficialExtension{
{Name: "aw", Owner: "github", Repo: "gh-aw"},
{Name: "stack", Owner: "github", Repo: "gh-stack"},
}

View file

@ -0,0 +1,15 @@
package extensions
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestOfficialExtension_Repository(t *testing.T) {
ext := &OfficialExtension{Name: "stack", Owner: "github", Repo: "gh-stack"}
repo := ext.Repository()
assert.Equal(t, "github", repo.RepoOwner())
assert.Equal(t, "gh-stack", repo.RepoName())
assert.Equal(t, "github.com", repo.RepoHost())
}