Merge branch 'cli:trunk' into codespaces-accept-perms

This commit is contained in:
Mark Phelps 2022-02-16 10:08:49 -05:00 committed by GitHub
commit 84aad52b22
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 393 additions and 158 deletions

View file

@ -27,6 +27,13 @@ jobs:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
- name: Install osslsigncode
run: sudo apt-get install -y osslsigncode
- name: Obtain signing cert
run: |
cert="$(mktemp -t cert.XXX)"
base64 -d <<<"$CERT_CONTENTS" > "$cert"
echo "CERT_FILE=$cert" >> $GITHUB_ENV
env:
CERT_CONTENTS: ${{ secrets.WINDOWS_CERT_PFX }}
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
with:
@ -35,8 +42,7 @@ jobs:
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
GORELEASER_CURRENT_TAG: ${{steps.changelog.outputs.tag-name}}
GITHUB_CERT_PASSWORD: ${{secrets.GITHUB_CERT_PASSWORD}}
DESKTOP_CERT_TOKEN: ${{secrets.DESKTOP_CERT_TOKEN}}
CERT_PASSWORD: ${{secrets.WINDOWS_CERT_PASSWORD}}
- name: Checkout documentation site
uses: actions/checkout@v2
with:
@ -147,15 +153,18 @@ jobs:
"${MSBUILD_PATH}\MSBuild.exe" ./build/windows/gh.wixproj -p:SourceDir="$PWD" -p:OutputPath="$PWD" -p:OutputName="$name" -p:ProductVersion="$version"
- name: Obtain signing cert
id: obtain_cert
shell: bash
run: |
base64 -d <<<"$CERT_CONTENTS" > ./cert.pfx
printf "::set-output name=cert-file::%s\n" ".\\cert.pfx"
env:
DESKTOP_CERT_TOKEN: ${{ secrets.DESKTOP_CERT_TOKEN }}
run: .\script\setup-windows-certificate.ps1
CERT_CONTENTS: ${{ secrets.WINDOWS_CERT_PFX }}
- name: Sign MSI
env:
CERT_FILE: ${{ steps.obtain_cert.outputs.cert-file }}
EXE_FILE: ${{ steps.buildmsi.outputs.msi }}
GITHUB_CERT_PASSWORD: ${{ secrets.GITHUB_CERT_PASSWORD }}
run: .\script\sign.ps1 -Certificate $env:CERT_FILE -Executable $env:EXE_FILE
CERT_PASSWORD: ${{ secrets.WINDOWS_CERT_PASSWORD }}
run: .\script\signtool sign /d "GitHub CLI" /f $env:CERT_FILE /p $env:CERT_PASSWORD /fd sha256 /tr http://timestamp.digicert.com /v $env:EXE_FILE
- name: Upload MSI
shell: bash
run: |

View file

@ -9,7 +9,6 @@ before:
hooks:
- go mod tidy
- make manpages GH_VERSION={{.Version}}
- ./script/prepare-windows-cert.sh '{{ if index .Env "GITHUB_CERT_PASSWORD" }}{{ .Env.GITHUB_CERT_PASSWORD}}{{ end }}' '{{ if index .Env "DESKTOP_CERT_TOKEN" }}{{ .Env.DESKTOP_CERT_TOKEN}}{{ end }}'
builds:
- <<: &build_defaults

View file

@ -5,13 +5,14 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/cli/cli/v2/internal/ghinstance"
"io"
"net/http"
"sort"
"strings"
"time"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/shurcooL/githubv4"
)

View file

@ -14,8 +14,8 @@ our release schedule.
Install:
```bash
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/etc/apt/trusted.gpg.d/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/trusted.gpg.d/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
sudo apt update
sudo apt install gh
```

View file

@ -36,6 +36,7 @@ type ListOptions struct {
HeadBranch string
Labels []string
Author string
AppAuthor string
Assignee string
Search string
Draft string
@ -82,6 +83,14 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
opts.Draft = strconv.FormatBool(draft)
}
if err := cmdutil.MutuallyExclusive(
"specify only `--author` or `--app`",
opts.Author != "",
opts.AppAuthor != "",
); err != nil {
return err
}
if runF != nil {
return runF(opts)
}
@ -99,6 +108,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
cmd.Flags().StringVarP(&opts.HeadBranch, "head", "H", "", "Filter by head branch")
cmd.Flags().StringSliceVarP(&opts.Labels, "label", "l", nil, "Filter by labels")
cmd.Flags().StringVarP(&opts.Author, "author", "A", "", "Filter by author")
cmd.Flags().StringVar(&opts.AppAuthor, "app", "", "Filter by GitHub App author")
cmd.Flags().StringVarP(&opts.Assignee, "assignee", "a", "", "Filter by assignee")
cmd.Flags().StringVarP(&opts.Search, "search", "S", "", "Search pull requests with `query`")
cmd.Flags().BoolVarP(&draft, "draft", "d", false, "Filter by draft state")
@ -163,6 +173,10 @@ func listRun(opts *ListOptions) error {
return opts.Browser.Browse(openURL)
}
if opts.AppAuthor != "" {
filters.Author = fmt.Sprintf("app/%s", opts.AppAuthor)
}
listResult, err := listPullRequests(httpClient, baseRepo, filters, opts.LimitResults)
if err != nil {
return err

View file

@ -229,6 +229,53 @@ func TestPRList_filteringDraft(t *testing.T) {
}
}
func TestPRList_filteringAuthor(t *testing.T) {
tests := []struct {
name string
cli string
expectedQuery string
}{
{
name: "author @me",
cli: `--author "@me"`,
expectedQuery: `repo:OWNER/REPO is:pr is:open author:@me`,
},
{
name: "author user",
cli: `--author "monalisa"`,
expectedQuery: `repo:OWNER/REPO is:pr is:open author:monalisa`,
},
{
name: "app author",
cli: `--author "app/dependabot"`,
expectedQuery: `repo:OWNER/REPO is:pr is:open author:app/dependabot`,
},
{
name: "app author with app option",
cli: `--app "dependabot"`,
expectedQuery: `repo:OWNER/REPO is:pr is:open author:app/dependabot`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
http := initFakeHTTP()
defer http.Verify(t)
http.Register(
httpmock.GraphQL(`query PullRequestSearch\b`),
httpmock.GraphQLQuery(`{}`, func(_ string, params map[string]interface{}) {
assert.Equal(t, test.expectedQuery, params["q"].(string))
}))
_, err := runCommand(http, true, test.cli)
if err != nil {
t.Fatal(err)
}
})
}
}
func TestPRList_withInvalidLimitFlag(t *testing.T) {
http := initFakeHTTP()
defer http.Verify(t)

View file

@ -174,6 +174,7 @@ func createRun(opts *CreateOptions) error {
return err
}
var existingTag bool
if opts.TagName == "" {
tags, err := getTags(httpClient, baseRepo, 5)
if err != nil {
@ -198,6 +199,7 @@ func createRun(opts *CreateOptions) error {
return fmt.Errorf("could not prompt: %w", err)
}
if tag != createNewTagOption {
existingTag = true
opts.TagName = tag
}
}
@ -213,6 +215,29 @@ func createRun(opts *CreateOptions) error {
}
}
var tagDescription string
if opts.RepoOverride == "" {
tagDescription, _ = gitTagInfo(opts.TagName)
// If there is a local tag with the same name as specified
// the user may not want to create a new tag on the remote
// as the local one might be annotated or signed.
// If the user specifies the target take that as explict instruction
// to create the tag on the remote pointing to the target regardless
// of local tag status.
// If a remote tag with the same name as specified exists already
// then a new tag will not be created so ignore local tag status.
if tagDescription != "" && !existingTag && opts.Target == "" {
remoteExists, err := remoteTagExists(httpClient, baseRepo, opts.TagName)
if err != nil {
return err
}
if !remoteExists {
return fmt.Errorf("tag %s exists locally but has not been pushed to %s, please push it before continuing or specify the `--target` flag to create a new tag",
opts.TagName, ghrepo.FullName(baseRepo))
}
}
}
if !opts.BodyProvided && opts.IO.CanPrompt() {
editorCommand, err := cmdutil.DetermineEditor(opts.Config)
if err != nil {
@ -220,7 +245,6 @@ func createRun(opts *CreateOptions) error {
}
var generatedNotes *releaseNotes
var tagDescription string
var generatedChangelog string
params := map[string]interface{}{
@ -236,7 +260,6 @@ func createRun(opts *CreateOptions) error {
if opts.RepoOverride == "" {
headRef := opts.TagName
tagDescription, _ = gitTagInfo(opts.TagName)
if tagDescription == "" {
if opts.Target != "" {
// TODO: use the remote-tracking version of the branch ref
@ -366,13 +389,6 @@ func createRun(opts *CreateOptions) error {
}
}
if opts.Draft && len(opts.DiscussionCategory) > 0 {
return fmt.Errorf(
"%s Discussions not supported with draft releases",
opts.IO.ColorScheme().FailureIcon(),
)
}
params := map[string]interface{}{
"tag_name": opts.TagName,
"draft": opts.Draft,
@ -420,7 +436,7 @@ func createRun(opts *CreateOptions) error {
}
if !opts.Draft {
rel, err := publishRelease(httpClient, newRelease.APIURL)
rel, err := publishRelease(httpClient, newRelease.APIURL, opts.DiscussionCategory)
if err != nil {
return err
}

View file

@ -287,7 +287,7 @@ func Test_createRun(t *testing.T) {
name string
isTTY bool
opts CreateOptions
wantParams interface{}
httpStubs func(t *testing.T, reg *httpmock.Registry)
wantErr string
wantStdout string
wantStderr string
@ -302,12 +302,20 @@ func Test_createRun(t *testing.T) {
BodyProvided: true,
Target: "",
},
wantParams: map[string]interface{}{
"tag_name": "v1.2.3",
"name": "The Big 1.2",
"body": "* Fixed bugs",
"draft": false,
"prerelease": false,
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
"url": "https://api.github.com/releases/123",
"upload_url": "https://api.github.com/assets/upload",
"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
}`, func(params map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"tag_name": "v1.2.3",
"name": "The Big 1.2",
"body": "* Fixed bugs",
"draft": false,
"prerelease": false,
}, params)
}))
},
wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
wantStderr: ``,
@ -323,13 +331,21 @@ func Test_createRun(t *testing.T) {
Target: "",
DiscussionCategory: "General",
},
wantParams: map[string]interface{}{
"tag_name": "v1.2.3",
"name": "The Big 1.2",
"body": "* Fixed bugs",
"draft": false,
"prerelease": false,
"discussion_category_name": "General",
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
"url": "https://api.github.com/releases/123",
"upload_url": "https://api.github.com/assets/upload",
"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
}`, func(params map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"tag_name": "v1.2.3",
"name": "The Big 1.2",
"body": "* Fixed bugs",
"draft": false,
"prerelease": false,
"discussion_category_name": "General",
}, params)
}))
},
wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
wantStderr: ``,
@ -344,11 +360,19 @@ func Test_createRun(t *testing.T) {
BodyProvided: true,
Target: "main",
},
wantParams: map[string]interface{}{
"tag_name": "v1.2.3",
"draft": false,
"prerelease": false,
"target_commitish": "main",
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
"url": "https://api.github.com/releases/123",
"upload_url": "https://api.github.com/assets/upload",
"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
}`, func(params map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"tag_name": "v1.2.3",
"draft": false,
"prerelease": false,
"target_commitish": "main",
}, params)
}))
},
wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
wantStderr: ``,
@ -364,35 +388,22 @@ func Test_createRun(t *testing.T) {
Draft: true,
Target: "",
},
wantParams: map[string]interface{}{
"tag_name": "v1.2.3",
"draft": true,
"prerelease": false,
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
"url": "https://api.github.com/releases/123",
"upload_url": "https://api.github.com/assets/upload",
"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
}`, func(params map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"tag_name": "v1.2.3",
"draft": true,
"prerelease": false,
}, params)
}))
},
wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
wantStderr: ``,
},
{
name: "discussion category for draft release",
isTTY: true,
opts: CreateOptions{
TagName: "v1.2.3",
Name: "",
Body: "",
BodyProvided: true,
Draft: true,
Target: "",
DiscussionCategory: "general",
},
wantParams: map[string]interface{}{
"tag_name": "v1.2.3",
"draft": true,
"prerelease": false,
"discussion_category_name": "general",
},
wantErr: "X Discussions not supported with draft releases",
wantStdout: "",
},
{
name: "with generate notes",
isTTY: true,
@ -404,11 +415,19 @@ func Test_createRun(t *testing.T) {
BodyProvided: true,
GenerateNotes: true,
},
wantParams: map[string]interface{}{
"tag_name": "v1.2.3",
"draft": false,
"prerelease": false,
"generate_release_notes": true,
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
"url": "https://api.github.com/releases/123",
"upload_url": "https://api.github.com/assets/upload",
"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
}`, func(params map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"tag_name": "v1.2.3",
"draft": false,
"prerelease": false,
"generate_release_notes": true,
}, params)
}))
},
wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
wantErr: "",
@ -433,10 +452,97 @@ func Test_createRun(t *testing.T) {
},
Concurrency: 1,
},
wantParams: map[string]interface{}{
"tag_name": "v1.2.3",
"draft": true,
"prerelease": false,
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
"url": "https://api.github.com/releases/123",
"upload_url": "https://api.github.com/assets/upload",
"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
}`, func(params map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"tag_name": "v1.2.3",
"draft": true,
"prerelease": false,
}, params)
}))
reg.Register(httpmock.REST("POST", "assets/upload"), func(req *http.Request) (*http.Response, error) {
q := req.URL.Query()
assert.Equal(t, "ball.tgz", q.Get("name"))
assert.Equal(t, "", q.Get("label"))
return &http.Response{
StatusCode: 201,
Request: req,
Body: ioutil.NopCloser(bytes.NewBufferString(`{}`)),
Header: map[string][]string{
"Content-Type": {"application/json"},
},
}, nil
})
reg.Register(httpmock.REST("PATCH", "releases/123"), httpmock.RESTPayload(201, `{
"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3-final"
}`, func(params map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"draft": false,
}, params)
}))
},
wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3-final\n",
wantStderr: ``,
},
{
name: "upload files and create discussion",
isTTY: true,
opts: CreateOptions{
TagName: "v1.2.3",
Name: "",
Body: "",
BodyProvided: true,
Draft: false,
Target: "",
Assets: []*shared.AssetForUpload{
{
Name: "ball.tgz",
Open: func() (io.ReadCloser, error) {
return ioutil.NopCloser(bytes.NewBufferString(`TARBALL`)), nil
},
},
},
DiscussionCategory: "general",
Concurrency: 1,
},
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
"url": "https://api.github.com/releases/123",
"upload_url": "https://api.github.com/assets/upload",
"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
}`, func(params map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"tag_name": "v1.2.3",
"draft": true,
"prerelease": false,
"discussion_category_name": "general",
}, params)
}))
reg.Register(httpmock.REST("POST", "assets/upload"), func(req *http.Request) (*http.Response, error) {
q := req.URL.Query()
assert.Equal(t, "ball.tgz", q.Get("name"))
assert.Equal(t, "", q.Get("label"))
return &http.Response{
StatusCode: 201,
Request: req,
Body: ioutil.NopCloser(bytes.NewBufferString(`{}`)),
Header: map[string][]string{
"Content-Type": {"application/json"},
},
}, nil
})
reg.Register(httpmock.REST("PATCH", "releases/123"), httpmock.RESTPayload(201, `{
"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3-final"
}`, func(params map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"draft": false,
"discussion_category_name": "general",
}, params)
}))
},
wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3-final\n",
wantStderr: ``,
@ -450,15 +556,10 @@ func Test_createRun(t *testing.T) {
io.SetStderrTTY(tt.isTTY)
fakeHTTP := &httpmock.Registry{}
fakeHTTP.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.StatusStringResponse(201, `{
"url": "https://api.github.com/releases/123",
"upload_url": "https://api.github.com/assets/upload",
"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
}`))
fakeHTTP.Register(httpmock.REST("POST", "assets/upload"), httpmock.StatusStringResponse(201, `{}`))
fakeHTTP.Register(httpmock.REST("PATCH", "releases/123"), httpmock.StatusStringResponse(201, `{
"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3-final"
}`))
if tt.httpStubs != nil {
tt.httpStubs(t, fakeHTTP)
}
defer fakeHTTP.Verify(t)
tt.opts.IO = io
tt.opts.HttpClient = func() (*http.Client, error) {
@ -476,26 +577,6 @@ func Test_createRun(t *testing.T) {
require.NoError(t, err)
}
bb, err := ioutil.ReadAll(fakeHTTP.Requests[0].Body)
require.NoError(t, err)
var params interface{}
err = json.Unmarshal(bb, &params)
require.NoError(t, err)
assert.Equal(t, tt.wantParams, params)
if len(tt.opts.Assets) > 0 {
q := fakeHTTP.Requests[1].URL.Query()
assert.Equal(t, tt.opts.Assets[0].Name, q.Get("name"))
assert.Equal(t, tt.opts.Assets[0].Label, q.Get("label"))
bb, err := ioutil.ReadAll(fakeHTTP.Requests[2].Body)
require.NoError(t, err)
var updateParams interface{}
err = json.Unmarshal(bb, &updateParams)
require.NoError(t, err)
assert.Equal(t, map[string]interface{}{"draft": false}, updateParams)
}
assert.Equal(t, tt.wantStdout, stdout.String())
assert.Equal(t, tt.wantStderr, stderr.String())
})
@ -673,6 +754,8 @@ func Test_createRun_interactive(t *testing.T) {
rs.Register(`git describe --tags --abbrev=0 v1\.2\.3\^`, 1, "")
},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(httpmock.GraphQL("RepositoryFindRef"),
httpmock.StringResponse(`{"data":{"repository":{"ref": {"id": "tag id"}}}}`))
reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"),
httpmock.StatusStringResponse(404, `{}`))
reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"),
@ -690,6 +773,57 @@ func Test_createRun_interactive(t *testing.T) {
},
wantOut: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
},
{
name: "error when unpublished local tag and target not specified",
opts: &CreateOptions{
TagName: "v1.2.3",
},
runStubs: func(rs *run.CommandStubber) {
rs.Register(`git tag --list`, 0, "tag exists")
},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(httpmock.GraphQL("RepositoryFindRef"),
httpmock.StringResponse(`{"data":{"repository":{"ref": {"id": ""}}}}`))
},
wantErr: "tag v1.2.3 exists locally but has not been pushed to OWNER/REPO, please push it before continuing or specify the `--target` flag to create a new tag",
},
{
name: "create a release when unpublished local tag and target specified",
opts: &CreateOptions{
TagName: "v1.2.3",
Target: "main",
},
askStubs: func(as *prompt.AskStubber) {
as.StubPrompt("Title (optional)").AnswerWith("")
as.StubPrompt("Release notes").
AssertOptions([]string{"Write my own", "Write using generated notes as template", "Write using git tag message as template", "Leave blank"}).
AnswerWith("Leave blank")
as.StubPrompt("Is this a prerelease?").AnswerWith(false)
as.StubPrompt("Submit?").AnswerWith("Publish release")
},
runStubs: func(rs *run.CommandStubber) {
rs.Register(`git tag --list`, 0, "tag exists")
},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"),
httpmock.StatusStringResponse(200, `{
"name": "generated name",
"body": "generated body"
}`))
reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.StatusStringResponse(201, `{
"url": "https://api.github.com/releases/123",
"upload_url": "https://api.github.com/assets/upload",
"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
}`))
},
wantParams: map[string]interface{}{
"draft": false,
"prerelease": false,
"tag_name": "v1.2.3",
"target_commitish": "main",
},
wantOut: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
},
}
for _, tt := range tests {
ios, _, stdout, stderr := iostreams.Test()
@ -739,7 +873,17 @@ func Test_createRun_interactive(t *testing.T) {
}
if tt.wantParams != nil {
bb, err := ioutil.ReadAll(reg.Requests[1].Body)
var r *http.Request
for _, req := range reg.Requests {
if req.URL.Path == "/repos/OWNER/REPO/releases" {
r = req
break
}
}
if r == nil {
t.Fatalf("no http requests for creating a release found")
}
bb, err := ioutil.ReadAll(r.Body)
assert.NoError(t, err)
var params map[string]interface{}
err = json.Unmarshal(bb, &params)

View file

@ -2,6 +2,7 @@ package create
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
@ -13,6 +14,7 @@ import (
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmd/release/shared"
graphql "github.com/cli/shurcooL-graphql"
)
type tag struct {
@ -26,6 +28,25 @@ type releaseNotes struct {
var notImplementedError = errors.New("not implemented")
func remoteTagExists(httpClient *http.Client, repo ghrepo.Interface, tagName string) (bool, error) {
gql := graphql.NewClient(ghinstance.GraphQLEndpoint(repo.RepoHost()), httpClient)
qualifiedTagName := fmt.Sprintf("refs/tags/%s", tagName)
var query struct {
Repository struct {
Ref struct {
ID string
} `graphql:"ref(qualifiedName: $tagName)"`
} `graphql:"repository(owner: $owner, name: $name)"`
}
variables := map[string]interface{}{
"owner": graphql.String(repo.RepoOwner()),
"name": graphql.String(repo.RepoName()),
"tagName": graphql.String(qualifiedTagName),
}
err := gql.QueryNamed(context.Background(), "RepositoryFindRef", &query, variables)
return query.Repository.Ref.ID != "", err
}
func getTags(httpClient *http.Client, repo ghrepo.Interface, limit int) ([]tag, error) {
path := fmt.Sprintf("repos/%s/%s/tags?per_page=%d", repo.RepoOwner(), repo.RepoName(), limit)
url := ghinstance.RESTPrefix(repo.RepoHost()) + path
@ -134,8 +155,17 @@ func createRelease(httpClient *http.Client, repo ghrepo.Interface, params map[st
return &newRelease, err
}
func publishRelease(httpClient *http.Client, releaseURL string) (*shared.Release, error) {
req, err := http.NewRequest("PATCH", releaseURL, bytes.NewBufferString(`{"draft":false}`))
func publishRelease(httpClient *http.Client, releaseURL string, discussionCategory string) (*shared.Release, error) {
params := map[string]interface{}{"draft": false}
if discussionCategory != "" {
params["discussion_category_name"] = discussionCategory
}
bodyBytes, err := json.Marshal(params)
if err != nil {
return nil, err
}
req, err := http.NewRequest("PATCH", releaseURL, bytes.NewBuffer(bodyBytes))
if err != nil {
return nil, err
}

View file

@ -57,7 +57,19 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobr
- by URL, e.g. "https://github.com/OWNER/REPO"
`),
},
Long: heredoc.Docf(`
Edit repository settings.
To toggle a setting off, use the %[1]s--flag=false%[1]s syntax.
`, "`"),
Args: cobra.MaximumNArgs(1),
Example: heredoc.Doc(`
# enable issues and wiki
gh repo edit --enable-issues --enable-wiki
# disable projects
gh repo edit --enable-projects=false
`),
RunE: func(cmd *cobra.Command, args []string) error {
if cmd.Flags().NFlag() == 0 {
return cmdutil.FlagErrorf("at least one flag is required")

View file

@ -1,19 +0,0 @@
#!/bin/bash
set -e
GITHUB_CERT_PASSWORD=$1
DESKTOP_CERT_TOKEN=$2
if [[ -z "$GITHUB_CERT_PASSWORD" || -z "$DESKTOP_CERT_TOKEN" ]]; then
echo "skipping windows signing prep; cert password or token not found"
exit 0
fi
curl \
-H "Authorization: token $DESKTOP_CERT_TOKEN" \
-H "Accept: application/vnd.github.v3.raw" \
--output windows-certificate.pfx \
https://api.github.com/repos/desktop/desktop-secrets/contents/windows-certificate.pfx
openssl pkcs12 -in windows-certificate.pfx -nocerts -nodes -out private-key.pem -passin pass:${GITHUB_CERT_PASSWORD}
openssl pkcs12 -in windows-certificate.pfx -nokeys -nodes -out certificate.pem -passin pass:${GITHUB_CERT_PASSWORD}

View file

@ -1,12 +0,0 @@
$scriptPath = split-path -parent $MyInvocation.MyCommand.Definition
$certFile = "$scriptPath\windows-certificate.pfx"
$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headers.Add("Authorization", "token $env:DESKTOP_CERT_TOKEN")
$headers.Add("Accept", 'application/vnd.github.v3.raw')
Invoke-WebRequest 'https://api.github.com/repos/desktop/desktop-secrets/contents/windows-certificate.pfx' `
-Headers $headers `
-OutFile "$certFile"
Write-Output "::set-output name=cert-file::$certFile"

View file

@ -1,26 +1,25 @@
#!/bin/bash
set -e
if [[ ! -e certificate.pem || ! -e private-key.pem ]]; then
echo "skipping windows signing; cert or key not found"
EXE="$1"
if [ -z "$CERT_FILE" ]; then
echo "skipping Windows code-signing; CERT_FILE not set" >&2
exit 0
fi
EXECUTABLE_PATH=$1
ARCH="386"
if [[ $EXECUTABLE_PATH =~ "amd64" ]]; then
ARCH="amd64"
if [ ! -f "$CERT_FILE" ]; then
echo "error Windows code-signing; file '$CERT_FILE' not found" >&2
exit 1
fi
OUT_PATH=gh_signed-${ARCH}.exe
if [ -z "$CERT_PASSWORD" ]; then
echo "error Windows code-signing; no value for CERT_PASSWORD" >&2
exit 1
fi
osslsigncode sign \
-certs certificate.pem \
-key private-key.pem \
-n "GitHub CLI" \
-t http://timestamp.digicert.com \
-in $EXECUTABLE_PATH \
-out $OUT_PATH
osslsigncode sign -n "GitHub CLI" -t http://timestamp.digicert.com \
-pkcs12 "$CERT_FILE" -readpass <(printf "%s" "$CERT_PASSWORD") -h sha256 \
-in "$EXE" -out "$EXE"~
mv $OUT_PATH $EXECUTABLE_PATH
mv "$EXE"~ "$EXE"

View file

@ -6,12 +6,7 @@ param (
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$thumbprint = "fb713a60a7fa79dfc03cb301ca05d4e8c1bdd431"
$passwd = $env:GITHUB_CERT_PASSWORD
$ProgramName = "GitHub CLI"
$scriptPath = split-path -parent $MyInvocation.MyCommand.Definition
& $scriptPath\signtool.exe sign /d $ProgramName /f $Certificate /p $passwd `
/sha1 $thumbprint /fd sha256 /tr http://timestamp.digicert.com /td sha256 /v `
$Executable
& $scriptPath\signtool.exe sign /d $ProgramName /f $Certificate /p $env:CERT_PASSWORD /fd sha256 /tr http://timestamp.digicert.com /v $Executable