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  }