feat: added starred repositories option for releases widget

This commit is contained in:
Benjamin Jasper 2024-05-14 21:34:56 +02:00
parent 7743664527
commit 95e7f780fc
3 changed files with 185 additions and 12 deletions

View File

@ -768,17 +768,23 @@ Optional URL to an image which will be used as the icon for the site. Can be an
Whether to open the link in the same or a new tab.
### Releases
Display a list of releases for specific repositories on Github. Draft releases and prereleases will not be shown.
Display a list of releases for specific repositories or for your starred repositories on GitHub. Draft releases and prereleases will not be shown.
Example:
```yaml
# You can specify a list of repositories
- type: releases
repositories:
- immich-app/immich
- go-gitea/gitea
- dani-garcia/vaultwarden
- jellyfin/jellyfin
# Or you can use your starred repositories
- type: releases
starred: true
token: your-github-token
```
Preview:
@ -789,14 +795,20 @@ Preview:
| Name | Type | Required | Default |
| ---- | ---- | -------- | ------- |
| repositories | array | yes | |
| repositories | array | no | [] |
| starred | bool | no | false |
| token | string | no | |
| limit | integer | no | 10 |
| collapse-after | integer | no | 5 |
| releases-search-limit | integer | no | 10 |
##### `repositories`
A list of repositores for which to fetch the latest release for. Only the name/repo is required, not the full URL.
##### `starred`
When set to `true` it will fetch the latest releases from all of your starred repositories. Depending on the number of repositories you have starred, this can have an effect on the loading time.
When set to true, you must also set the `token` property, as the starred repositories list is personalized to the user.
##### `token`
Without authentication Github allows for up to 60 requests per hour. You can easily exceed this limit and start seeing errors if you're tracking lots of repositories or your cache time is low. To circumvent this you can [create a read only token from your Github account](https://github.com/settings/personal-access-tokens/new) and provide it here.
@ -823,8 +835,12 @@ This way you can safely check your `glance.yml` in version control without expos
##### `limit`
The maximum number of releases to show.
#### `collapse-after`
##### `collapse-after`
How many releases are visible before the "SHOW MORE" button appears. Set to `-1` to never collapse.
##### `releases-search-limit`
This is the number of releases Glance will fetch for each repository until it finds the first release that is not a draft or prerelease.
You may decrease this value, to improve performance, at the risk of missing some releases.
### Repository
Display general information about a repository as well as a list of the latest open pull requests and issues.

View File

@ -1,6 +1,8 @@
package feed
import (
"bytes"
"encoding/json"
"fmt"
"log/slog"
"net/http"
@ -19,6 +21,38 @@ type githubReleaseResponseJson struct {
} `json:"reactions"`
}
type starredRepositoriesResponseJson struct {
Errors []struct {
Message string `json:"message"`
} `json:"errors"`
Data struct {
Viewer struct {
StarredRepositories struct {
PageInfo struct {
HasNextPage bool `json:"hasNextPage"`
EndCursor string `json:"endCursor"`
} `json:"pageInfo"`
Nodes []struct {
NameWithOwner string `json:"nameWithOwner"`
Releases struct {
Nodes []struct {
Name string `json:"name"`
URL string `json:"url"`
IsDraft bool `json:"isDraft"`
IsPrerelease bool `json:"isPrerelease"`
PublishedAt string `json:"publishedAt"`
TagName string `json:"tagName"`
Reactions struct {
TotalCount int `json:"totalCount"`
} `json:"reactions"`
} `json:"nodes"`
} `json:"releases"`
} `json:"nodes"`
} `json:"starredRepositories"`
} `json:"viewer"`
} `json:"data"`
}
func parseGithubTime(t string) time.Time {
parsedTime, err := time.Parse("2006-01-02T15:04:05Z", t)
@ -29,7 +63,117 @@ func parseGithubTime(t string) time.Time {
return parsedTime
}
func FetchLatestReleasesFromGithub(repositories []string, token string) (AppReleases, error) {
func FetchStarredRepositoriesReleasesFromGithub(token string, maxReleases int) (AppReleases, error) {
if token == "" {
return nil, fmt.Errorf("%w: no github token provided", ErrNoContent)
}
afterCursor := ""
releases := make(AppReleases, 0, 10)
graphqlClient := http.Client{
Timeout: time.Second * 10,
}
for true {
graphQLQuery := fmt.Sprintf(`query StarredReleases {
viewer {
starredRepositories(first: 50, after: "%s") {
pageInfo {
hasNextPage
endCursor
}
nodes {
nameWithOwner
releases(first: %d, orderBy: {field: CREATED_AT, direction: DESC}) {
nodes {
name
url
publishedAt
tagName
url
isDraft
isPrerelease
reactions {
totalCount
}
}
}
}
}
}
}`, afterCursor, maxReleases)
jsonBody := map[string]string{
"query": graphQLQuery,
}
requestBody, err := json.Marshal(jsonBody)
if err != nil {
return nil, fmt.Errorf("%w: could not marshal request body: %s", ErrNoContent, err)
}
request, err := http.NewRequest("POST", "https://api.github.com/graphql", bytes.NewBuffer(requestBody))
if err != nil {
return nil, fmt.Errorf("%w: could not create request", err)
}
request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
response, err := decodeJsonFromRequest[starredRepositoriesResponseJson](&graphqlClient, request)
if err != nil {
return nil, fmt.Errorf("%w: could not get starred releases: %s", ErrNoContent, err)
}
if (response.Errors != nil) && (len(response.Errors) > 0) {
return nil, fmt.Errorf("%w: could not get starred releases: %s", ErrNoContent, response.Errors[0].Message)
}
for _, repository := range response.Data.Viewer.StarredRepositories.Nodes {
for _, release := range repository.Releases.Nodes {
if release.IsDraft || release.IsPrerelease {
continue
}
version := release.TagName
if version[0] != 'v' {
version = "v" + version
}
releases = append(releases, AppRelease{
Name: repository.NameWithOwner,
Version: version,
NotesUrl: release.URL,
TimeReleased: parseGithubTime(release.PublishedAt),
Downvotes: release.Reactions.TotalCount,
})
break
}
}
afterCursor = response.Data.Viewer.StarredRepositories.PageInfo.EndCursor
if !response.Data.Viewer.StarredRepositories.PageInfo.HasNextPage {
break
}
}
if len(releases) == 0 {
return nil, ErrNoContent
}
releases.SortByNewest()
return releases, nil
}
func FetchLatestReleasesFromGithub(repositories []string, token string, maxReleases int) (AppReleases, error) {
appReleases := make(AppReleases, 0, len(repositories))
if len(repositories) == 0 {
@ -39,7 +183,7 @@ func FetchLatestReleasesFromGithub(repositories []string, token string) (AppRele
requests := make([]*http.Request, len(repositories))
for i, repository := range repositories {
request, _ := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/repos/%s/releases?per_page=10", repository), nil)
request, _ := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/repos/%s/releases?per_page=%d", repository, maxReleases), nil)
if token != "" {
request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))

View File

@ -10,12 +10,14 @@ import (
)
type Releases struct {
widgetBase `yaml:",inline"`
Releases feed.AppReleases `yaml:"-"`
Repositories []string `yaml:"repositories"`
Token OptionalEnvString `yaml:"token"`
Limit int `yaml:"limit"`
CollapseAfter int `yaml:"collapse-after"`
widgetBase `yaml:",inline"`
Releases feed.AppReleases `yaml:"-"`
Repositories []string `yaml:"repositories"`
Token OptionalEnvString `yaml:"token"`
Limit int `yaml:"limit"`
CollapseAfter int `yaml:"collapse-after"`
ReleasesSearchLimit int `yaml:"releases-search-limit"`
Starred bool `yaml:"starred"`
}
func (widget *Releases) Initialize() error {
@ -33,7 +35,18 @@ func (widget *Releases) Initialize() error {
}
func (widget *Releases) Update(ctx context.Context) {
releases, err := feed.FetchLatestReleasesFromGithub(widget.Repositories, string(widget.Token))
var err error
var releases []feed.AppRelease
if widget.ReleasesSearchLimit <= 0 {
widget.ReleasesSearchLimit = 10
}
if widget.Starred {
releases, err = feed.FetchStarredRepositoriesReleasesFromGithub(string(widget.Token), widget.ReleasesSearchLimit)
} else {
releases, err = feed.FetchLatestReleasesFromGithub(widget.Repositories, string(widget.Token), widget.ReleasesSearchLimit)
}
if !widget.canContinueUpdateAfterHandlingErr(err) {
return