blob: 18ead2baf49f37fcad9138cd9eb2b953f59f4d5b [file] [log] [blame]
package chrome_branch
/*
Package chrome_branch provides utilities for retrieving Chrome release
branches.
*/
import (
"context"
"encoding/json"
"fmt"
"net/http"
"regexp"
"strconv"
"go.skia.org/infra/go/git"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/util"
)
const (
// RefMain is the ref name for the main branch.
RefMain = git.DefaultRef
schedulePhaseBeta = "beta"
schedulePhaseDev = "branch"
schedulePhaseStable = "stable"
schedulePhaseStableCut = "stable_cut"
jsonURL = "https://chromiumdash.appspot.com/fetch_milestones"
os = "linux"
refTmplRelease = "refs/branch-heads/%d"
)
var versionRegex = regexp.MustCompile(`(\d+)\.\d+\.(\d+)\.\d+`)
// ReleaseBranchRef returns the fully-qualified ref for the release branch with
// the given number.
func ReleaseBranchRef(number int) string {
return fmt.Sprintf(refTmplRelease, number)
}
// Branch describes a single Chrome release branch.
type Branch struct {
// Milestone number for this branch.
Milestone int `json:"milestone"`
// Branch number for this branch. Always zero for the main branch, because
// numbered release candidates are cut from this branch regularly and there
// is no single number which refers to it.
Number int `json:"number"`
// Fully-qualified ref for this branch.
Ref string `json:"ref"`
// Corresponding V8 ref (calculated)
V8Branch string
}
// Copy the Branch.
func (b *Branch) Copy() *Branch {
if b == nil {
return nil
}
return &Branch{
Milestone: b.Milestone,
Number: b.Number,
Ref: b.Ref,
V8Branch: b.V8Branch,
}
}
// Validate returns an error if the Branch is not valid.
func (b *Branch) Validate() error {
if b.Milestone == 0 {
return skerr.Fmt("Milestone is required.")
}
if b.Ref == "" {
return skerr.Fmt("Ref is required.")
}
if b.Ref == RefMain {
if b.Number != 0 {
return skerr.Fmt("Number must be zero for main branch.")
}
} else {
if b.Number == 0 {
return skerr.Fmt("Number is required for non-main branches.")
}
}
if b.V8Branch == "" {
return skerr.Fmt("V8Branch is required.")
}
return nil
}
// Branches describes the mapping from Chrome release channel name to branch
// number.
type Branches struct {
Main *Branch `json:"main"`
Dev *Branch `json:"dev"`
Beta *Branch `json:"beta"`
Stable *Branch `json:"stable"`
}
// Copy the Branches.
func (b *Branches) Copy() *Branches {
return &Branches{
Main: b.Main.Copy(),
Dev: b.Dev.Copy(),
Beta: b.Beta.Copy(),
Stable: b.Stable.Copy(),
}
}
// Validate returns an error if the Branches are not valid.
func (b *Branches) Validate() error {
if b.Beta == nil {
return skerr.Fmt("Beta branch is missing.")
}
if err := b.Beta.Validate(); err != nil {
return skerr.Wrapf(err, "Beta branch is invalid")
}
if b.Dev != nil {
if err := b.Dev.Validate(); err != nil {
return skerr.Wrapf(err, "Dev branch is invalid")
}
}
if b.Stable == nil {
return skerr.Fmt("Stable branch is missing.")
}
if err := b.Stable.Validate(); err != nil {
return skerr.Wrapf(err, "Stable branch is invalid")
}
if b.Main == nil {
return skerr.Fmt("Main branch is missing.")
}
if err := b.Main.Validate(); err != nil {
return skerr.Wrapf(err, "Main branch is invalid")
}
return nil
}
// Get retrieves the current Branches and the list of active milestones.
func Get(ctx context.Context, c *http.Client) (*Branches, []*Branch, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, jsonURL, nil)
if err != nil {
return nil, nil, skerr.Wrap(err)
}
resp, err := c.Do(req)
if err != nil {
return nil, nil, skerr.Wrap(err)
}
defer util.Close(resp.Body)
type milestone struct {
Milestone int `json:"milestone"`
ChromiumBranch string `json:"chromium_branch"`
ScheduleActive bool `json:"schedule_active"`
SchedulePhase string `json:"schedule_phase"`
}
var milestones []milestone
if err := json.NewDecoder(resp.Body).Decode(&milestones); err != nil {
return nil, nil, skerr.Wrap(err)
}
byPhase := map[string]*Branch{}
byMilestone := map[int]*Branch{}
activeMilestones := []*Branch{}
for _, milestone := range milestones {
branch := &Branch{}
branch.Milestone = milestone.Milestone
branch.V8Branch = fmt.Sprintf("%d.%d", milestone.Milestone/10, branch.Milestone%10)
number, err := strconv.Atoi(milestone.ChromiumBranch)
if err != nil {
return nil, nil, skerr.Wrapf(err, "invalid branch number %q for channel %q", milestone.ChromiumBranch, milestone.SchedulePhase)
}
branch.Number = number
branch.Ref = ReleaseBranchRef(number)
byPhase[milestone.SchedulePhase] = branch
byMilestone[milestone.Milestone] = branch
if milestone.ScheduleActive {
activeMilestones = append(activeMilestones, branch)
}
}
rv := &Branches{}
rv.Stable = byPhase[schedulePhaseStable]
rv.Dev = byPhase[schedulePhaseDev]
rv.Beta = byPhase[schedulePhaseBeta]
if rv.Beta == nil {
rv.Beta = byPhase[schedulePhaseStableCut]
}
if rv.Beta == nil && rv.Stable != nil {
rv.Beta = byMilestone[rv.Stable.Milestone+1]
}
if rv.Beta != nil {
mainMilestoneMinusOne := rv.Beta.Milestone
if rv.Dev != nil && rv.Dev.Milestone > mainMilestoneMinusOne {
mainMilestoneMinusOne = rv.Dev.Milestone
}
rv.Main = &Branch{
Milestone: mainMilestoneMinusOne + 1,
Number: 0,
Ref: RefMain,
V8Branch: RefMain,
}
}
if err := rv.Validate(); err != nil {
return nil, nil, err
}
return rv, activeMilestones, nil
}
// Client is a wrapper for Get which facilitates testing.
type Client interface {
// Get retrieves the current Branches and the list of active milestones.
Get(context.Context) (*Branches, []*Branch, error)
}
// client implements Client.
type client struct {
*http.Client
}
// NewClient returns a Client instance.
func NewClient(c *http.Client) Client {
return &client{
Client: c,
}
}
// See documentation for Client interface.
func (c *client) Get(ctx context.Context) (*Branches, []*Branch, error) {
return Get(ctx, c.Client)
}