mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Build: Add github release command to build/cmd (#56349)
* Add github release command to build/cmd * Use go-github library and implement dry-run * Make tag optional and default to metadata * Fix minor bug with tag default * Make some refactors to ease testing * Add tests for publish github command * Refactor publish github tests * Refactor test helper function name * Isolate local test
This commit is contained in:
committed by
GitHub
parent
4b68918b0b
commit
96a97f9827
@@ -232,6 +232,32 @@ func main() {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "github",
|
||||
Usage: "Publish packages to GitHub releases",
|
||||
Action: PublishGitHub,
|
||||
Flags: []cli.Flag{
|
||||
&dryRunFlag,
|
||||
&cli.StringFlag{
|
||||
Name: "path",
|
||||
Required: true,
|
||||
Usage: "Path to the asset to be published",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "repo",
|
||||
Required: true,
|
||||
Usage: "GitHub repository",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "tag",
|
||||
Usage: "Release tag (default from metadata)ß",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "create",
|
||||
Usage: "Create release if it doesn't exist",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
157
pkg/build/cmd/publishgithub.go
Normal file
157
pkg/build/cmd/publishgithub.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-github/github"
|
||||
"github.com/urfave/cli/v2"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type githubRepositoryService interface {
|
||||
GetReleaseByTag(ctx context.Context, owner string, repo string, tag string) (*github.RepositoryRelease, *github.Response, error)
|
||||
CreateRelease(ctx context.Context, owner string, repo string, release *github.RepositoryRelease) (*github.RepositoryRelease, *github.Response, error)
|
||||
UploadReleaseAsset(ctx context.Context, owner string, repo string, id int64, opt *github.UploadOptions, file *os.File) (*github.ReleaseAsset, *github.Response, error)
|
||||
}
|
||||
|
||||
type githubRepo struct {
|
||||
owner string
|
||||
name string
|
||||
}
|
||||
|
||||
type publishGithubFlags struct {
|
||||
create bool
|
||||
dryRun bool
|
||||
tag string
|
||||
repo *githubRepo
|
||||
artifactPath string
|
||||
}
|
||||
|
||||
var (
|
||||
newGithubClient = githubRepositoryClient
|
||||
errTokenIsEmpty = errors.New("the environment variable GH_TOKEN must be set")
|
||||
errTagIsEmpty = errors.New(`failed to retrieve release tag from metadata, use "--tag" to set it manually`)
|
||||
errReleaseNotFound = errors.New(`release not found, use "--create" to create the release`)
|
||||
)
|
||||
|
||||
func PublishGitHub(ctx *cli.Context) error {
|
||||
token := os.Getenv("GH_TOKEN")
|
||||
f, err := getFlags(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if f.tag == "" {
|
||||
return errTagIsEmpty
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
return errTokenIsEmpty
|
||||
}
|
||||
|
||||
if f.dryRun {
|
||||
return runDryRun(f, token, ctx)
|
||||
}
|
||||
|
||||
client := newGithubClient(ctx.Context, token)
|
||||
release, res, err := client.GetReleaseByTag(ctx.Context, f.repo.owner, f.repo.name, f.tag)
|
||||
if err != nil && res.StatusCode != 404 {
|
||||
return err
|
||||
}
|
||||
|
||||
if release == nil {
|
||||
if f.create {
|
||||
release, _, err = client.CreateRelease(ctx.Context, f.repo.owner, f.repo.name, &github.RepositoryRelease{TagName: &f.tag})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return errReleaseNotFound
|
||||
}
|
||||
}
|
||||
|
||||
artifactName := path.Base(f.artifactPath)
|
||||
file, err := os.Open(f.artifactPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
asset, _, err := client.UploadReleaseAsset(ctx.Context, f.repo.owner, f.repo.name, *release.ID, &github.UploadOptions{Name: artifactName}, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Asset '%s' uploaded to release '%s' on repository '%s/%s'\nDownload: %s\n", *asset.Name, f.tag, f.repo.owner, f.repo.name, *asset.BrowserDownloadURL)
|
||||
return nil
|
||||
}
|
||||
|
||||
func githubRepositoryClient(ctx context.Context, token string) githubRepositoryService {
|
||||
ts := oauth2.StaticTokenSource(
|
||||
&oauth2.Token{AccessToken: token},
|
||||
)
|
||||
tc := oauth2.NewClient(ctx, ts)
|
||||
|
||||
client := github.NewClient(tc)
|
||||
return client.Repositories
|
||||
}
|
||||
|
||||
func getFlags(ctx *cli.Context) (*publishGithubFlags, error) {
|
||||
metadata, err := GenerateMetadata(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tag := ctx.Value("tag").(string)
|
||||
if tag == "" && metadata.GrafanaVersion != "" {
|
||||
tag = fmt.Sprintf("v%s", metadata.GrafanaVersion)
|
||||
}
|
||||
fullRepo := ctx.Value("repo").(string)
|
||||
dryRun := ctx.Value("dry-run").(bool)
|
||||
owner := strings.Split(fullRepo, "/")[0]
|
||||
name := strings.Split(fullRepo, "/")[1]
|
||||
create := ctx.Value("create").(bool)
|
||||
artifactPath := ctx.Value("path").(string)
|
||||
return &publishGithubFlags{
|
||||
artifactPath: artifactPath,
|
||||
create: create,
|
||||
dryRun: dryRun,
|
||||
tag: tag,
|
||||
repo: &githubRepo{
|
||||
owner: owner,
|
||||
name: name,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func runDryRun(f *publishGithubFlags, token string, ctx *cli.Context) error {
|
||||
client := newGithubClient(ctx.Context, token)
|
||||
fmt.Println("Dry-Run: Retrieving release on repository by tag")
|
||||
release, res, err := client.GetReleaseByTag(ctx.Context, f.repo.owner, f.repo.name, f.tag)
|
||||
if err != nil && res.StatusCode != 404 {
|
||||
fmt.Println("Dry-Run: GitHub communication error:\n", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if release == nil {
|
||||
if f.create {
|
||||
fmt.Println("Dry-Run: Release doesn't exist and --create is enabled, so it would try to create the release")
|
||||
} else {
|
||||
fmt.Println("Dry-Run: Release doesn't exist and --create is disabled, so it would fail with error")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
artifactName := path.Base(f.artifactPath)
|
||||
fmt.Printf("Dry-Run: Opening file for release: %s\n", f.artifactPath)
|
||||
_, err = os.Open(f.artifactPath)
|
||||
if err != nil {
|
||||
fmt.Println("Dry-Run: Error opening file\n", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("Dry-Run: Would upload asset '%s' to release '%s' on repo '%s/%s' and return download URL if successful\n", artifactName, f.tag, f.repo.owner, f.repo.name)
|
||||
return nil
|
||||
}
|
||||
224
pkg/build/cmd/publishgithub_test.go
Normal file
224
pkg/build/cmd/publishgithub_test.go
Normal file
@@ -0,0 +1,224 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-github/github"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
type githubPublishTestCases struct {
|
||||
name string
|
||||
args []string
|
||||
token string
|
||||
expectedError error
|
||||
errorContains string
|
||||
expectedOutput string
|
||||
mockedService *mockGitHubRepositoryServiceImpl
|
||||
}
|
||||
|
||||
var mockGitHubRepositoryService = &mockGitHubRepositoryServiceImpl{}
|
||||
|
||||
func mockGithubRepositoryClient(context.Context, string) githubRepositoryService {
|
||||
return mockGitHubRepositoryService
|
||||
}
|
||||
|
||||
func TestPublishGitHub(t *testing.T) {
|
||||
t.Setenv("DRONE_BUILD_EVENT", "promote")
|
||||
testApp, testPath := setupPublishGithubTests(t)
|
||||
mockErrUnauthorized := errors.New("401")
|
||||
|
||||
testCases := []githubPublishTestCases{
|
||||
{
|
||||
name: "try to publish without required flags",
|
||||
errorContains: `Required flags "path, repo" not set`,
|
||||
},
|
||||
{
|
||||
name: "try to publish without token",
|
||||
args: []string{"--path", testPath, "--repo", "test/test", "--tag", "v1.0.0"},
|
||||
expectedError: errTokenIsEmpty,
|
||||
},
|
||||
{
|
||||
name: "try to publish with invalid token",
|
||||
token: "invalid",
|
||||
args: []string{"--path", testPath, "--repo", "test/test", "--tag", "v1.0.0"},
|
||||
mockedService: &mockGitHubRepositoryServiceImpl{tagErr: mockErrUnauthorized},
|
||||
expectedError: mockErrUnauthorized,
|
||||
},
|
||||
{
|
||||
name: "try to publish with valid token and nonexisting tag with create disabled",
|
||||
token: "valid",
|
||||
args: []string{"--path", testPath, "--repo", "test/test", "--tag", "v1.0.0"},
|
||||
mockedService: &mockGitHubRepositoryServiceImpl{tagErr: errReleaseNotFound},
|
||||
expectedError: errReleaseNotFound,
|
||||
},
|
||||
{
|
||||
name: "try to publish with valid token and nonexisting tag with create enabled",
|
||||
token: "valid",
|
||||
args: []string{"--path", testPath, "--repo", "test/test", "--tag", "v1.0.0", "--create"},
|
||||
mockedService: &mockGitHubRepositoryServiceImpl{tagErr: errReleaseNotFound},
|
||||
},
|
||||
{
|
||||
name: "try to publish with valid token and existing tag",
|
||||
token: "valid",
|
||||
args: []string{"--path", testPath, "--repo", "test/test", "--tag", "v1.0.0"},
|
||||
},
|
||||
{
|
||||
name: "dry run with invalid token",
|
||||
token: "invalid",
|
||||
args: []string{"--dry-run", "--path", testPath, "--repo", "test/test", "--tag", "v1.0.0"},
|
||||
mockedService: &mockGitHubRepositoryServiceImpl{tagErr: mockErrUnauthorized},
|
||||
expectedOutput: "GitHub communication error",
|
||||
},
|
||||
{
|
||||
name: "dry run with valid token and nonexisting tag with create disabled",
|
||||
token: "valid",
|
||||
args: []string{"--dry-run", "--path", testPath, "--repo", "test/test", "--tag", "v1.0.0"},
|
||||
mockedService: &mockGitHubRepositoryServiceImpl{tagErr: errReleaseNotFound},
|
||||
expectedOutput: "Release doesn't exist",
|
||||
},
|
||||
{
|
||||
name: "dry run with valid token and nonexisting tag with create enabled",
|
||||
token: "valid",
|
||||
args: []string{"--dry-run", "--path", testPath, "--repo", "test/test", "--tag", "v1.0.0", "--create"},
|
||||
mockedService: &mockGitHubRepositoryServiceImpl{tagErr: errReleaseNotFound},
|
||||
expectedOutput: "Would upload asset",
|
||||
},
|
||||
{
|
||||
name: "dry run with valid token and existing tag",
|
||||
token: "valid",
|
||||
args: []string{"--dry-run", "--path", testPath, "--repo", "test/test", "--tag", "v1.0.0"},
|
||||
expectedOutput: "Would upload asset",
|
||||
},
|
||||
}
|
||||
|
||||
if os.Getenv("DRONE_COMMIT") == "" {
|
||||
// this test only works locally due to Drone environment
|
||||
testCases = append(testCases,
|
||||
githubPublishTestCases{
|
||||
name: "try to publish without tag",
|
||||
args: []string{"--path", testPath, "--repo", "test/test"},
|
||||
expectedError: errTagIsEmpty,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if test.token != "" {
|
||||
t.Setenv("GH_TOKEN", test.token)
|
||||
}
|
||||
if test.mockedService != nil {
|
||||
mockGitHubRepositoryService = test.mockedService
|
||||
} else {
|
||||
mockGitHubRepositoryService = &mockGitHubRepositoryServiceImpl{}
|
||||
}
|
||||
args := []string{"run"}
|
||||
args = append(args, test.args...)
|
||||
out, err := captureStdout(t, func() error {
|
||||
return testApp.Run(args)
|
||||
})
|
||||
if test.expectedOutput != "" {
|
||||
assert.Contains(t, out, test.expectedOutput)
|
||||
}
|
||||
if test.expectedError != nil || test.errorContains != "" {
|
||||
assert.Error(t, err)
|
||||
if test.expectedError != nil {
|
||||
assert.ErrorIs(t, err, test.expectedError)
|
||||
}
|
||||
if test.errorContains != "" {
|
||||
assert.ErrorContains(t, err, test.errorContains)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func setupPublishGithubTests(t *testing.T) (*cli.App, string) {
|
||||
t.Helper()
|
||||
ex, err := os.Executable()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
testPath := filepath.Dir(ex)
|
||||
|
||||
newGithubClient = mockGithubRepositoryClient
|
||||
|
||||
testApp := cli.NewApp()
|
||||
testApp.Action = PublishGitHub
|
||||
testApp.Flags = []cli.Flag{
|
||||
&dryRunFlag,
|
||||
&cli.StringFlag{
|
||||
Name: "path",
|
||||
Required: true,
|
||||
Usage: "Path to the asset to be published",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "repo",
|
||||
Required: true,
|
||||
Usage: "GitHub repository",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "tag",
|
||||
Usage: "Release tag (default from metadata)ß",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "create",
|
||||
Usage: "Create release if it doesn't exist",
|
||||
},
|
||||
}
|
||||
return testApp, testPath
|
||||
}
|
||||
|
||||
func captureStdout(t *testing.T, fn func() error) (string, error) {
|
||||
t.Helper()
|
||||
rescueStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
err := fn()
|
||||
werr := w.Close()
|
||||
if werr != nil {
|
||||
return "", err
|
||||
}
|
||||
out, _ := io.ReadAll(r)
|
||||
os.Stdout = rescueStdout
|
||||
return string(out), err
|
||||
}
|
||||
|
||||
type mockGitHubRepositoryServiceImpl struct {
|
||||
tagErr error
|
||||
createErr error
|
||||
uploadErr error
|
||||
}
|
||||
|
||||
func (m *mockGitHubRepositoryServiceImpl) GetReleaseByTag(ctx context.Context, owner string, repo string, tag string) (*github.RepositoryRelease, *github.Response, error) {
|
||||
var release *github.RepositoryRelease
|
||||
res := &github.Response{Response: &http.Response{}}
|
||||
if m.tagErr == nil {
|
||||
releaseID := int64(1)
|
||||
release = &github.RepositoryRelease{ID: &releaseID}
|
||||
} else if errors.Is(m.tagErr, errReleaseNotFound) {
|
||||
res.StatusCode = 404
|
||||
}
|
||||
return release, res, m.tagErr
|
||||
}
|
||||
|
||||
func (m *mockGitHubRepositoryServiceImpl) CreateRelease(ctx context.Context, owner string, repo string, release *github.RepositoryRelease) (*github.RepositoryRelease, *github.Response, error) {
|
||||
releaseID := int64(1)
|
||||
return &github.RepositoryRelease{ID: &releaseID}, &github.Response{}, m.createErr
|
||||
}
|
||||
|
||||
func (m *mockGitHubRepositoryServiceImpl) UploadReleaseAsset(ctx context.Context, owner string, repo string, id int64, opt *github.UploadOptions, file *os.File) (*github.ReleaseAsset, *github.Response, error) {
|
||||
assetName := "test"
|
||||
assetUrl := "testurl.com.br"
|
||||
return &github.ReleaseAsset{Name: &assetName, BrowserDownloadURL: &assetUrl}, &github.Response{}, m.uploadErr
|
||||
}
|
||||
Reference in New Issue
Block a user