Merge branch 'trunk' into sammorrowdrums/skills-replace-git-in-publish-tests
This commit is contained in:
commit
bce04e3245
8 changed files with 258 additions and 10 deletions
21
.github/CODEOWNERS
vendored
21
.github/CODEOWNERS
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
72
pkg/cmd/root/official_extension_stub.go
Normal file
72
pkg/cmd/root/official_extension_stub.go
Normal 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
|
||||
}
|
||||
119
pkg/cmd/root/official_extension_stub_test.go
Normal file
119
pkg/cmd/root/official_extension_stub_test.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
26
pkg/extensions/official.go
Normal file
26
pkg/extensions/official.go
Normal 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"},
|
||||
}
|
||||
15
pkg/extensions/official_test.go
Normal file
15
pkg/extensions/official_test.go
Normal 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())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue