0
0
mirror of https://github.com/thegeeklab/wp-gitea-release.git synced 2024-11-14 09:00:44 +00:00

refactor: rework gitea client and add tests (#44)

This commit is contained in:
Robert Kaussow 2024-05-08 12:54:51 +02:00 committed by GitHub
parent 8b057ea06b
commit a196e5f4ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 471 additions and 91 deletions

View File

@ -23,7 +23,6 @@ linters:
- errchkjson - errchkjson
- errname - errname
- errorlint - errorlint
- execinquery
- exhaustive - exhaustive
- exportloopref - exportloopref
- forcetypeassert - forcetypeassert
@ -37,12 +36,12 @@ linters:
- gocyclo - gocyclo
- godot - godot
- godox - godox
- goerr113 - err113
- gofmt - gofmt
- gofumpt - gofumpt
- goheader - goheader
- goimports - goimports
- gomnd - mnd
- gomoddirectives - gomoddirectives
- gomodguard - gomodguard
- goprintffuncname - goprintffuncname
@ -95,3 +94,9 @@ run:
linters-settings: linters-settings:
gofumpt: gofumpt:
extra-rules: true extra-rules: true
issues:
exclude-rules:
- path: "_test.go"
linters:
- err113

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"net/http"
"net/url" "net/url"
"path/filepath" "path/filepath"
"strings" "strings"
@ -70,19 +69,17 @@ func (p *Plugin) Validate() error {
// Execute provides the implementation of the plugin. // Execute provides the implementation of the plugin.
func (p *Plugin) Execute() error { func (p *Plugin) Execute() error {
httpClient := &http.Client{} gitea, err := gitea.NewClient(
client, err := gitea.NewClient(
p.Settings.baseURL.String(), p.Settings.baseURL.String(),
gitea.SetToken(p.Settings.APIKey), gitea.SetToken(p.Settings.APIKey),
gitea.SetHTTPClient(httpClient), gitea.SetHTTPClient(p.Network.Client),
) )
if err != nil { if err != nil {
return err return err
} }
r := &Release{ client := NewGiteaClient(gitea)
Client: client, client.Release.Opt = GiteaReleaseOpt{
Owner: p.Metadata.Repository.Owner, Owner: p.Metadata.Repository.Owner,
Repo: p.Metadata.Repository.Name, Repo: p.Metadata.Repository.Name,
Tag: strings.TrimPrefix(p.Settings.CommitRef, "refs/tags/"), Tag: strings.TrimPrefix(p.Settings.CommitRef, "refs/tags/"),
@ -93,12 +90,20 @@ func (p *Plugin) Execute() error {
Note: p.Settings.Note, Note: p.Settings.Note,
} }
release, err := r.buildRelease() release, err := client.Release.Find()
if err != nil { if err != nil && !errors.Is(err, ErrReleaseNotFound) {
return fmt.Errorf("failed to create the release: %w", err) return fmt.Errorf("failed to retrieve release: %w", err)
} }
if err := r.uploadFiles(release.ID, p.Settings.files); err != nil { // If no release was found by that tag, create a new one.
if release == nil {
release, err = client.Release.Create()
if err != nil {
return fmt.Errorf("failed to create release: %w", err)
}
}
if err := client.Release.AddAttachments(release.ID, p.Settings.files); err != nil {
return fmt.Errorf("failed to upload the files: %w", err) return fmt.Errorf("failed to upload the files: %w", err)
} }

View File

@ -15,9 +15,17 @@ var (
ErrFileExists = errors.New("asset file already exist") ErrFileExists = errors.New("asset file already exist")
) )
// Release represents a release for a Gitea repository. type GiteaClient struct {
type Release struct { client *gitea.Client
*gitea.Client Release *GiteaRelease
}
type GiteaRelease struct {
client *gitea.Client
Opt GiteaReleaseOpt
}
type GiteaReleaseOpt struct {
Owner string Owner string
Repo string Repo string
Tag string Tag string
@ -28,65 +36,66 @@ type Release struct {
Note string Note string
} }
// buildRelease attempts to retrieve an existing release by the specified tag name. type FileExists string
func (rc *Release) buildRelease() (*gitea.Release, error) {
// first attempt to get a release by that tag
release, err := rc.getRelease()
if err != nil && release == nil { const (
fmt.Println(err) FileExistsOverwrite FileExists = "overwrite"
} else if release != nil { FileExistsFail FileExists = "fail"
return release, nil FileExistsSkip FileExists = "skip"
)
// NewGiteaClient creates a new GiteaClient instance with the provided Gitea client.
func NewGiteaClient(client *gitea.Client) *GiteaClient {
return &GiteaClient{
client: client,
Release: &GiteaRelease{
client: client,
Opt: GiteaReleaseOpt{},
},
} }
// if no release was found by that tag, create a new one
release, err = rc.newRelease()
if err != nil {
return nil, fmt.Errorf("failed to retrieve or create a release: %w", err)
}
return release, nil
} }
// getRelease retrieves the release with the specified tag name from the repository. // Find retrieves the release with the specified tag name from the repository.
func (rc *Release) getRelease() (*gitea.Release, error) { // If the release is not found, it returns an ErrReleaseNotFound error.
releases, _, err := rc.Client.ListReleases(rc.Owner, rc.Repo, gitea.ListReleasesOptions{}) func (r *GiteaRelease) Find() (*gitea.Release, error) {
releases, _, err := r.client.ListReleases(r.Opt.Owner, r.Opt.Repo, gitea.ListReleasesOptions{})
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, release := range releases { for _, release := range releases {
if release.TagName == rc.Tag { if release.TagName == r.Opt.Tag {
log.Info().Msgf("successfully retrieved %s release", rc.Tag) log.Info().Msgf("found release: %s", r.Opt.Tag)
return release, nil return release, nil
} }
} }
return nil, fmt.Errorf("%w: %s", ErrReleaseNotFound, rc.Tag) return nil, fmt.Errorf("%w: %s", ErrReleaseNotFound, r.Opt.Tag)
} }
// newRelease creates a new release on the repository with the specified options. // Create creates a new release on the Gitea repository with the specified options.
func (rc *Release) newRelease() (*gitea.Release, error) { // It returns the created release or an error if the creation failed.
r := gitea.CreateReleaseOption{ func (r *GiteaRelease) Create() (*gitea.Release, error) {
TagName: rc.Tag, opts := gitea.CreateReleaseOption{
IsDraft: rc.Draft, TagName: r.Opt.Tag,
IsPrerelease: rc.Prerelease, IsDraft: r.Opt.Draft,
Title: rc.Title, IsPrerelease: r.Opt.Prerelease,
Note: rc.Note, Title: r.Opt.Title,
Note: r.Opt.Note,
} }
release, _, err := rc.Client.CreateRelease(rc.Owner, rc.Repo, r) release, _, err := r.client.CreateRelease(r.Opt.Owner, r.Opt.Repo, opts)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create release: %w", err) return nil, fmt.Errorf("failed to create release: %w", err)
} }
log.Info().Msgf("successfully created %s release", rc.Tag) log.Info().Msgf("created release: %s", r.Opt.Tag)
return release, nil return release, nil
} }
// uploadFiles uploads the specified files as attachments to the release with the given ID. // AddAttachments uploads the specified files as attachments to the release with the given ID.
// It first checks for any existing attachments with the same names, // It first checks for any existing attachments with the same names,
// and handles them according to the FileExists option: // and handles them according to the FileExists option:
// //
@ -95,61 +104,66 @@ func (rc *Release) newRelease() (*gitea.Release, error) {
// - "skip": skips uploading the file and logs a warning // - "skip": skips uploading the file and logs a warning
// //
// If there are no conflicts, it uploads the new files as attachments to the release. // If there are no conflicts, it uploads the new files as attachments to the release.
func (rc *Release) uploadFiles(releaseID int64, files []string) error { func (r *GiteaRelease) AddAttachments(releaseID int64, files []string) error {
attachments, _, err := rc.Client.ListReleaseAttachments( attachments, _, err := r.client.ListReleaseAttachments(
rc.Owner, r.Opt.Owner,
rc.Repo, r.Opt.Repo,
releaseID, releaseID,
gitea.ListReleaseAttachmentsOptions{}, gitea.ListReleaseAttachmentsOptions{},
) )
if err != nil { if err != nil {
return fmt.Errorf("failed to fetch existing assets: %w", err) return fmt.Errorf("failed to fetch attachments: %w", err)
} }
var uploadFiles []string existingAttachments := make(map[string]bool)
attachmentsMap := make(map[string]*gitea.Attachment)
for _, attachment := range attachments {
attachmentsMap[attachment.Name] = attachment
existingAttachments[attachment.Name] = true
}
files:
for _, file := range files { for _, file := range files {
for _, attachment := range attachments { fileName := path.Base(file)
if attachment.Name == path.Base(file) { if existingAttachments[fileName] {
switch rc.FileExists { switch FileExists(r.Opt.FileExists) {
case "overwrite": case FileExistsOverwrite:
// do nothing _, err := r.client.DeleteReleaseAttachment(r.Opt.Owner, r.Opt.Repo, releaseID, attachmentsMap[fileName].ID)
case "fail": if err != nil {
return fmt.Errorf("%w: %s", ErrFileExists, path.Base(file)) return fmt.Errorf("failed to delete artifact: %s: %w", fileName, err)
case "skip":
log.Warn().Msgf("skipping pre-existing %s artifact", attachment.Name)
continue files
} }
log.Info().Msgf("deleted artifact: %s", fileName)
case FileExistsFail:
return fmt.Errorf("%w: %s", ErrFileExists, fileName)
case FileExistsSkip:
log.Warn().Msgf("skip existing artifact: %s", fileName)
continue
} }
} }
uploadFiles = append(uploadFiles, file) if err := r.uploadFile(releaseID, file); err != nil {
} return err
for _, file := range uploadFiles {
handle, err := os.Open(file)
if err != nil {
return fmt.Errorf("failed to read %s artifact: %w", file, err)
} }
for _, attachment := range attachments {
if attachment.Name == path.Base(file) {
if _, err := rc.Client.DeleteReleaseAttachment(rc.Owner, rc.Repo, releaseID, attachment.ID); err != nil {
return fmt.Errorf("failed to delete %s artifact: %w", file, err)
}
log.Info().Msgf("successfully deleted old %s artifact", attachment.Name)
}
}
if _, _, err = rc.Client.CreateReleaseAttachment(rc.Owner, rc.Repo, releaseID, handle, path.Base(file)); err != nil {
return fmt.Errorf("failed to upload %s artifact: %w", file, err)
}
log.Info().Msgf("successfully uploaded %s artifact", file)
} }
return nil return nil
} }
func (r *GiteaRelease) uploadFile(releaseID int64, file string) error {
handle, err := os.Open(file)
if err != nil {
return fmt.Errorf("failed to read artifact: %s: %w", file, err)
}
defer handle.Close()
_, _, err = r.client.CreateReleaseAttachment(r.Opt.Owner, r.Opt.Repo, releaseID, handle, path.Base(file))
if err != nil {
return fmt.Errorf("failed to upload artifact: %s: %w", file, err)
}
log.Info().Msgf("uploaded artifact: %s", path.Base(file))
return nil
}

356
plugin/release_test.go Normal file
View File

@ -0,0 +1,356 @@
package plugin
import (
"bytes"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/rs/zerolog/log"
"code.gitea.io/sdk/gitea"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
)
func giteaMockHandler(t *testing.T, opt GiteaReleaseOpt) func(http.ResponseWriter, *http.Request) {
t.Helper()
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Println(r.RequestURI)
switch r.RequestURI {
case "/api/v1/version":
_, err := io.WriteString(w, `{"version":"1.21.0"}`)
if err != nil {
t.Fail()
}
case "/api/v1/repos/test-owner/test-repo/releases?limit=0&page=1":
_, err := io.WriteString(w, `[{
"id": 1,
"tag_name": "v1.0.0",
"name": "Release v1.0.0",
"body": "This is the release notes for v1.0.0",
"draft": false,
"prerelease": false,
"created_at": "2023-05-01T12:00:00Z",
"published_at": "2023-05-01T12:30:00Z"
}]`)
if err != nil {
t.Fail()
}
case "/api/v1/repos/test-owner/test-repo/releases":
_, err := io.WriteString(w, fmt.Sprintf(`{
"id": 1,
"tag_name": "%s",
"name": "Release %s",
"body": "This is the release notes for %s",
"draft": %t,
"prerelease": %t,
"created_at": "2023-05-01T12:00:00Z",
"published_at": "2023-05-01T12:30:00Z"
}`, opt.Tag, opt.Tag, opt.Tag, opt.Draft, opt.Prerelease))
if err != nil {
t.Fail()
}
case "/api/v1/repos/test-owner/test-repo/releases/1/assets?limit=0&page=1":
_, err := io.WriteString(w, `[{
"id": 1,
"name": "file1.txt",
"size": 1024,
"created_at": "2023-05-01T12:30:00Z"
}]`)
if err != nil {
t.Fail()
}
case "/api/v1/repos/test-owner/test-repo/releases/1/assets":
_, err := io.WriteString(w, `{
"id": 1,
"name": "file1.txt",
"size": 1024,
"created_at": "2023-05-01T12:30:00Z"
}`)
if err != nil {
t.Fail()
}
}
}
}
func TestGiteaReleaseFind(t *testing.T) {
tests := []struct {
name string
opt GiteaReleaseOpt
want *gitea.Release
wantErr error
}{
{
name: "find release by tag",
opt: GiteaReleaseOpt{
Owner: "test-owner",
Repo: "test-repo",
Tag: "v1.0.0",
},
want: &gitea.Release{
TagName: "v1.0.0",
},
},
{
name: "release not found",
opt: GiteaReleaseOpt{
Owner: "test-owner",
Repo: "test-repo",
Tag: "v1.1.0",
},
want: nil,
wantErr: ErrReleaseNotFound,
},
}
for _, tt := range tests {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
giteaMockHandler(t, tt.opt)(w, r)
}))
defer ts.Close()
g, _ := gitea.NewClient(ts.URL)
client := NewGiteaClient(g)
t.Run(tt.name, func(t *testing.T) {
client.Release.Opt = tt.opt
release, err := client.Release.Find()
if tt.want == nil {
assert.Error(t, err)
assert.Nil(t, release)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.want.TagName, release.TagName)
})
}
}
func TestGiteaReleaseCreate(t *testing.T) {
tests := []struct {
name string
opt GiteaReleaseOpt
want *gitea.Release
wantErr error
}{
{
name: "create release",
opt: GiteaReleaseOpt{
Owner: "test-owner",
Repo: "test-repo",
Tag: "v1.1.0",
Title: "Release v1.1.0",
Note: "This is the release notes for v1.1.0",
Draft: false,
Prerelease: false,
},
want: &gitea.Release{
TagName: "v1.1.0",
Title: "Release v1.1.0",
Note: "This is the release notes for v1.1.0",
IsDraft: false,
IsPrerelease: false,
},
},
{
name: "create draft release",
opt: GiteaReleaseOpt{
Owner: "test-owner",
Repo: "test-repo",
Tag: "v1.2.0",
Title: "Release v1.2.0",
Note: "This is the release notes for v1.2.0",
Draft: true,
Prerelease: false,
},
want: &gitea.Release{
TagName: "v1.2.0",
Title: "Release v1.2.0",
Note: "This is the release notes for v1.2.0",
IsDraft: true,
IsPrerelease: false,
},
},
{
name: "create prerelease",
opt: GiteaReleaseOpt{
Owner: "test-owner",
Repo: "test-repo",
Tag: "v1.3.0-rc1",
Title: "Release v1.3.0-rc1",
Note: "This is the release notes for v1.3.0-rc1",
Draft: false,
Prerelease: true,
},
want: &gitea.Release{
TagName: "v1.3.0-rc1",
Title: "Release v1.3.0-rc1",
Note: "This is the release notes for v1.3.0-rc1",
IsDraft: false,
IsPrerelease: true,
},
},
}
for _, tt := range tests {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
giteaMockHandler(t, tt.opt)(w, r)
}))
defer ts.Close()
g, _ := gitea.NewClient(ts.URL)
client := NewGiteaClient(g)
t.Run(tt.name, func(t *testing.T) {
client.Release.Opt = tt.opt
release, err := client.Release.Create()
if tt.wantErr != nil {
assert.Error(t, err)
assert.Nil(t, release)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.want.TagName, release.TagName)
assert.Equal(t, tt.want.Title, release.Title)
assert.Equal(t, tt.want.Note, release.Note)
assert.Equal(t, tt.want.IsDraft, release.IsDraft)
assert.Equal(t, tt.want.IsPrerelease, release.IsPrerelease)
})
}
}
func TestGiteaReleaseAddAttachments(t *testing.T) {
logBuffer := &bytes.Buffer{}
logger := zerolog.New(logBuffer)
log.Logger = logger
tests := []struct {
name string
opt GiteaReleaseOpt
files []string
fileExists string
wantErr error
wantLogs []string
}{
{
name: "add new attachments",
opt: GiteaReleaseOpt{
Owner: "test-owner",
Repo: "test-repo",
Tag: "v2.0.0",
Title: "Release v2.0.0",
FileExists: "overwrite",
},
files: []string{createTempFile(t, "file1.txt"), createTempFile(t, "file2.txt")},
wantLogs: []string{"uploaded artifact: file1.txt", "uploaded artifact: file2.txt"},
},
{
name: "fail on existing attachments",
opt: GiteaReleaseOpt{
Owner: "test-owner",
Repo: "test-repo",
Tag: "v2.0.0",
Title: "Release v2.0.0",
FileExists: "fail",
},
files: []string{createTempFile(t, "file1.txt"), createTempFile(t, "file2.txt")},
wantErr: ErrFileExists,
},
{
name: "overwrite on existing attachments",
opt: GiteaReleaseOpt{
Owner: "test-owner",
Repo: "test-repo",
Tag: "v2.0.0",
Title: "Release v2.0.0",
FileExists: "overwrite",
},
files: []string{createTempFile(t, "file1.txt"), createTempFile(t, "file2.txt")},
wantErr: nil,
wantLogs: []string{"deleted artifact: file1.txt", "uploaded artifact: file1.txt"},
},
{
name: "skip on existing attachments",
opt: GiteaReleaseOpt{
Owner: "test-owner",
Repo: "test-repo",
Tag: "v2.0.0",
Title: "Release v2.0.0",
FileExists: "skip",
},
files: []string{createTempFile(t, "file1.txt"), createTempFile(t, "file2.txt")},
wantErr: nil,
wantLogs: []string{"skip existing artifact: file1"},
},
{
name: "fail on invalid file",
opt: GiteaReleaseOpt{
Owner: "test-owner",
Repo: "test-repo",
Tag: "v2.0.0",
Title: "Release v2.0.0",
FileExists: "overwrite",
},
files: []string{"testdata/file1.txt", "testdata/invalid.txt"},
wantErr: errors.New("no such file or directory"),
},
}
for _, tt := range tests {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
giteaMockHandler(t, tt.opt)(w, r)
}))
defer ts.Close()
logBuffer.Reset()
g, _ := gitea.NewClient(ts.URL)
client := NewGiteaClient(g)
t.Run(tt.name, func(t *testing.T) {
client.Release.Opt = tt.opt
release, _ := client.Release.Create()
err := client.Release.AddAttachments(release.ID, tt.files)
// Assert log output.
for _, l := range tt.wantLogs {
assert.Contains(t, logBuffer.String(), l)
}
if tt.wantErr != nil {
assert.Error(t, err)
assert.ErrorContains(t, err, tt.wantErr.Error())
return
}
assert.NoError(t, err)
})
}
}
func createTempFile(t *testing.T, name string) string {
t.Helper()
name = filepath.Join(t.TempDir(), name)
_ = os.WriteFile(name, []byte("hello"), 0o600)
return name
}