func (build Build) Finished(ctx context.Context, opener Opener) (*Finished, error) {
	path, err := build.Path.ResolveReference(&url.URL{Path: "finished.json"})
	if err != nil {
		return nil, fmt.Errorf("resolve: %w", err)
	}
	var finished Finished
	err = readJSON(ctx, opener, *path, &finished)
	if errors.Is(err, storage.ErrObjectNotExist) {
		finished.Running = true
		return &finished, nil
	}
	if err != nil {
		return nil, fmt.Errorf("read: %w", err)
	}
	return &finished, nil
}
Finished parses the build's finished metadata.
package gcs
import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/url"
	"path"
	"regexp"
	"strconv"
	"strings"
	"cloud.google.com/go/storage"
	"github.com/fvbommel/sortorder"
	"google.golang.org/api/iterator"
	core "k8s.io/api/core/v1"
	"github.com/GoogleCloudPlatform/testgrid/metadata"
	"github.com/GoogleCloudPlatform/testgrid/metadata/junit"
)
type PodInfo struct {
	Pod *core.Pod `json:"pod,omitempty"`
}
const (
	MissingPodInfo = "podinfo.json not found, please install prow's GCS reporter"
	NoPodUtils = "not using decoration, please set decorate: true on prowjob"
)
func truncate(s string, max int) string {
	if max <= 0 {
		return s
	}
	l := len(s)
	if l < max {
		return s
	}
	h := max / 2
	return s[:h] + "..." + s[l-h:]
}
func checkContainerStatus(status core.ContainerStatus) (bool, string) {
	name := status.Name
	if status.State.Waiting != nil {
		return false, fmt.Sprintf("%s still waiting: %s", name, status.State.Waiting.Message)
	}
	if status.State.Running != nil {
		return false, fmt.Sprintf("%s still running", name)
	}
	if status.State.Terminated != nil && status.State.Terminated.ExitCode != 0 {
		return false, fmt.Sprintf("%s exited %d: %s", name, status.State.Terminated.ExitCode, truncate(status.State.Terminated.Message, 140))
	}
	return true, ""
}
func (pi PodInfo) Summarize() (bool, string) {
	if pi.Pod == nil {
		return false, MissingPodInfo
	}
	if pi.Pod.Status.Phase == core.PodSucceeded {
		return true, ""
	}
	conditions := make(map[core.PodConditionType]core.PodCondition, len(pi.Pod.Status.Conditions))
	for _, cond := range pi.Pod.Status.Conditions {
		conditions[cond.Type] = cond
	}
	if cond, ok := conditions[core.PodScheduled]; ok && cond.Status != core.ConditionTrue {
		return false, fmt.Sprintf("pod did not schedule: %s", cond.Message)
	}
	if cond, ok := conditions[core.PodInitialized]; ok && cond.Status != core.ConditionTrue {
		return false, fmt.Sprintf("pod could not initialize: %s", cond.Message)
	}
	for _, status := range pi.Pod.Status.InitContainerStatuses {
		if pass, msg := checkContainerStatus(status); !pass {
			return pass, fmt.Sprintf("init container %s", msg)
		}
	}
	var foundSidecar bool
	for _, status := range pi.Pod.Status.ContainerStatuses {
		if status.Name == "sidecar" {
			foundSidecar = true
		}
		pass, msg := checkContainerStatus(status)
		if pass {
			continue
		}
		if status.Name == "sidecar" {
			return pass, msg
		}
		if status.State.Terminated == nil {
			return pass, msg
		}
	}
	if !foundSidecar {
		return true, NoPodUtils
	}
	return true, ""
}
type Started struct {
	metadata.Started
	Pending bool
}
type Finished struct {
	metadata.Finished
	Running bool
}
type Build struct {
	Path     Path
	baseName string
}
func (build Build) object() string {
	o := build.Path.Object()
	if strings.HasSuffix(o, "/") {
		return o[0 : len(o)-1]
	}
	return o
}
func (build Build) Build() string {
	return path.Base(build.object())
}
func (build Build) Job() string {
	return path.Base(path.Dir(build.object()))
}
func (build Build) String() string {
	return build.Path.String()
}
func readLink(objAttrs *storage.ObjectAttrs) string {
	if link, ok := objAttrs.Metadata["x-goog-meta-link"]; ok {
		return link
	}
	if link, ok := objAttrs.Metadata["link"]; ok {
		return link
	}
	return ""
}
func hackOffset(offset *string) string {
	if *offset == "" {
		return ""
	}
	if strings.HasSuffix(*offset, "/") {
		*offset = (*offset)[:len(*offset)-1]
	}
	dir, offsetBaseName := path.Split(*offset)
	const first = 1000000000000000000
	if n, err := strconv.Atoi(offsetBaseName); err == nil && n < first {
		*offset = path.Join(dir, "0")
	}
	return offsetBaseName
}
func ListBuilds(parent context.Context, lister Lister, gcsPath Path, after *Path) ([]Build, error) {
	ctx, cancel := context.WithCancel(parent)
	defer cancel()
	var offset string
	if after != nil {
		offset = after.Object()
	}
	offsetBaseName := hackOffset(&offset)
	if !strings.HasSuffix(offset, "/") {
		offset += "/"
	}
	it := lister.Objects(ctx, gcsPath, "/", offset)
	var all []Build
	for {
		objAttrs, err := it.Next()
		if errors.Is(err, iterator.Done) {
			break
		}
		if err != nil {
			return nil, fmt.Errorf("list objects: %w", err)
		}
		if link := readLink(objAttrs); link != "" {
			link = strings.TrimSpace(link)
			u, err := url.Parse(link)
			if err != nil {
				return nil, fmt.Errorf("parse %s link: %v", objAttrs.Name, err)
			}
			if !strings.HasSuffix(u.Path, "/") {
				u.Path += "/"
			}
			var linkPath Path
			if err := linkPath.SetURL(u); err != nil {
				return nil, fmt.Errorf("bad %s link path %s: %w", objAttrs.Name, u, err)
			}
			all = append(all, Build{
				Path:     linkPath,
				baseName: path.Base(objAttrs.Name),
			})
			continue
		}
		if objAttrs.Prefix == "" {
			continue  
		}
		loc := "gs://" + gcsPath.Bucket() + "/" + objAttrs.Prefix
		gcsPath, err := NewPath(loc)
		if err != nil {
			return nil, fmt.Errorf("bad path %q: %w", loc, err)
		}
		all = append(all, Build{
			Path:     *gcsPath,
			baseName: path.Base(objAttrs.Prefix),
		})
	}
	Sort(all)
	if offsetBaseName != "" {
		for i, b := range all {
			if sortorder.NaturalLess(b.baseName, offsetBaseName) || b.baseName == offsetBaseName {
				return all[:i], nil  
			}
		}
	}
	return all, nil
}
var re = regexp.MustCompile(`.+/junit((_[^_]+)?(_\d+-\d+)?(_\d+)?|.+)?\.xml$`)
func dropPrefix(name string) string {
	if len(name) == 0 {
		return name
	}
	return name[1:]
}
func parseSuitesMeta(name string) map[string]string {
	mat := re.FindStringSubmatch(name)
	if mat == nil {
		return nil
	}
	c, ti, th := dropPrefix(mat[2]), dropPrefix(mat[3]), dropPrefix(mat[4])
	if c == "" && ti == "" && th == "" {
		c = mat[1]
	}
	return map[string]string{
		"Context":   c,
		"Timestamp": ti,
		"Thread":    th,
	}
}
func readJSON(ctx context.Context, opener Opener, p Path, i interface{}) error {
	reader, _, err := opener.Open(ctx, p)
	if errors.Is(err, storage.ErrObjectNotExist) {
		return err
	}
	if err != nil {
		return fmt.Errorf("open: %w", err)
	}
	defer reader.Close()
	if err = json.NewDecoder(reader).Decode(i); err != nil {
		return fmt.Errorf("decode: %w", err)
	}
	if err := reader.Close(); err != nil {
		return fmt.Errorf("close: %w", err)
	}
	return nil
}
func (build Build) PodInfo(ctx context.Context, opener Opener) (*PodInfo, error) {
	path, err := build.Path.ResolveReference(&url.URL{Path: "podinfo.json"})
	if err != nil {
		return nil, fmt.Errorf("resolve: %w", err)
	}
	var podInfo PodInfo
	err = readJSON(ctx, opener, *path, &podInfo)
	if errors.Is(err, storage.ErrObjectNotExist) {
		return nil, nil
	}
	if err != nil {
		return nil, fmt.Errorf("read: %w", err)
	}
	return &podInfo, nil
}
func (build Build) Started(ctx context.Context, opener Opener) (*Started, error) {
	path, err := build.Path.ResolveReference(&url.URL{Path: "started.json"})
	if err != nil {
		return nil, fmt.Errorf("resolve: %w", err)
	}
	var started Started
	err = readJSON(ctx, opener, *path, &started)
	if errors.Is(err, storage.ErrObjectNotExist) {
		started.Pending = true
		return &started, nil
	}
	if err != nil {
		return nil, fmt.Errorf("read: %w", err)
	}
	return &started, nil
}