Skip to content

Commit f404fc1

Browse files
feat(downloads): new endpoint that redirects to the latest semantic-release binary
1 parent 406c28b commit f404fc1

File tree

5 files changed

+121
-30
lines changed

5 files changed

+121
-30
lines changed

internal/server/cache.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type (
1616
const (
1717
cacheKeyPrefixBatchRequest cacheKeyPrefix = "batch"
1818
cacheKeyPrefixRequest cacheKeyPrefix = "request"
19+
cacheKeyPrefixGitHub cacheKeyPrefix = "github"
1920
)
2021

2122
func (s *Server) getCacheKeyFromRequest(r *http.Request) cacheKey {
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package server
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"strings"
8+
9+
"github.com/go-chi/chi/v5"
10+
"github.com/google/go-github/v50/github"
11+
)
12+
13+
func (s *Server) getLatestSemRelRelease(ctx context.Context) (*github.RepositoryRelease, error) {
14+
semrelCacheKey := s.getCacheKeyWithPrefix(cacheKeyPrefixGitHub, "semantic-release/latest")
15+
cachedLatestRelease, ok := s.getFromCache(semrelCacheKey)
16+
if ok {
17+
return cachedLatestRelease.(*github.RepositoryRelease), nil
18+
}
19+
20+
err := s.ghSemaphore.Acquire(ctx, 1)
21+
if err != nil {
22+
return nil, fmt.Errorf("could not acquire semaphore")
23+
}
24+
defer s.ghSemaphore.Release(1)
25+
26+
latestRelease, _, err := s.ghClient.Repositories.GetLatestRelease(ctx, "go-semantic-release", "semantic-release")
27+
if err != nil {
28+
return nil, err
29+
}
30+
s.setInCache(semrelCacheKey, latestRelease)
31+
return latestRelease, nil
32+
}
33+
34+
func (s *Server) downloadLatestSemRelBinary(w http.ResponseWriter, r *http.Request) {
35+
os := chi.URLParam(r, "os")
36+
arch := chi.URLParam(r, "arch")
37+
if os == "" || arch == "" {
38+
s.writeJSONError(w, r, http.StatusBadRequest, fmt.Errorf("missing os or arch"))
39+
return
40+
}
41+
42+
latestRelease, err := s.getLatestSemRelRelease(r.Context())
43+
if err != nil {
44+
s.writeJSONError(w, r, http.StatusInternalServerError, err, "could not get latest release")
45+
return
46+
}
47+
48+
osArchIdentifier := strings.ToLower(fmt.Sprintf("%s_%s", os, arch))
49+
for _, asset := range latestRelease.Assets {
50+
if strings.Contains(asset.GetName(), osArchIdentifier) {
51+
http.Redirect(w, r, asset.GetBrowserDownloadURL(), http.StatusFound)
52+
return
53+
}
54+
}
55+
s.writeJSONError(w, r, http.StatusNotFound, fmt.Errorf("could not find binary for %s/%s", os, arch))
56+
}

internal/server/handlers.go

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,26 +33,32 @@ func (s *Server) updateAllPlugins(w http.ResponseWriter, r *http.Request) {
3333
return
3434
}
3535
}
36+
3637
s.invalidateByPrefix(s.getCacheKeyPrefixFromPluginName(""))
3738
s.writeJSON(w, map[string]bool{"ok": true})
3839
}
3940

4041
func (s *Server) updatePlugin(w http.ResponseWriter, r *http.Request) {
41-
err := s.ghSemaphore.Acquire(r.Context(), 1)
42-
if err != nil {
43-
s.writeJSONError(w, r, http.StatusTooManyRequests, err, "could not acquire semaphore")
44-
return
45-
}
46-
defer s.ghSemaphore.Release(1)
47-
4842
pluginVersion := chi.URLParam(r, "version")
4943
pluginName := chi.URLParam(r, "plugin")
44+
if pluginName == "" {
45+
s.writeJSONError(w, r, http.StatusBadRequest, fmt.Errorf("plugin name is missing"))
46+
return
47+
}
5048
p := config.Plugins.Find(pluginName)
5149
if p == nil {
52-
s.writeJSONError(w, r, 404, fmt.Errorf("plugin %s not found", pluginName))
50+
s.writeJSONError(w, r, http.StatusNotFound, fmt.Errorf("plugin %s not found", pluginName))
5351
return
5452
}
5553
s.log.Infof("updating plugin %s@%s", p.GetFullName(), pluginVersion)
54+
55+
err := s.ghSemaphore.Acquire(r.Context(), 1)
56+
if err != nil {
57+
s.writeJSONError(w, r, http.StatusTooManyRequests, err, "could not acquire semaphore")
58+
return
59+
}
60+
defer s.ghSemaphore.Release(1)
61+
5662
if err := p.Update(r.Context(), s.db, s.ghClient, pluginVersion); err != nil {
5763
s.writeJSONError(w, r, http.StatusInternalServerError, err, "could not update plugin")
5864
return
@@ -65,9 +71,13 @@ func (s *Server) updatePlugin(w http.ResponseWriter, r *http.Request) {
6571
func (s *Server) getPlugin(w http.ResponseWriter, r *http.Request) {
6672
pluginVersion := chi.URLParam(r, "version")
6773
pluginName := chi.URLParam(r, "plugin")
74+
if pluginName == "" {
75+
s.writeJSONError(w, r, http.StatusBadRequest, fmt.Errorf("plugin name is missing"))
76+
return
77+
}
6878
p := config.Plugins.Find(pluginName)
6979
if p == nil {
70-
s.writeJSONError(w, r, 404, fmt.Errorf("plugin %s not found", pluginName))
80+
s.writeJSONError(w, r, http.StatusNotFound, fmt.Errorf("plugin %s not found", pluginName))
7181
return
7282
}
7383
var err error
@@ -89,7 +99,7 @@ func (s *Server) listPluginVersions(w http.ResponseWriter, r *http.Request) {
8999
pluginName := chi.URLParam(r, "plugin")
90100
p := config.Plugins.Find(pluginName)
91101
if p == nil {
92-
s.writeJSONError(w, r, 404, fmt.Errorf("plugin %s not found", pluginName))
102+
s.writeJSONError(w, r, http.StatusNotFound, fmt.Errorf("plugin %s not found", pluginName))
93103
return
94104
}
95105

internal/server/handlers_test.go

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,16 @@ import (
3030
)
3131

3232
func newGitHubClient() *github.Client {
33+
exampleAssets := []*github.ReleaseAsset{
34+
{Name: github.String("test_linux_amd64"), BrowserDownloadURL: github.String("https://example.com/test_linux_amd64")},
35+
}
3336
mockedHTTPClient := mock.NewMockedHTTPClient(
3437
mock.WithRequestMatch(
3538
mock.GetReposReleasesByOwnerByRepo,
3639
[]*github.RepositoryRelease{
37-
{Draft: github.Bool(false), TagName: github.String("v1.0.0"), Assets: []*github.ReleaseAsset{{}}},
38-
{Draft: github.Bool(false), TagName: github.String("v1.0.1"), Assets: []*github.ReleaseAsset{{}}},
39-
{Draft: github.Bool(true), TagName: github.String("v2.0.0-beta"), Assets: []*github.ReleaseAsset{{}}},
40+
{Draft: github.Bool(false), TagName: github.String("v1.0.0"), Assets: exampleAssets},
41+
{Draft: github.Bool(false), TagName: github.String("v1.0.1"), Assets: exampleAssets},
42+
{Draft: github.Bool(true), TagName: github.String("v2.0.0-beta"), Assets: exampleAssets},
4043
{
4144
Draft: github.Bool(false),
4245
TagName: github.String("v2.0.0"),
@@ -57,11 +60,15 @@ func newGitHubClient() *github.Client {
5760
mock.GetReposReleasesLatestByOwnerByRepo,
5861
&github.RepositoryRelease{
5962
TagName: github.String("v2.0.0"),
60-
Assets: []*github.ReleaseAsset{{}},
63+
Assets: exampleAssets,
6164
},
6265
&github.RepositoryRelease{
6366
TagName: github.String("v3.0.0"),
64-
Assets: []*github.ReleaseAsset{{}},
67+
Assets: exampleAssets,
68+
},
69+
&github.RepositoryRelease{
70+
TagName: github.String("v3.0.0"),
71+
Assets: exampleAssets,
6572
},
6673
),
6774
mock.WithRequestMatch(
@@ -423,3 +430,12 @@ func TestBatchEndpointBadRequests(t *testing.T) {
423430
require.Equal(t, http.StatusBadRequest, rr.Code)
424431
require.Contains(t, decodeError(t, rr.Body.Bytes()), "could not resolve")
425432
}
433+
434+
func TestDownloadLatestSemRel(t *testing.T) {
435+
s, _, closeFn := newTestServer(t)
436+
defer closeFn()
437+
438+
rr := sendRequest(s, "GET", "/downloads/linux/amd64/semantic-release", nil)
439+
require.Equal(t, http.StatusFound, rr.Code)
440+
require.Equal(t, "https://example.com/test_linux_amd64", rr.Header().Get("Location"))
441+
}

internal/server/server.go

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,26 @@ func (s *Server) indexHandler(w http.ResponseWriter, r *http.Request) {
5050
})
5151
}
5252

53+
func (s *Server) apiV2Routes(r chi.Router) {
54+
r.Route("/plugins", func(r chi.Router) {
55+
r.With(s.cacheMiddleware).Group(func(r chi.Router) {
56+
r.Get("/", s.listPlugins)
57+
r.Get("/{plugin}", s.getPlugin)
58+
r.Get("/{plugin}/versions", s.listPluginVersions)
59+
r.Get("/{plugin}/versions/{version}", s.getPlugin)
60+
})
61+
62+
r.Post("/_batch", s.batchGetPlugins)
63+
64+
// routes to update the plugin index
65+
r.With(s.authMiddleware).Group(func(r chi.Router) {
66+
r.Put("/", s.updateAllPlugins)
67+
r.Put("/{plugin}", s.updatePlugin)
68+
r.Put("/{plugin}/versions/{version}", s.updatePlugin)
69+
})
70+
})
71+
}
72+
5373
func New(log *logrus.Logger, db *firestore.Client, ghClient *github.Client, storage *s3.Client, serverCfg *config.ServerConfig) *Server {
5474
router := chi.NewRouter()
5575
server := &Server{
@@ -79,22 +99,10 @@ func New(log *logrus.Logger, db *firestore.Client, ghClient *github.Client, stor
7999
// serve legacy API
80100
router.Handle("/api/v1/*", http.StripPrefix("/api/v1/", http.FileServer(http.FS(legacyV1.PluginIndex))))
81101

82-
router.Route("/api/v2/plugins", func(r chi.Router) {
83-
r.With(server.cacheMiddleware).Group(func(r chi.Router) {
84-
r.Get("/", server.listPlugins)
85-
r.Get("/{plugin}", server.getPlugin)
86-
r.Get("/{plugin}/versions", server.listPluginVersions)
87-
r.Get("/{plugin}/versions/{version}", server.getPlugin)
88-
})
89-
r.Post("/_batch", server.batchGetPlugins)
102+
router.Route("/api/v2", server.apiV2Routes)
90103

91-
// routes to update the plugin index
92-
r.With(server.authMiddleware).Group(func(r chi.Router) {
93-
r.Put("/", server.updateAllPlugins)
94-
r.Put("/{plugin}", server.updatePlugin)
95-
r.Put("/{plugin}/versions/{version}", server.updatePlugin)
96-
})
97-
})
104+
// downloads route
105+
router.Get("/downloads/{os}/{arch}/semantic-release", server.downloadLatestSemRelBinary)
98106

99107
return server
100108
}

0 commit comments

Comments
 (0)