Merge remote-tracking branch 'upstream/trunk' into attestation-refactor-policy
This commit is contained in:
commit
fa2574c1a8
7 changed files with 340 additions and 244 deletions
|
|
@ -15,32 +15,54 @@ import (
|
|||
)
|
||||
|
||||
func TestLiveSigstoreVerifier(t *testing.T) {
|
||||
t.Run("with invalid signature", func(t *testing.T) {
|
||||
attestations := getAttestationsFor(t, "../test/data/sigstoreBundle-invalid-signature.json")
|
||||
require.NotNil(t, attestations)
|
||||
type testcase struct {
|
||||
name string
|
||||
attestations []*api.Attestation
|
||||
expectErr bool
|
||||
errContains string
|
||||
}
|
||||
|
||||
testcases := []testcase{
|
||||
{
|
||||
name: "with invalid signature",
|
||||
attestations: getAttestationsFor(t, "../test/data/sigstoreBundle-invalid-signature.json"),
|
||||
expectErr: true,
|
||||
errContains: "verifying with issuer \"sigstore.dev\"",
|
||||
},
|
||||
{
|
||||
name: "with valid artifact and JSON lines file containing multiple Sigstore bundles",
|
||||
attestations: getAttestationsFor(t, "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl"),
|
||||
},
|
||||
{
|
||||
name: "with invalid bundle version",
|
||||
attestations: getAttestationsFor(t, "../test/data/sigstore-js-2.1.0-bundle-v0.1.json"),
|
||||
expectErr: true,
|
||||
errContains: "unsupported bundle version",
|
||||
},
|
||||
{
|
||||
name: "with no attestations",
|
||||
attestations: []*api.Attestation{},
|
||||
expectErr: true,
|
||||
errContains: "no attestations were verified",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
verifier := NewLiveSigstoreVerifier(SigstoreConfig{
|
||||
Logger: io.NewTestHandler(),
|
||||
})
|
||||
|
||||
res := verifier.Verify(attestations, publicGoodPolicy(t))
|
||||
require.Error(t, res.Error)
|
||||
require.ErrorContains(t, res.Error, "verifying with issuer \"sigstore.dev\"")
|
||||
require.Nil(t, res.VerifyResults)
|
||||
})
|
||||
res := verifier.Verify(tc.attestations, publicGoodPolicy(t))
|
||||
|
||||
t.Run("with valid artifact and JSON lines file containing multiple Sigstore bundles", func(t *testing.T) {
|
||||
attestations := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl")
|
||||
require.Len(t, attestations, 2)
|
||||
|
||||
verifier := NewLiveSigstoreVerifier(SigstoreConfig{
|
||||
Logger: io.NewTestHandler(),
|
||||
})
|
||||
|
||||
res := verifier.Verify(attestations, publicGoodPolicy(t))
|
||||
require.Len(t, res.VerifyResults, 2)
|
||||
require.NoError(t, res.Error)
|
||||
})
|
||||
if tc.expectErr {
|
||||
require.Error(t, res.Error, "test case: %s", tc.name)
|
||||
require.ErrorContains(t, res.Error, tc.errContains, "test case: %s", tc.name)
|
||||
require.Nil(t, res.VerifyResults, "test case: %s", tc.name)
|
||||
} else {
|
||||
require.Equal(t, len(tc.attestations), len(res.VerifyResults), "test case: %s", tc.name)
|
||||
require.NoError(t, res.Error, "test case: %s", tc.name)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("with GitHub Sigstore artifact", func(t *testing.T) {
|
||||
githubArtifactPath := test.NormalizeRelativePath("../test/data/github_provenance_demo-0.0.12-py3-none-any.whl")
|
||||
|
|
@ -72,34 +94,6 @@ func TestLiveSigstoreVerifier(t *testing.T) {
|
|||
require.Len(t, res.VerifyResults, 2)
|
||||
require.NoError(t, res.Error)
|
||||
})
|
||||
|
||||
t.Run("with invalid bundle version", func(t *testing.T) {
|
||||
attestations := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0-bundle-v0.1.json")
|
||||
require.Len(t, attestations, 1)
|
||||
|
||||
verifier := NewLiveSigstoreVerifier(SigstoreConfig{
|
||||
Logger: io.NewTestHandler(),
|
||||
})
|
||||
|
||||
res := verifier.Verify(attestations, publicGoodPolicy(t))
|
||||
require.Len(t, res.VerifyResults, 0)
|
||||
require.ErrorContains(t, res.Error, "unsupported bundle version")
|
||||
})
|
||||
|
||||
t.Run("with no attestations", func(t *testing.T) {
|
||||
attestations := []*api.Attestation{}
|
||||
require.Len(t, attestations, 0)
|
||||
|
||||
verifier := NewLiveSigstoreVerifier(SigstoreConfig{
|
||||
Logger: io.NewTestHandler(),
|
||||
TrustedRoot: test.NormalizeRelativePath("../test/data/trusted_root.json"),
|
||||
})
|
||||
|
||||
res := verifier.Verify(attestations, publicGoodPolicy(t))
|
||||
require.Len(t, res.VerifyResults, 0)
|
||||
require.NotNil(t, res.Error)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func publicGoodPolicy(t *testing.T) verify.PolicyBuilder {
|
||||
|
|
|
|||
|
|
@ -13,29 +13,28 @@ var (
|
|||
publicGoodBundlePath = test.NormalizeRelativePath("../test/data/psigstore-js-2.1.0-bundle.json")
|
||||
)
|
||||
|
||||
var baseOptions = Options{
|
||||
ArtifactPath: publicGoodArtifactPath,
|
||||
BundlePath: publicGoodBundlePath,
|
||||
DigestAlgorithm: "sha512",
|
||||
Limit: 1,
|
||||
Owner: "sigstore",
|
||||
OIDCIssuer: "some issuer",
|
||||
}
|
||||
|
||||
func TestAreFlagsValid(t *testing.T) {
|
||||
t.Run("has invalid Repo value", func(t *testing.T) {
|
||||
opts := Options{
|
||||
ArtifactPath: publicGoodArtifactPath,
|
||||
DigestAlgorithm: "sha512",
|
||||
OIDCIssuer: "some issuer",
|
||||
Repo: "sigstoresigstore-js",
|
||||
}
|
||||
opts := baseOptions
|
||||
opts.Repo = "sigstoresigstore-js"
|
||||
|
||||
err := opts.AreFlagsValid()
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "invalid value provided for repo")
|
||||
})
|
||||
|
||||
t.Run("invalid limit < 0", func(t *testing.T) {
|
||||
opts := Options{
|
||||
ArtifactPath: publicGoodArtifactPath,
|
||||
BundlePath: publicGoodBundlePath,
|
||||
DigestAlgorithm: "sha512",
|
||||
Owner: "sigstore",
|
||||
OIDCIssuer: "some issuer",
|
||||
Limit: 0,
|
||||
}
|
||||
t.Run("invalid limit == 0", func(t *testing.T) {
|
||||
opts := baseOptions
|
||||
opts.Limit = 0
|
||||
|
||||
err := opts.AreFlagsValid()
|
||||
require.Error(t, err)
|
||||
|
|
@ -43,19 +42,43 @@ func TestAreFlagsValid(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("invalid limit > 1000", func(t *testing.T) {
|
||||
opts := Options{
|
||||
ArtifactPath: publicGoodArtifactPath,
|
||||
BundlePath: publicGoodBundlePath,
|
||||
DigestAlgorithm: "sha512",
|
||||
Owner: "sigstore",
|
||||
OIDCIssuer: "some issuer",
|
||||
Limit: 1001,
|
||||
}
|
||||
opts := baseOptions
|
||||
opts.Limit = 1001
|
||||
|
||||
err := opts.AreFlagsValid()
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "limit 1001 not allowed, must be between 1 and 1000")
|
||||
})
|
||||
|
||||
t.Run("returns error when UseBundleFromRegistry is true and ArtifactPath is not an OCI path", func(t *testing.T) {
|
||||
opts := baseOptions
|
||||
opts.BundlePath = ""
|
||||
opts.UseBundleFromRegistry = true
|
||||
|
||||
err := opts.AreFlagsValid()
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "bundle-from-oci flag can only be used with OCI artifact paths")
|
||||
})
|
||||
|
||||
t.Run("does not return error when UseBundleFromRegistry is true and ArtifactPath is an OCI path", func(t *testing.T) {
|
||||
opts := baseOptions
|
||||
opts.ArtifactPath = "oci://sigstore/sigstore-js:2.1.0"
|
||||
opts.BundlePath = ""
|
||||
opts.UseBundleFromRegistry = true
|
||||
|
||||
err := opts.AreFlagsValid()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("returns error when UseBundleFromRegistry is true and BundlePath is provided", func(t *testing.T) {
|
||||
opts := baseOptions
|
||||
opts.ArtifactPath = "oci://sigstore/sigstore-js:2.1.0"
|
||||
opts.UseBundleFromRegistry = true
|
||||
|
||||
err := opts.AreFlagsValid()
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "bundle-from-oci flag cannot be used with bundle-path flag")
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetPolicyFlags(t *testing.T) {
|
||||
|
|
@ -116,47 +139,4 @@ func TestSetPolicyFlags(t *testing.T) {
|
|||
require.Equal(t, "sigstore", opts.Owner)
|
||||
require.Equal(t, "^https://github/foo", opts.SANRegex)
|
||||
})
|
||||
|
||||
t.Run("returns error when UseBundleFromRegistry is true and ArtifactPath is not an OCI path", func(t *testing.T) {
|
||||
opts := Options{
|
||||
ArtifactPath: publicGoodArtifactPath,
|
||||
DigestAlgorithm: "sha512",
|
||||
Owner: "sigstore",
|
||||
UseBundleFromRegistry: true,
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
err := opts.AreFlagsValid()
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "bundle-from-oci flag can only be used with OCI artifact paths")
|
||||
})
|
||||
|
||||
t.Run("does not return error when UseBundleFromRegistry is true and ArtifactPath is an OCI path", func(t *testing.T) {
|
||||
opts := Options{
|
||||
ArtifactPath: "oci://sigstore/sigstore-js:2.1.0",
|
||||
DigestAlgorithm: "sha512",
|
||||
OIDCIssuer: "some issuer",
|
||||
Owner: "sigstore",
|
||||
UseBundleFromRegistry: true,
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
err := opts.AreFlagsValid()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("returns error when UseBundleFromRegistry is true and BundlePath is provided", func(t *testing.T) {
|
||||
opts := Options{
|
||||
ArtifactPath: "oci://sigstore/sigstore-js:2.1.0",
|
||||
BundlePath: publicGoodBundlePath,
|
||||
DigestAlgorithm: "sha512",
|
||||
Owner: "sigstore",
|
||||
UseBundleFromRegistry: true,
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
err := opts.AreFlagsValid()
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "bundle-from-oci flag cannot be used with bundle-path flag")
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,18 +36,28 @@ func TestValidateSignerWorkflow(t *testing.T) {
|
|||
providedSignerWorkflow string
|
||||
expectedWorkflowRegex string
|
||||
host string
|
||||
expectErr bool
|
||||
errContains string
|
||||
}
|
||||
|
||||
testcases := []testcase{
|
||||
{
|
||||
name: "workflow with no host specified",
|
||||
providedSignerWorkflow: "github/artifact-attestations-workflows/.github/workflows/attest.yml",
|
||||
expectedWorkflowRegex: "^https://github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml",
|
||||
expectErr: true,
|
||||
errContains: "unknown host",
|
||||
},
|
||||
{
|
||||
name: "workflow with host specified",
|
||||
name: "workflow with default host",
|
||||
providedSignerWorkflow: "github/artifact-attestations-workflows/.github/workflows/attest.yml",
|
||||
expectedWorkflowRegex: "^https://github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml",
|
||||
host: "github.com",
|
||||
},
|
||||
{
|
||||
name: "workflow with workflow URL included",
|
||||
providedSignerWorkflow: "github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml",
|
||||
expectedWorkflowRegex: "^https://github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml",
|
||||
host: "github.com",
|
||||
},
|
||||
{
|
||||
name: "workflow with GH_HOST set",
|
||||
|
|
@ -61,44 +71,25 @@ func TestValidateSignerWorkflow(t *testing.T) {
|
|||
expectedWorkflowRegex: "^https://authedhost.github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml",
|
||||
host: "authedhost.github.com",
|
||||
},
|
||||
{
|
||||
name: "workflow with authenticated host",
|
||||
providedSignerWorkflow: "github/artifact-attestations-workflows/.github/workflows/attest.yml",
|
||||
expectedWorkflowRegex: "^https://authedhost.github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml",
|
||||
host: "authedhost.github.com",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
cmdFactory := factory.New("test")
|
||||
|
||||
opts := &Options{
|
||||
Config: cmdFactory.Config,
|
||||
Config: factory.New("test").Config,
|
||||
SignerWorkflow: tc.providedSignerWorkflow,
|
||||
}
|
||||
|
||||
// All host resolution is done verify.go:RunE
|
||||
if tc.host == "" {
|
||||
// Set to default host
|
||||
tc.host = "github.com"
|
||||
}
|
||||
opts.Hostname = tc.host
|
||||
|
||||
workflowRegex, err := validateSignerWorkflow(opts)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expectedWorkflowRegex, workflowRegex)
|
||||
|
||||
if tc.expectErr {
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, tc.errContains)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expectedWorkflowRegex, workflowRegex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSignerWorkflowNoHost(t *testing.T) {
|
||||
cmdFactory := factory.New("test")
|
||||
opts := &Options{
|
||||
Config: cmdFactory.Config,
|
||||
SignerWorkflow: "github/artifact-attestations-workflows/.github/workflows/attest.yml",
|
||||
}
|
||||
|
||||
workflowRegex, err := validateSignerWorkflow(opts)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "unknown host")
|
||||
require.Equal(t, "", workflowRegex)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,6 +83,33 @@ func TestVerifyIntegration(t *testing.T) {
|
|||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "expected SourceRepositoryURI to be https://github.com/fakeowner/fakerepo, got https://github.com/sigstore/sigstore-js")
|
||||
})
|
||||
|
||||
t.Run("with no matching OIDC issuer", func(t *testing.T) {
|
||||
opts := publicGoodOpts
|
||||
opts.OIDCIssuer = "some-other-issuer"
|
||||
|
||||
err := runVerify(&opts)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "expected Issuer to be some-other-issuer, got https://token.actions.githubusercontent.com")
|
||||
})
|
||||
|
||||
t.Run("with invalid SAN", func(t *testing.T) {
|
||||
opts := publicGoodOpts
|
||||
opts.SAN = "fake san"
|
||||
|
||||
err := runVerify(&opts)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "verifying with issuer \"sigstore.dev\"")
|
||||
})
|
||||
|
||||
t.Run("with invalid SAN regex", func(t *testing.T) {
|
||||
opts := publicGoodOpts
|
||||
opts.SANRegex = "^https://github.com/sigstore/not-real/"
|
||||
|
||||
err := runVerify(&opts)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "verifying with issuer \"sigstore.dev\"")
|
||||
})
|
||||
}
|
||||
|
||||
func TestVerifyIntegrationCustomIssuer(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -453,75 +453,6 @@ func TestRunVerify(t *testing.T) {
|
|||
require.ErrorContains(t, err, "failed to fetch attestations from wrong-owner")
|
||||
})
|
||||
|
||||
// TODO: this test can only be tested with a live SigstoreVerifier
|
||||
// add integration tests or HTTP mocked sigstore verifier tests
|
||||
// to test this case
|
||||
t.Run("with invalid OIDC issuer", func(t *testing.T) {
|
||||
t.Skip()
|
||||
opts := publicGoodOpts
|
||||
opts.OIDCIssuer = "not-a-real-issuer"
|
||||
require.Error(t, runVerify(&opts))
|
||||
})
|
||||
|
||||
// TODO: this test can only be tested with a live SigstoreVerifier
|
||||
// add integration tests or HTTP mocked sigstore verifier tests
|
||||
// to test this case
|
||||
t.Run("with SAN enforcement", func(t *testing.T) {
|
||||
t.Skip()
|
||||
opts := Options{
|
||||
ArtifactPath: artifactPath,
|
||||
BundlePath: bundlePath,
|
||||
APIClient: api.NewTestClient(),
|
||||
DigestAlgorithm: "sha512",
|
||||
Logger: logger,
|
||||
OIDCIssuer: verification.GitHubOIDCIssuer,
|
||||
Owner: "sigstore",
|
||||
SAN: SigstoreSanValue,
|
||||
SigstoreVerifier: verification.NewMockSigstoreVerifier(t),
|
||||
}
|
||||
require.Nil(t, runVerify(&opts))
|
||||
})
|
||||
|
||||
// TODO: this test can only be tested with a live SigstoreVerifier
|
||||
// add integration tests or HTTP mocked sigstore verifier tests
|
||||
// to test this case
|
||||
t.Run("with invalid SAN", func(t *testing.T) {
|
||||
t.Skip()
|
||||
opts := publicGoodOpts
|
||||
opts.SAN = "fake san"
|
||||
require.Error(t, runVerify(&opts))
|
||||
})
|
||||
|
||||
// TODO: this test can only be tested with a live SigstoreVerifier
|
||||
// add integration tests or HTTP mocked sigstore verifier tests
|
||||
// to test this case
|
||||
t.Run("with SAN regex enforcement", func(t *testing.T) {
|
||||
t.Skip()
|
||||
opts := publicGoodOpts
|
||||
opts.SANRegex = SigstoreSanRegex
|
||||
require.Nil(t, runVerify(&opts))
|
||||
})
|
||||
|
||||
// TODO: this test can only be tested with a live SigstoreVerifier
|
||||
// add integration tests or HTTP mocked sigstore verifier tests
|
||||
// to test this case
|
||||
t.Run("with invalid SAN regex", func(t *testing.T) {
|
||||
t.Skip()
|
||||
opts := publicGoodOpts
|
||||
opts.SANRegex = "^https://github.com/sigstore/not-real/"
|
||||
require.Error(t, runVerify(&opts))
|
||||
})
|
||||
|
||||
// TODO: this test can only be tested with a live SigstoreVerifier
|
||||
// add integration tests or HTTP mocked sigstore verifier tests
|
||||
// to test this case
|
||||
t.Run("with no matching OIDC issuer", func(t *testing.T) {
|
||||
t.Skip()
|
||||
opts := publicGoodOpts
|
||||
opts.OIDCIssuer = "some-other-issuer"
|
||||
require.Error(t, runVerify(&opts))
|
||||
})
|
||||
|
||||
t.Run("with missing API client", func(t *testing.T) {
|
||||
customOpts := publicGoodOpts
|
||||
customOpts.APIClient = nil
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import (
|
|||
fd "github.com/cli/cli/v2/internal/featuredetection"
|
||||
"github.com/cli/cli/v2/internal/ghinstance"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/text"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/cli/v2/pkg/set"
|
||||
|
|
@ -49,15 +50,16 @@ const (
|
|||
)
|
||||
|
||||
type EditOptions struct {
|
||||
HTTPClient *http.Client
|
||||
Repository ghrepo.Interface
|
||||
IO *iostreams.IOStreams
|
||||
Edits EditRepositoryInput
|
||||
AddTopics []string
|
||||
RemoveTopics []string
|
||||
InteractiveMode bool
|
||||
Detector fd.Detector
|
||||
Prompter iprompter
|
||||
HTTPClient *http.Client
|
||||
Repository ghrepo.Interface
|
||||
IO *iostreams.IOStreams
|
||||
Edits EditRepositoryInput
|
||||
AddTopics []string
|
||||
RemoveTopics []string
|
||||
AcceptVisibilityChangeConsequences bool
|
||||
InteractiveMode bool
|
||||
Detector fd.Detector
|
||||
Prompter iprompter
|
||||
// Cache of current repo topics to avoid retrieving them
|
||||
// in multiple flows.
|
||||
topicsCache []string
|
||||
|
|
@ -103,7 +105,16 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobr
|
|||
|
||||
To toggle a setting off, use the %[1]s--<flag>=false%[1]s syntax.
|
||||
|
||||
Note that changing repository visibility to private will cause loss of stars and watchers.
|
||||
Changing repository visibility can have unexpected consequences including but not limited to:
|
||||
|
||||
- Losing stars and watchers, affecting repository ranking
|
||||
- Detaching public forks from the network
|
||||
- Disabling push rulesets
|
||||
- Allowing access to GitHub Actions history and logs
|
||||
|
||||
When the %[1]s--visibility%[1]s flag is used, %[1]s--accept-visibility-change-consequences%[1]s flag is required.
|
||||
|
||||
For information on all the potential consequences, see <https://gh.io/setting-repository-visibility>
|
||||
`, "`"),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Example: heredoc.Doc(`
|
||||
|
|
@ -142,6 +153,10 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobr
|
|||
return cmdutil.FlagErrorf("specify properties to edit when not running interactively")
|
||||
}
|
||||
|
||||
if opts.Edits.Visibility != nil && !opts.AcceptVisibilityChangeConsequences {
|
||||
return cmdutil.FlagErrorf("use of --visibility flag requires --accept-visibility-change-consequences flag")
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
|
|
@ -167,6 +182,7 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobr
|
|||
cmdutil.NilBoolFlag(cmd, &opts.Edits.AllowUpdateBranch, "allow-update-branch", "", "Allow a pull request head branch that is behind its base branch to be updated")
|
||||
cmd.Flags().StringSliceVar(&opts.AddTopics, "add-topic", nil, "Add repository topic")
|
||||
cmd.Flags().StringSliceVar(&opts.RemoveTopics, "remove-topic", nil, "Remove repository topic")
|
||||
cmd.Flags().BoolVar(&opts.AcceptVisibilityChangeConsequences, "accept-visibility-change-consequences", false, "Accept the consequences of changing the repository visibility")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -379,23 +395,26 @@ func interactiveRepoEdit(opts *EditOptions, r *api.Repository) error {
|
|||
}
|
||||
opts.Edits.EnableProjects = &a
|
||||
case optionVisibility:
|
||||
cs := opts.IO.ColorScheme()
|
||||
fmt.Fprintf(opts.IO.ErrOut, "%s Danger zone: changing repository visibility can have unexpected consequences; consult https://gh.io/setting-repository-visibility before continuing.\n", cs.WarningIcon())
|
||||
|
||||
visibilityOptions := []string{"public", "private", "internal"}
|
||||
selected, err := p.Select("Visibility", strings.ToLower(r.Visibility), visibilityOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
confirmed := true
|
||||
if visibilityOptions[selected] == "private" &&
|
||||
(r.StargazerCount > 0 || r.Watchers.TotalCount > 0) {
|
||||
cs := opts.IO.ColorScheme()
|
||||
fmt.Fprintf(opts.IO.ErrOut, "%s Changing the repository visibility to private will cause permanent loss of stars and watchers.\n", cs.WarningIcon())
|
||||
confirmed, err = p.Confirm("Do you want to change visibility to private?", false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
selectedVisibility := visibilityOptions[selected]
|
||||
|
||||
if selectedVisibility != r.Visibility && (r.StargazerCount > 0 || r.Watchers.TotalCount > 0) {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "%s Changing the repository visibility to %s will cause permanent loss of %s and %s.\n", cs.WarningIcon(), selectedVisibility, text.Pluralize(r.StargazerCount, "star"), text.Pluralize(r.Watchers.TotalCount, "watcher"))
|
||||
}
|
||||
|
||||
confirmed, err := p.Confirm(fmt.Sprintf("Do you want to change visibility to %s?", selectedVisibility), false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if confirmed {
|
||||
opts.Edits.Visibility = &visibilityOptions[selected]
|
||||
opts.Edits.Visibility = &selectedVisibility
|
||||
}
|
||||
case optionMergeOptions:
|
||||
var defaultMergeOptions []string
|
||||
|
|
|
|||
|
|
@ -34,6 +34,63 @@ func TestNewCmdEdit(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "deny public visibility change without accepting consequences",
|
||||
args: "--visibility public",
|
||||
wantOpts: EditOptions{
|
||||
Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
|
||||
Edits: EditRepositoryInput{},
|
||||
},
|
||||
wantErr: "use of --visibility flag requires --accept-visibility-change-consequences flag",
|
||||
},
|
||||
{
|
||||
name: "allow public visibility change with accepting consequences",
|
||||
args: "--visibility public --accept-visibility-change-consequences",
|
||||
wantOpts: EditOptions{
|
||||
Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
|
||||
Edits: EditRepositoryInput{
|
||||
Visibility: sp("public"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "deny private visibility change without accepting consequences",
|
||||
args: "--visibility private",
|
||||
wantOpts: EditOptions{
|
||||
Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
|
||||
Edits: EditRepositoryInput{},
|
||||
},
|
||||
wantErr: "use of --visibility flag requires --accept-visibility-change-consequences flag",
|
||||
},
|
||||
{
|
||||
name: "allow private visibility change with accepting consequences",
|
||||
args: "--visibility private --accept-visibility-change-consequences",
|
||||
wantOpts: EditOptions{
|
||||
Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
|
||||
Edits: EditRepositoryInput{
|
||||
Visibility: sp("private"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "deny internal visibility change without accepting consequences",
|
||||
args: "--visibility internal",
|
||||
wantOpts: EditOptions{
|
||||
Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
|
||||
Edits: EditRepositoryInput{},
|
||||
},
|
||||
wantErr: "use of --visibility flag requires --accept-visibility-change-consequences flag",
|
||||
},
|
||||
{
|
||||
name: "allow internal visibility change with accepting consequences",
|
||||
args: "--visibility internal --accept-visibility-change-consequences",
|
||||
wantOpts: EditOptions{
|
||||
Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
|
||||
Edits: EditRepositoryInput{
|
||||
Visibility: sp("internal"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
@ -241,6 +298,109 @@ func Test_editRun_interactive(t *testing.T) {
|
|||
}))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "skipping visibility without confirmation",
|
||||
opts: EditOptions{
|
||||
Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
|
||||
InteractiveMode: true,
|
||||
},
|
||||
promptStubs: func(pm *prompter.MockPrompter) {
|
||||
pm.RegisterMultiSelect("What do you want to edit?", nil, editList,
|
||||
func(_ string, _, opts []string) ([]int, error) {
|
||||
return []int{8}, nil
|
||||
})
|
||||
pm.RegisterSelect("Visibility", []string{"public", "private", "internal"},
|
||||
func(_, _ string, opts []string) (int, error) {
|
||||
return prompter.IndexFor(opts, "private")
|
||||
})
|
||||
pm.RegisterConfirm("Do you want to change visibility to private?", func(_ string, _ bool) (bool, error) {
|
||||
return false, nil
|
||||
})
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query RepositoryInfo\b`),
|
||||
httpmock.StringResponse(`
|
||||
{
|
||||
"data": {
|
||||
"repository": {
|
||||
"visibility": "public",
|
||||
"description": "description",
|
||||
"homePageUrl": "https://url.com",
|
||||
"defaultBranchRef": {
|
||||
"name": "main"
|
||||
},
|
||||
"stargazerCount": 10,
|
||||
"isInOrganization": false,
|
||||
"repositoryTopics": {
|
||||
"nodes": [{
|
||||
"topic": {
|
||||
"name": "x"
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}`))
|
||||
reg.Exclude(t, httpmock.REST("PATCH", "repos/OWNER/REPO"))
|
||||
},
|
||||
wantsStderr: "Changing the repository visibility to private will cause permanent loss of 10 stars and 0 watchers.",
|
||||
},
|
||||
{
|
||||
name: "changing visibility with confirmation",
|
||||
opts: EditOptions{
|
||||
Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
|
||||
InteractiveMode: true,
|
||||
},
|
||||
promptStubs: func(pm *prompter.MockPrompter) {
|
||||
pm.RegisterMultiSelect("What do you want to edit?", nil, editList,
|
||||
func(_ string, _, opts []string) ([]int, error) {
|
||||
return []int{8}, nil
|
||||
})
|
||||
pm.RegisterSelect("Visibility", []string{"public", "private", "internal"},
|
||||
func(_, _ string, opts []string) (int, error) {
|
||||
return prompter.IndexFor(opts, "private")
|
||||
})
|
||||
pm.RegisterConfirm("Do you want to change visibility to private?", func(_ string, _ bool) (bool, error) {
|
||||
return true, nil
|
||||
})
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query RepositoryInfo\b`),
|
||||
httpmock.StringResponse(`
|
||||
{
|
||||
"data": {
|
||||
"repository": {
|
||||
"visibility": "public",
|
||||
"description": "description",
|
||||
"homePageUrl": "https://url.com",
|
||||
"defaultBranchRef": {
|
||||
"name": "main"
|
||||
},
|
||||
"stargazerCount": 10,
|
||||
"watchers": {
|
||||
"totalCount": 15
|
||||
},
|
||||
"isInOrganization": false,
|
||||
"repositoryTopics": {
|
||||
"nodes": [{
|
||||
"topic": {
|
||||
"name": "x"
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}`))
|
||||
reg.Register(
|
||||
httpmock.REST("PATCH", "repos/OWNER/REPO"),
|
||||
httpmock.RESTPayload(200, `{}`, func(payload map[string]interface{}) {
|
||||
assert.Equal(t, "private", payload["visibility"])
|
||||
}))
|
||||
},
|
||||
wantsStderr: "Changing the repository visibility to private will cause permanent loss of 10 stars and 15 watchers",
|
||||
},
|
||||
{
|
||||
name: "the rest",
|
||||
opts: EditOptions{
|
||||
|
|
@ -250,7 +410,7 @@ func Test_editRun_interactive(t *testing.T) {
|
|||
promptStubs: func(pm *prompter.MockPrompter) {
|
||||
pm.RegisterMultiSelect("What do you want to edit?", nil, editList,
|
||||
func(_ string, _, opts []string) ([]int, error) {
|
||||
return []int{0, 2, 3, 5, 6, 8, 9}, nil
|
||||
return []int{0, 2, 3, 5, 6, 9}, nil
|
||||
})
|
||||
pm.RegisterInput("Default branch name", func(_, _ string) (string, error) {
|
||||
return "trunk", nil
|
||||
|
|
@ -267,13 +427,6 @@ func Test_editRun_interactive(t *testing.T) {
|
|||
pm.RegisterConfirm("Convert into a template repository?", func(_ string, _ bool) (bool, error) {
|
||||
return true, nil
|
||||
})
|
||||
pm.RegisterSelect("Visibility", []string{"public", "private", "internal"},
|
||||
func(_, _ string, opts []string) (int, error) {
|
||||
return prompter.IndexFor(opts, "private")
|
||||
})
|
||||
pm.RegisterConfirm("Do you want to change visibility to private?", func(_ string, _ bool) (bool, error) {
|
||||
return true, nil
|
||||
})
|
||||
pm.RegisterConfirm("Enable Wikis?", func(_ string, _ bool) (bool, error) {
|
||||
return true, nil
|
||||
})
|
||||
|
|
@ -310,7 +463,6 @@ func Test_editRun_interactive(t *testing.T) {
|
|||
assert.Equal(t, "https://zombo.com", payload["homepage"])
|
||||
assert.Equal(t, true, payload["has_issues"])
|
||||
assert.Equal(t, true, payload["has_projects"])
|
||||
assert.Equal(t, "private", payload["visibility"])
|
||||
assert.Equal(t, true, payload["is_template"])
|
||||
assert.Equal(t, true, payload["has_wiki"])
|
||||
}))
|
||||
|
|
@ -484,7 +636,7 @@ func Test_editRun_interactive(t *testing.T) {
|
|||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
ios, _, _, stderr := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
ios.SetStdinTTY(true)
|
||||
ios.SetStderrTTY(true)
|
||||
|
|
@ -509,9 +661,11 @@ func Test_editRun_interactive(t *testing.T) {
|
|||
if tt.wantsErr == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
assert.EqualError(t, err, tt.wantsErr)
|
||||
require.EqualError(t, err, tt.wantsErr)
|
||||
return
|
||||
}
|
||||
|
||||
assert.Contains(t, stderr.String(), tt.wantsStderr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue