Merge branch 'cli:trunk' into codespaces-accept-perms
This commit is contained in:
commit
84aad52b22
14 changed files with 393 additions and 158 deletions
21
.github/workflows/releases.yml
vendored
21
.github/workflows/releases.yml
vendored
|
|
@ -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: |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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, ¶ms)
|
||||
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, ¶ms)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue