github.com/web-platform-tests/wpt.fyi@v0.0.0-20240530210107-70cf978996f1/api/manifest/api.go (about) 1 // Copyright 2018 The WPT Dashboard Project. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 //go:generate mockgen -destination mock_manifest/api_mock.go github.com/web-platform-tests/wpt.fyi/api/manifest API 6 7 package manifest 8 9 import ( 10 "context" 11 "fmt" 12 "io" 13 "regexp" 14 "time" 15 16 "github.com/google/go-github/v47/github" 17 "github.com/web-platform-tests/wpt.fyi/shared" 18 ) 19 20 // AssetRegex is the pattern for a valid manifest filename. 21 // The full sha is captured in group 1. 22 var AssetRegex = regexp.MustCompile(`^MANIFEST-([0-9a-fA-F]{40}).json.gz$`) 23 24 // API handles manifest-related fetches and caching. 25 type API interface { 26 GetManifestForSHA(string) (string, []byte, error) 27 NewRedis(duration time.Duration) shared.ReadWritable 28 } 29 30 type apiImpl struct { 31 ctx context.Context // nolint:containedctx // TODO: Fix containedctx lint error 32 } 33 34 // NewAPI returns an API implementation for the given context. 35 // nolint:ireturn // TODO: Fix ireturn lint error 36 func NewAPI(ctx context.Context) API { 37 return apiImpl{ 38 ctx: ctx, 39 } 40 } 41 42 // GetManifestForSHA loads the (gzipped) contents of the manifest JSON for the release associated 43 // with the given SHA, if any. 44 func (a apiImpl) GetManifestForSHA(sha string) (fetchedSHA string, manifest []byte, err error) { 45 aeAPI := shared.NewAppEngineAPI(a.ctx) 46 fetchedSHA, body, err := getGitHubReleaseAssetForSHA(aeAPI, sha) 47 if err != nil { 48 return fetchedSHA, nil, err 49 } 50 data, err := io.ReadAll(body) 51 if err != nil { 52 return fetchedSHA, nil, err 53 } 54 55 return fetchedSHA, data, err 56 } 57 58 // getGitHubReleaseAssetForSHA gets the bytes for the SHA's release's manifest json gzip asset. 59 // This is done using a few hops on the GitHub API, so should be cached afterward. 60 func getGitHubReleaseAssetForSHA(aeAPI shared.AppEngineAPI, sha string) ( 61 fetchedSHA string, 62 manifest io.Reader, 63 err error, 64 ) { 65 client, err := aeAPI.GetGitHubClient() 66 if err != nil { 67 return "", nil, err 68 } 69 var release *github.RepositoryRelease 70 releaseTag := "latest" 71 if shared.IsLatest(sha) { 72 // Use GitHub's API for latest release. 73 release, _, err = client.Repositories.GetLatestRelease(aeAPI.Context(), shared.WPTRepoOwner, shared.WPTRepoName) 74 } else { 75 q := fmt.Sprintf("SHA:%s repo:web-platform-tests/wpt", sha) 76 var issues *github.IssuesSearchResult 77 issues, _, err = client.Search.Issues(aeAPI.Context(), q, nil) 78 if err != nil { 79 return "", nil, err 80 } 81 if issues == nil || len(issues.Issues) < 1 { 82 return "", nil, fmt.Errorf("No search results found for SHA %s", sha) 83 } 84 85 releaseTag = fmt.Sprintf("merge_pr_%d", issues.Issues[0].GetNumber()) 86 release, _, err = client.Repositories.GetReleaseByTag( 87 aeAPI.Context(), 88 shared.WPTRepoOwner, 89 shared.WPTRepoName, 90 releaseTag, 91 ) 92 if err != nil { 93 // nolint: godox // TODO: golangci-lint discovered that this error was being shadowed. 94 // Review if we should actually return the error. In the meantime, ignore it. 95 log := shared.GetLogger(aeAPI.Context()) 96 log.Warningf("GetReleaseByTag failed with error %w. Will ignore", err) 97 err = nil 98 } 99 } 100 101 if err != nil { 102 return "", nil, err 103 } else if release == nil || len(release.Assets) < 1 { 104 return "", nil, fmt.Errorf("No assets found for %s release", releaseTag) 105 } 106 // Get (and unzip) the asset with name "MANIFEST-{sha}.json.gz" 107 for _, asset := range release.Assets { 108 name := asset.GetName() 109 var url string 110 if matches := AssetRegex.FindStringSubmatch(name); matches != nil { 111 fetchedSHA = matches[1] 112 url = asset.GetBrowserDownloadURL() 113 114 client := aeAPI.GetHTTPClient() 115 resp, err := client.Get(url) 116 if err != nil { 117 return fetchedSHA, nil, err 118 } 119 120 return fetchedSHA, resp.Body, err 121 } 122 } 123 124 return "", nil, fmt.Errorf("No manifest asset found for release %s", releaseTag) 125 } 126 127 // NewRedis creates a new redisReadWritable with the given duration. 128 // nolint:ireturn // TODO: Fix ireturn lint error 129 func (a apiImpl) NewRedis(duration time.Duration) shared.ReadWritable { 130 return shared.NewRedisReadWritable(a.ctx, duration) 131 }