Merge branch 'trunk' into 8386-update-release-doc-with-homebrew-info

This commit is contained in:
Azeem Sajid 2025-01-31 13:22:28 +05:00 committed by GitHub
commit 0f16c870f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 483 additions and 29 deletions

View file

@ -33,7 +33,7 @@ jobs:
steps:
- name: Validate tag name format
run: |
if [[ ! "${{ inputs.tag_name }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$]]; then
if [[ ! "${{ inputs.tag_name }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Invalid tag name format. Must be in the form v1.2.3"
exit 1
fi

2
go.mod
View file

@ -29,7 +29,7 @@ require (
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-version v1.3.0
github.com/henvic/httpretty v0.1.4
github.com/in-toto/attestation v1.1.0
github.com/in-toto/attestation v1.1.1
github.com/joho/godotenv v1.5.1
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/mattn/go-colorable v0.1.14

4
go.sum
View file

@ -266,8 +266,8 @@ github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef h1:A9HsByNhogrvm9cWb28sjiS3i7tcKCkflWFEkHfuAgM=
github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
github.com/in-toto/attestation v1.1.0 h1:oRWzfmZPDSctChD0VaQV7MJrywKOzyNrtpENQFq//2Q=
github.com/in-toto/attestation v1.1.0/go.mod h1:DB59ytd3z7cIHgXxwpSX2SABrU6WJUKg/grpdgHVgVs=
github.com/in-toto/attestation v1.1.1 h1:QD3d+oATQ0dFsWoNh5oT0udQ3tUrOsZZ0Fc3tSgWbzI=
github.com/in-toto/attestation v1.1.1/go.mod h1:Dcq1zVwA2V7Qin8I7rgOi+i837wEf/mOZwRm047Sjys=
github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU=
github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3lUTQd+eF9HdeMo=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=

View file

@ -176,23 +176,25 @@ func (c *LiveClient) fetchBundleFromAttestations(attestations []*Attestation) ([
return fmt.Errorf("attestation has no bundle or bundle URL")
}
// for now, we fallback to the bundle field if the bundle URL is empty
if a.BundleURL == "" {
c.logger.VerbosePrintf("Bundle URL is empty. Falling back to bundle field\n\n")
// If the bundle field is nil, try to fetch the bundle with the provided URL
if a.Bundle == nil {
c.logger.VerbosePrintf("Bundle field is empty. Trying to fetch with bundle URL\n\n")
b, err := c.GetBundle(a.BundleURL)
if err != nil {
return fmt.Errorf("failed to fetch bundle with URL: %w", err)
}
fetched[i] = &Attestation{
Bundle: a.Bundle,
Bundle: b,
}
return nil
}
// otherwise fetch the bundle with the provided URL
b, err := c.GetBundle(a.BundleURL)
if err != nil {
return fmt.Errorf("failed to fetch bundle with URL: %w", err)
}
// otherwise fall back to the bundle field
c.logger.VerbosePrintf("Fetching bundle from Bundle field\n\n")
fetched[i] = &Attestation{
Bundle: b,
Bundle: a.Bundle,
}
return nil
})
}

View file

@ -180,7 +180,7 @@ func TestGetByDigest_Error(t *testing.T) {
require.Nil(t, attestations)
}
func TestFetchBundleFromAttestations(t *testing.T) {
func TestFetchBundleFromAttestations_BundleURL(t *testing.T) {
httpClient := &mockHttpClient{}
client := LiveClient{
httpClient: httpClient,
@ -188,12 +188,15 @@ func TestFetchBundleFromAttestations(t *testing.T) {
}
att1 := makeTestAttestation()
att1.Bundle = nil
att2 := makeTestAttestation()
att2.Bundle = nil
// zero out the bundle field so it tries fetching by URL
attestations := []*Attestation{&att1, &att2}
fetched, err := client.fetchBundleFromAttestations(attestations)
require.NoError(t, err)
require.Len(t, fetched, 2)
require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", fetched[0].Bundle.GetMediaType())
require.NotNil(t, "application/vnd.dev.sigstore.bundle.v0.3+json", fetched[0].Bundle.GetMediaType())
httpClient.AssertNumberOfCalls(t, "OnGetSuccess", 2)
}
@ -211,7 +214,7 @@ func TestFetchBundleFromAttestations_InvalidAttestation(t *testing.T) {
require.Nil(t, fetched, 2)
}
func TestFetchBundleFromAttestations_Fail(t *testing.T) {
func TestFetchBundleFromAttestations_Fail_BundleURL(t *testing.T) {
httpClient := &failAfterOneCallHttpClient{}
c := &LiveClient{
@ -220,7 +223,10 @@ func TestFetchBundleFromAttestations_Fail(t *testing.T) {
}
att1 := makeTestAttestation()
att1.Bundle = nil
att2 := makeTestAttestation()
att2.Bundle = nil
// zero out the bundle field so it tries fetching by URL
attestations := []*Attestation{&att1, &att2}
fetched, err := c.fetchBundleFromAttestations(attestations)
require.Error(t, err)
@ -237,6 +243,7 @@ func TestFetchBundleFromAttestations_FetchByURLFail(t *testing.T) {
}
a := makeTestAttestation()
a.Bundle = nil
attestations := []*Attestation{&a}
bundle, err := c.fetchBundleFromAttestations(attestations)
require.Error(t, err)

View file

@ -4,6 +4,7 @@ import (
"github.com/MakeNowJust/heredoc"
cmdCreate "github.com/cli/cli/v2/pkg/cmd/repo/autolink/create"
cmdList "github.com/cli/cli/v2/pkg/cmd/repo/autolink/list"
cmdView "github.com/cli/cli/v2/pkg/cmd/repo/autolink/view"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/spf13/cobra"
)
@ -24,6 +25,7 @@ func NewCmdAutolink(f *cmdutil.Factory) *cobra.Command {
cmd.AddCommand(cmdList.NewCmdList(f, nil))
cmd.AddCommand(cmdCreate.NewCmdCreate(f, nil))
cmd.AddCommand(cmdView.NewCmdView(f, nil))
return cmd
}

View file

@ -109,9 +109,9 @@ func createRun(opts *createOptions) error {
cs := opts.IO.ColorScheme()
fmt.Fprintf(opts.IO.Out,
"%s Created repository autolink %d on %s\n",
"%s Created repository autolink %s on %s\n",
cs.SuccessIconWithColor(cs.Green),
autolink.ID,
cs.Cyanf("%d", autolink.ID),
ghrepo.FullName(repo))
return nil

View file

@ -12,7 +12,7 @@ import (
"github.com/stretchr/testify/require"
)
func TestAutoLinkLister_List(t *testing.T) {
func TestAutolinkLister_List(t *testing.T) {
tests := []struct {
name string
repo ghrepo.Interface

View file

@ -15,13 +15,6 @@ import (
"github.com/spf13/cobra"
)
var autolinkFields = []string{
"id",
"isAlphanumeric",
"keyPrefix",
"urlTemplate",
}
type listOptions struct {
BaseRepo func() (ghrepo.Interface, error)
Browser browser.Browser
@ -70,7 +63,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*listOptions) error) *cobra.Comman
}
cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "List autolink references in the web browser")
cmdutil.AddJSONFlags(cmd, &opts.Exporter, autolinkFields)
cmdutil.AddJSONFlags(cmd, &opts.Exporter, shared.AutolinkFields)
return cmd
}
@ -111,8 +104,10 @@ func listRun(opts *listOptions) error {
tp := tableprinter.New(opts.IO, tableprinter.WithHeader("ID", "KEY PREFIX", "URL TEMPLATE", "ALPHANUMERIC"))
cs := opts.IO.ColorScheme()
for _, autolink := range autolinks {
tp.AddField(fmt.Sprintf("%d", autolink.ID))
tp.AddField(cs.Cyanf("%d", autolink.ID))
tp.AddField(autolink.KeyPrefix)
tp.AddField(autolink.URLTemplate)
tp.AddField(strconv.FormatBool(autolink.IsAlphanumeric))

View file

@ -9,6 +9,13 @@ type Autolink struct {
URLTemplate string `json:"url_template"`
}
var AutolinkFields = []string{
"id",
"isAlphanumeric",
"keyPrefix",
"urlTemplate",
}
func (a *Autolink) ExportData(fields []string) map[string]interface{} {
return cmdutil.StructExportData(a, fields)
}

View file

@ -0,0 +1,46 @@
package view
import (
"encoding/json"
"fmt"
"net/http"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmd/repo/autolink/shared"
)
type AutolinkViewer struct {
HTTPClient *http.Client
}
func (a *AutolinkViewer) View(repo ghrepo.Interface, id string) (*shared.Autolink, error) {
path := fmt.Sprintf("repos/%s/%s/autolinks/%s", repo.RepoOwner(), repo.RepoName(), id)
url := ghinstance.RESTPrefix(repo.RepoHost()) + path
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := a.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return nil, fmt.Errorf("HTTP 404: Either no autolink with this ID exists for this repository or you are missing admin rights to the repository. (https://api.github.com/%s)", path)
} else if resp.StatusCode > 299 {
return nil, api.HandleHTTPError(resp)
}
var autolink shared.Autolink
err = json.NewDecoder(resp.Body).Decode(&autolink)
if err != nil {
return nil, err
}
return &autolink, nil
}

View file

@ -0,0 +1,102 @@
package view
import (
"fmt"
"net/http"
"testing"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmd/repo/autolink/shared"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAutolinkViewer_View(t *testing.T) {
repo := ghrepo.New("OWNER", "REPO")
tests := []struct {
name string
id string
stubStatus int
stubRespJSON string
expectedAutolink *shared.Autolink
expectErr bool
expectedErrMsg string
}{
{
name: "200 successful alphanumeric view",
id: "123",
stubStatus: 200,
stubRespJSON: `{
"id": 123,
"key_prefix": "TICKET-",
"url_template": "https://example.com/TICKET?query=<num>",
"is_alphanumeric": true
}`,
expectedAutolink: &shared.Autolink{
ID: 123,
KeyPrefix: "TICKET-",
URLTemplate: "https://example.com/TICKET?query=<num>",
IsAlphanumeric: true,
},
},
{
name: "200 successful numeric view",
id: "123",
stubStatus: 200,
stubRespJSON: `{
"id": 123,
"key_prefix": "TICKET-",
"url_template": "https://example.com/TICKET?query=<num>",
"is_alphanumeric": false
}`,
expectedAutolink: &shared.Autolink{
ID: 123,
KeyPrefix: "TICKET-",
URLTemplate: "https://example.com/TICKET?query=<num>",
IsAlphanumeric: false,
},
},
{
name: "404 repo or autolink not found",
id: "123",
stubStatus: 404,
stubRespJSON: `{
"message": "Not Found",
"documentation_url": "https://docs.github.com/rest/repos/autolinks#get-an-autolink-reference-of-a-repository",
"status": "404"
}`,
expectErr: true,
expectedErrMsg: "HTTP 404: Either no autolink with this ID exists for this repository or you are missing admin rights to the repository. (https://api.github.com/repos/OWNER/REPO/autolinks/123)",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reg := &httpmock.Registry{}
reg.Register(
httpmock.REST(
http.MethodGet,
fmt.Sprintf("repos/%s/%s/autolinks/%s", repo.RepoOwner(), repo.RepoName(), tt.id),
),
httpmock.StatusStringResponse(tt.stubStatus, tt.stubRespJSON),
)
defer reg.Verify(t)
autolinkCreator := &AutolinkViewer{
HTTPClient: &http.Client{Transport: reg},
}
autolink, err := autolinkCreator.View(repo, tt.id)
if tt.expectErr {
require.EqualError(t, err, tt.expectedErrMsg)
} else {
require.NoError(t, err)
assert.Equal(t, tt.expectedAutolink, autolink)
}
})
}
}

View file

@ -0,0 +1,96 @@
package view
import (
"fmt"
"github.com/cli/cli/v2/internal/browser"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmd/repo/autolink/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/spf13/cobra"
)
type viewOptions struct {
BaseRepo func() (ghrepo.Interface, error)
Browser browser.Browser
AutolinkClient AutolinkViewClient
IO *iostreams.IOStreams
Exporter cmdutil.Exporter
ID string
}
type AutolinkViewClient interface {
View(repo ghrepo.Interface, id string) (*shared.Autolink, error)
}
func NewCmdView(f *cmdutil.Factory, runF func(*viewOptions) error) *cobra.Command {
opts := &viewOptions{
Browser: f.Browser,
IO: f.IOStreams,
}
cmd := &cobra.Command{
Use: "view <id>",
Short: "View an autolink reference",
Long: "View an autolink reference for a repository.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
httpClient, err := f.HttpClient()
if err != nil {
return err
}
opts.BaseRepo = f.BaseRepo
opts.ID = args[0]
opts.AutolinkClient = &AutolinkViewer{HTTPClient: httpClient}
if runF != nil {
return runF(opts)
}
return viewRun(opts)
},
}
cmdutil.AddJSONFlags(cmd, &opts.Exporter, shared.AutolinkFields)
return cmd
}
func viewRun(opts *viewOptions) error {
repo, err := opts.BaseRepo()
if err != nil {
return err
}
out := opts.IO.Out
cs := opts.IO.ColorScheme()
autolink, err := opts.AutolinkClient.View(repo, opts.ID)
if err != nil {
return fmt.Errorf("%s %w", cs.Red("error viewing autolink:"), err)
}
if opts.Exporter != nil {
return opts.Exporter.Write(opts.IO, autolink)
}
fmt.Fprintf(out, "Autolink in %s\n\n", ghrepo.FullName(repo))
fmt.Fprint(out, cs.Bold("ID: "))
fmt.Fprintln(out, cs.Cyanf("%d", autolink.ID))
fmt.Fprint(out, cs.Bold("Key Prefix: "))
fmt.Fprintln(out, autolink.KeyPrefix)
fmt.Fprint(out, cs.Bold("URL Template: "))
fmt.Fprintln(out, autolink.URLTemplate)
fmt.Fprint(out, cs.Bold("Alphanumeric: "))
fmt.Fprintln(out, autolink.IsAlphanumeric)
return nil
}

View file

@ -0,0 +1,197 @@
package view
import (
"bytes"
"net/http"
"testing"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/browser"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmd/repo/autolink/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/jsonfieldstest"
"github.com/google/shlex"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestJSONFields(t *testing.T) {
jsonfieldstest.ExpectCommandToSupportJSONFields(t, NewCmdView, []string{
"id",
"isAlphanumeric",
"keyPrefix",
"urlTemplate",
})
}
func TestNewCmdView(t *testing.T) {
tests := []struct {
name string
input string
output viewOptions
wantErr bool
wantExporter bool
errMsg string
}{
{
name: "no argument",
input: "",
wantErr: true,
errMsg: "accepts 1 arg(s), received 0",
},
{
name: "id provided",
input: "123",
output: viewOptions{ID: "123"},
},
{
name: "json flag",
input: "123 --json id",
output: viewOptions{},
wantExporter: true,
},
{
name: "invalid json flag",
input: "123 --json invalid",
output: viewOptions{},
wantErr: true,
errMsg: "Unknown JSON field: \"invalid\"\nAvailable fields:\n id\n isAlphanumeric\n keyPrefix\n urlTemplate",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ios, _, _, _ := iostreams.Test()
f := &cmdutil.Factory{
IOStreams: ios,
}
f.HttpClient = func() (*http.Client, error) {
return &http.Client{}, nil
}
argv, err := shlex.Split(tt.input)
require.NoError(t, err)
var gotOpts *viewOptions
cmd := NewCmdView(f, func(opts *viewOptions) error {
gotOpts = opts
return nil
})
cmd.SetArgs(argv)
cmd.SetIn(&bytes.Buffer{})
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
_, err = cmd.ExecuteC()
if tt.wantErr {
require.EqualError(t, err, tt.errMsg)
} else {
require.NoError(t, err)
assert.Equal(t, tt.wantExporter, gotOpts.Exporter != nil)
}
})
}
}
type stubAutoLinkViewer struct {
autolink *shared.Autolink
err error
}
func (g stubAutoLinkViewer) View(repo ghrepo.Interface, id string) (*shared.Autolink, error) {
return g.autolink, g.err
}
type testAutolinkClientViewError struct{}
func (e testAutolinkClientViewError) Error() string {
return "autolink client view error"
}
func TestViewRun(t *testing.T) {
tests := []struct {
name string
opts *viewOptions
stubViewer stubAutoLinkViewer
expectedErr error
wantStdout string
}{
{
name: "view",
opts: &viewOptions{
ID: "1",
},
stubViewer: stubAutoLinkViewer{
autolink: &shared.Autolink{
ID: 1,
KeyPrefix: "TICKET-",
URLTemplate: "https://example.com/TICKET?query=<num>",
IsAlphanumeric: true,
},
},
wantStdout: heredoc.Doc(`
Autolink in OWNER/REPO
ID: 1
Key Prefix: TICKET-
URL Template: https://example.com/TICKET?query=<num>
Alphanumeric: true
`),
},
{
name: "view json",
opts: &viewOptions{
Exporter: func() cmdutil.Exporter {
exporter := cmdutil.NewJSONExporter()
exporter.SetFields([]string{"id"})
return exporter
}(),
},
stubViewer: stubAutoLinkViewer{
autolink: &shared.Autolink{
ID: 1,
KeyPrefix: "TICKET-",
URLTemplate: "https://example.com/TICKET?query=<num>",
IsAlphanumeric: true,
},
},
wantStdout: "{\"id\":1}\n",
},
{
name: "client error",
opts: &viewOptions{},
stubViewer: stubAutoLinkViewer{
autolink: nil,
err: testAutolinkClientViewError{},
},
expectedErr: testAutolinkClientViewError{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ios, _, stdout, _ := iostreams.Test()
opts := tt.opts
opts.IO = ios
opts.Browser = &browser.Stub{}
opts.IO = ios
opts.BaseRepo = func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil }
opts.AutolinkClient = &tt.stubViewer
err := viewRun(opts)
if tt.expectedErr != nil {
require.Error(t, err)
assert.ErrorIs(t, err, tt.expectedErr)
} else {
require.NoError(t, err)
assert.Equal(t, tt.wantStdout, stdout.String())
}
})
}
}