| 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) |
| } |