mirror of
https://github.com/Xevion/glance.git
synced 2025-12-15 08:11:59 -06:00
Merge pull request #165 from Fumesover/release/v0.6.0
releases: Add support for gitlab
This commit is contained in:
58
internal/feed/dockerhub.go
Normal file
58
internal/feed/dockerhub.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package feed
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type dockerHubRepositoryTagsResponse struct {
|
||||
Results []struct {
|
||||
Name string `json:"name"`
|
||||
LastPushed string `json:"tag_last_pushed"`
|
||||
} `json:"results"`
|
||||
}
|
||||
|
||||
const dockerHubReleaseNotesURLFormat = "https://hub.docker.com/r/%s/tags?name=%s"
|
||||
|
||||
func fetchLatestDockerHubRelease(request *ReleaseRequest) (*AppRelease, error) {
|
||||
parts := strings.Split(request.Repository, "/")
|
||||
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("invalid repository name: %s", request.Repository)
|
||||
}
|
||||
|
||||
httpRequest, err := http.NewRequest(
|
||||
"GET",
|
||||
fmt.Sprintf("https://hub.docker.com/v2/namespaces/%s/repositories/%s/tags", parts[0], parts[1]),
|
||||
nil,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if request.Token != nil {
|
||||
httpRequest.Header.Add("Authorization", "Bearer "+(*request.Token))
|
||||
}
|
||||
|
||||
response, err := decodeJsonFromRequest[dockerHubRepositoryTagsResponse](defaultClient, httpRequest)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(response.Results) == 0 {
|
||||
return nil, fmt.Errorf("no tags found for repository: %s", request.Repository)
|
||||
}
|
||||
|
||||
tag := response.Results[0]
|
||||
|
||||
return &AppRelease{
|
||||
Source: ReleaseSourceDockerHub,
|
||||
NotesUrl: fmt.Sprintf(dockerHubReleaseNotesURLFormat, request.Repository, tag.Name),
|
||||
Name: request.Repository,
|
||||
Version: tag.Name,
|
||||
TimeReleased: parseRFC3339Time(tag.LastPushed),
|
||||
}, nil
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package feed
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -17,85 +16,41 @@ type githubReleaseLatestResponseJson struct {
|
||||
} `json:"reactions"`
|
||||
}
|
||||
|
||||
func parseGithubTime(t string) time.Time {
|
||||
parsedTime, err := time.Parse("2006-01-02T15:04:05Z", t)
|
||||
|
||||
if err != nil {
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
return parsedTime
|
||||
}
|
||||
|
||||
func FetchLatestReleasesFromGithub(repositories []string, token string) (AppReleases, error) {
|
||||
appReleases := make(AppReleases, 0, len(repositories))
|
||||
|
||||
if len(repositories) == 0 {
|
||||
return appReleases, nil
|
||||
}
|
||||
|
||||
requests := make([]*http.Request, len(repositories))
|
||||
|
||||
for i, repository := range repositories {
|
||||
request, _ := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", repository), nil)
|
||||
|
||||
if token != "" {
|
||||
request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
}
|
||||
|
||||
requests[i] = request
|
||||
}
|
||||
|
||||
task := decodeJsonFromRequestTask[githubReleaseLatestResponseJson](defaultClient)
|
||||
job := newJob(task, requests).withWorkers(15)
|
||||
responses, errs, err := workerPoolDo(job)
|
||||
func fetchLatestGithubRelease(request *ReleaseRequest) (*AppRelease, error) {
|
||||
httpRequest, err := http.NewRequest(
|
||||
"GET",
|
||||
fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", request.Repository),
|
||||
nil,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var failed int
|
||||
|
||||
for i := range responses {
|
||||
if errs[i] != nil {
|
||||
failed++
|
||||
slog.Error("Failed to fetch or parse github release", "error", errs[i], "url", requests[i].URL)
|
||||
continue
|
||||
}
|
||||
|
||||
liveRelease := &responses[i]
|
||||
|
||||
if liveRelease == nil {
|
||||
slog.Error("No live release found", "repository", repositories[i], "url", requests[i].URL)
|
||||
continue
|
||||
}
|
||||
|
||||
version := liveRelease.TagName
|
||||
|
||||
if version[0] != 'v' {
|
||||
version = "v" + version
|
||||
}
|
||||
|
||||
appReleases = append(appReleases, AppRelease{
|
||||
Name: repositories[i],
|
||||
Version: version,
|
||||
NotesUrl: liveRelease.HtmlUrl,
|
||||
TimeReleased: parseGithubTime(liveRelease.PublishedAt),
|
||||
Downvotes: liveRelease.Reactions.Downvotes,
|
||||
})
|
||||
if request.Token != nil {
|
||||
httpRequest.Header.Add("Authorization", "Bearer "+(*request.Token))
|
||||
}
|
||||
|
||||
if len(appReleases) == 0 {
|
||||
return nil, ErrNoContent
|
||||
response, err := decodeJsonFromRequest[githubReleaseLatestResponseJson](defaultClient, httpRequest)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
appReleases.SortByNewest()
|
||||
version := response.TagName
|
||||
|
||||
if failed > 0 {
|
||||
return appReleases, fmt.Errorf("%w: could not get %d releases", ErrPartialContent, failed)
|
||||
if len(version) > 0 && version[0] != 'v' {
|
||||
version = "v" + version
|
||||
}
|
||||
|
||||
return appReleases, nil
|
||||
return &AppRelease{
|
||||
Source: ReleaseSourceGithub,
|
||||
Name: request.Repository,
|
||||
Version: version,
|
||||
NotesUrl: response.HtmlUrl,
|
||||
TimeReleased: parseRFC3339Time(response.PublishedAt),
|
||||
Downvotes: response.Reactions.Downvotes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type GithubTicket struct {
|
||||
@@ -201,7 +156,7 @@ func FetchRepositoryDetailsFromGithub(repository string, token string, maxPRs in
|
||||
for i := range PRsResponse.Tickets {
|
||||
details.PullRequests = append(details.PullRequests, GithubTicket{
|
||||
Number: PRsResponse.Tickets[i].Number,
|
||||
CreatedAt: parseGithubTime(PRsResponse.Tickets[i].CreatedAt),
|
||||
CreatedAt: parseRFC3339Time(PRsResponse.Tickets[i].CreatedAt),
|
||||
Title: PRsResponse.Tickets[i].Title,
|
||||
})
|
||||
}
|
||||
@@ -218,7 +173,7 @@ func FetchRepositoryDetailsFromGithub(repository string, token string, maxPRs in
|
||||
for i := range issuesResponse.Tickets {
|
||||
details.Issues = append(details.Issues, GithubTicket{
|
||||
Number: issuesResponse.Tickets[i].Number,
|
||||
CreatedAt: parseGithubTime(issuesResponse.Tickets[i].CreatedAt),
|
||||
CreatedAt: parseRFC3339Time(issuesResponse.Tickets[i].CreatedAt),
|
||||
Title: issuesResponse.Tickets[i].Title,
|
||||
})
|
||||
}
|
||||
|
||||
54
internal/feed/gitlab.go
Normal file
54
internal/feed/gitlab.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package feed
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type gitlabReleaseResponseJson struct {
|
||||
TagName string `json:"tag_name"`
|
||||
ReleasedAt string `json:"released_at"`
|
||||
Links struct {
|
||||
Self string `json:"self"`
|
||||
} `json:"_links"`
|
||||
}
|
||||
|
||||
func fetchLatestGitLabRelease(request *ReleaseRequest) (*AppRelease, error) {
|
||||
httpRequest, err := http.NewRequest(
|
||||
"GET",
|
||||
fmt.Sprintf(
|
||||
"https://gitlab.com/api/v4/projects/%s/releases/permalink/latest",
|
||||
url.QueryEscape(request.Repository),
|
||||
),
|
||||
nil,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if request.Token != nil {
|
||||
httpRequest.Header.Add("PRIVATE-TOKEN", *request.Token)
|
||||
}
|
||||
|
||||
response, err := decodeJsonFromRequest[gitlabReleaseResponseJson](defaultClient, httpRequest)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
version := response.TagName
|
||||
|
||||
if len(version) > 0 && version[0] != 'v' {
|
||||
version = "v" + version
|
||||
}
|
||||
|
||||
return &AppRelease{
|
||||
Source: ReleaseSourceGitlab,
|
||||
Name: request.Repository,
|
||||
Version: version,
|
||||
NotesUrl: response.Links.Self,
|
||||
TimeReleased: parseRFC3339Time(response.ReleasedAt),
|
||||
}, nil
|
||||
}
|
||||
@@ -41,6 +41,7 @@ type Weather struct {
|
||||
}
|
||||
|
||||
type AppRelease struct {
|
||||
Source ReleaseSource
|
||||
Name string
|
||||
Version string
|
||||
NotesUrl string
|
||||
|
||||
69
internal/feed/releases.go
Normal file
69
internal/feed/releases.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package feed
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
type ReleaseSource string
|
||||
|
||||
const (
|
||||
ReleaseSourceGithub ReleaseSource = "github"
|
||||
ReleaseSourceGitlab ReleaseSource = "gitlab"
|
||||
ReleaseSourceDockerHub ReleaseSource = "dockerhub"
|
||||
)
|
||||
|
||||
type ReleaseRequest struct {
|
||||
Source ReleaseSource
|
||||
Repository string
|
||||
Token *string
|
||||
}
|
||||
|
||||
func FetchLatestReleases(requests []*ReleaseRequest) (AppReleases, error) {
|
||||
job := newJob(fetchLatestReleaseTask, requests).withWorkers(20)
|
||||
results, errs, err := workerPoolDo(job)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var failed int
|
||||
|
||||
releases := make(AppReleases, 0, len(requests))
|
||||
|
||||
for i := range results {
|
||||
if errs[i] != nil {
|
||||
failed++
|
||||
slog.Error("Failed to fetch release", "source", requests[i].Source, "repository", requests[i].Repository, "error", errs[i])
|
||||
continue
|
||||
}
|
||||
|
||||
releases = append(releases, *results[i])
|
||||
}
|
||||
|
||||
if failed == len(requests) {
|
||||
return nil, ErrNoContent
|
||||
}
|
||||
|
||||
releases.SortByNewest()
|
||||
|
||||
if failed > 0 {
|
||||
return releases, fmt.Errorf("%w: could not get %d releases", ErrPartialContent, failed)
|
||||
}
|
||||
|
||||
return releases, nil
|
||||
}
|
||||
|
||||
func fetchLatestReleaseTask(request *ReleaseRequest) (*AppRelease, error) {
|
||||
switch request.Source {
|
||||
case ReleaseSourceGithub:
|
||||
return fetchLatestGithubRelease(request)
|
||||
case ReleaseSourceGitlab:
|
||||
return fetchLatestGitLabRelease(request)
|
||||
case ReleaseSourceDockerHub:
|
||||
return fetchLatestDockerHubRelease(request)
|
||||
}
|
||||
|
||||
return nil, errors.New("unsupported source")
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -79,7 +80,6 @@ func maybeCopySliceWithoutZeroValues[T int | float64](values []T) []T {
|
||||
return values
|
||||
}
|
||||
|
||||
|
||||
var urlSchemePattern = regexp.MustCompile(`^[a-z]+:\/\/`)
|
||||
|
||||
func stripURLScheme(url string) string {
|
||||
@@ -95,3 +95,13 @@ func limitStringLength(s string, max int) (string, bool) {
|
||||
|
||||
return s, false
|
||||
}
|
||||
|
||||
func parseRFC3339Time(t string) time.Time {
|
||||
parsed, err := time.Parse(time.RFC3339, t)
|
||||
|
||||
if err != nil {
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
return parsed
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user