Skip to content

Commit

Permalink
Add --update flag to schedule-builder
Browse files Browse the repository at this point in the history
The schedule builder is now able to update the schedule based on the
current date. The logic is also able to backfill missed releases and
will stop adding more if the next target date is in the future.

Signed-off-by: Sascha Grunert <[email protected]>
  • Loading branch information
saschagrunert committed Apr 3, 2024
1 parent 921a789 commit e12f3c0
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 41 deletions.
101 changes: 93 additions & 8 deletions cmd/schedule-builder/cmd/markdown.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,22 @@ import (
"bytes"
"embed"
"fmt"
"os"
"strings"
"text/template"
"time"

"github.com/olekukonko/tablewriter"
"github.com/sirupsen/logrus"
"sigs.k8s.io/release-utils/util"
"sigs.k8s.io/yaml"
)

//go:embed templates/*.tmpl
var tpls embed.FS

// runs with `--type=patch` to return the patch schedule
func parseSchedule(patchSchedule PatchSchedule) string {
func parsePatchSchedule(patchSchedule PatchSchedule) string {
output := []string{}
output = append(output, "### Timeline\n")
for _, releaseSchedule := range patchSchedule.Schedules {
Expand Down Expand Up @@ -65,8 +69,6 @@ func parseSchedule(patchSchedule PatchSchedule) string {
scheduleOut := strings.Join(output, "\n")

logrus.Info("Schedule parsed")
println(scheduleOut)

return scheduleOut
}

Expand Down Expand Up @@ -108,15 +110,13 @@ func parseReleaseSchedule(releaseSchedule ReleaseSchedule) string {
relSched.TimelineOutput = tableString.String()
}

scheduleOut := ProcessFile("templates/rel-schedule.tmpl", relSched)
scheduleOut := processFile("templates/rel-schedule.tmpl", relSched)

logrus.Info("Release Schedule parsed")
println(scheduleOut)

return scheduleOut
}

func patchReleaseInPreviousList(a string, previousPatches []PatchRelease) bool {
func patchReleaseInPreviousList(a string, previousPatches []*PatchRelease) bool {
for _, b := range previousPatches {
if b.Release == a {
return true
Expand All @@ -141,10 +141,95 @@ func process(t *template.Template, vars interface{}) string {
return tmplBytes.String()
}

func ProcessFile(fileName string, vars interface{}) string {
func processFile(fileName string, vars interface{}) string {
tmpl, err := template.ParseFS(tpls, fileName)
if err != nil {
panic(err)
}
return process(tmpl, vars)
}

func updatePatchSchedule(refTime time.Time, schedule PatchSchedule, filePath string) error {
const refDate = "2006-01-02"

for _, schedule := range schedule.Schedules {
for {
eolDate, err := time.Parse(refDate, schedule.EndOfLifeDate)
if err != nil {
return fmt.Errorf("parse end of life date: %w", err)
}

if refTime.After(eolDate) {
logrus.Infof("Skipping end of life release: %s", schedule.Release)
break
}

targetDate, err := time.Parse(refDate, schedule.Next.TargetDate)
if err != nil {
return fmt.Errorf("parse target date: %w", err)
}

if targetDate.After(refTime) {
break
}

// Copy the release to the previousPatches section
schedule.PreviousPatches = append([]*PatchRelease{schedule.Next}, schedule.PreviousPatches...)

// Create a new next release
nextReleaseVersion, err := util.TagStringToSemver(schedule.Next.Release)
if err != nil {
return fmt.Errorf("parse semver version: %w", err)
}
if err := nextReleaseVersion.IncrementPatch(); err != nil {
return fmt.Errorf("increment patch version: %w", err)
}

cherryPickDeadline, err := time.Parse(refDate, schedule.Next.CherryPickDeadline)
if err != nil {
return fmt.Errorf("parse cherry pick deadline: %w", err)
}
cherryPickDeadlinePlusOneMonth := cherryPickDeadline.AddDate(0, 1, 0)
cherryPickDay := firstFriday(cherryPickDeadlinePlusOneMonth)
newCherryPickDeadline := time.Date(cherryPickDeadlinePlusOneMonth.Year(), cherryPickDeadlinePlusOneMonth.Month(), cherryPickDay, 0, 0, 0, 0, time.UTC)

targetDatePlusOneMonth := targetDate.AddDate(0, 1, 0)
targetDateDay := secondTuesday(targetDatePlusOneMonth)
newTargetDate := time.Date(targetDatePlusOneMonth.Year(), targetDatePlusOneMonth.Month(), targetDateDay, 0, 0, 0, 0, time.UTC)

schedule.Next = &PatchRelease{
Release: nextReleaseVersion.String(),
CherryPickDeadline: newCherryPickDeadline.Format(refDate),
TargetDate: newTargetDate.Format(refDate),
}

logrus.Infof("Adding release schedule: %+v", schedule.Next)
}
}

yamlBytes, err := yaml.Marshal(schedule)
if err != nil {
return fmt.Errorf("marshal schedule YAML: %w", err)
}

//nolint:gocritic,gosec
if err := os.WriteFile(filePath, yamlBytes, 0o644); err != nil {
return fmt.Errorf("write schedule YAML: %w", err)
}

logrus.Infof("Wrote schedule YAML to: %v", filePath)
return nil
}

func secondTuesday(t time.Time) int {
return firstMonday(t) + 8
}

func firstFriday(t time.Time) int {
return firstMonday(t) + 4
}

func firstMonday(from time.Time) int {
t := time.Date(from.Year(), from.Month(), 1, 0, 0, 0, 0, time.UTC)
return (8-int(t.Weekday()))%7 + 1
}
97 changes: 91 additions & 6 deletions cmd/schedule-builder/cmd/markdown_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@ package cmd

import (
"fmt"
"os"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sigs.k8s.io/yaml"
)

const expectedPatchSchedule = `### Timeline
Expand Down Expand Up @@ -121,15 +125,15 @@ Please refer to the [release phases document](../release_phases.md).
[release phases document]: ../release_phases.md
`

func TestParseSchedule(t *testing.T) {
func TestParsePatchSchedule(t *testing.T) {
testcases := []struct {
name string
schedule PatchSchedule
}{
{
name: "next patch is not in previous patch list",
schedule: PatchSchedule{
Schedules: []Schedule{
Schedules: []*Schedule{
{
Release: "X.Y",
Next: &PatchRelease{
Expand All @@ -139,7 +143,7 @@ func TestParseSchedule(t *testing.T) {
},
EndOfLifeDate: "NOW",
MaintenanceModeStartDate: "THEN",
PreviousPatches: []PatchRelease{
PreviousPatches: []*PatchRelease{
{
Release: "X.Y.XXX",
CherryPickDeadline: "2020-05-15",
Expand All @@ -159,7 +163,7 @@ func TestParseSchedule(t *testing.T) {
{
name: "next patch is in previous patch list",
schedule: PatchSchedule{
Schedules: []Schedule{
Schedules: []*Schedule{
{
Release: "X.Y",
Next: &PatchRelease{
Expand All @@ -169,7 +173,7 @@ func TestParseSchedule(t *testing.T) {
},
EndOfLifeDate: "NOW",
MaintenanceModeStartDate: "THEN",
PreviousPatches: []PatchRelease{
PreviousPatches: []*PatchRelease{
{
Release: "X.Y.ZZZ",
CherryPickDeadline: "2020-06-12",
Expand All @@ -195,7 +199,7 @@ func TestParseSchedule(t *testing.T) {

for _, tc := range testcases {
fmt.Printf("Test case: %s\n", tc.name)
out := parseSchedule(tc.schedule)
out := parsePatchSchedule(tc.schedule)
require.Equal(t, out, expectedPatchSchedule)
}
}
Expand Down Expand Up @@ -319,3 +323,84 @@ func TestParseReleaseSchedule(t *testing.T) {
require.Equal(t, out, expectedReleaseSchedule)
}
}

func TestUpdatePatchSchedule(t *testing.T) {
for _, tc := range []struct {
name string
refTime time.Time
givenSchedule, expectedSchedule PatchSchedule
}{
{
name: "succeed to update the schedule",
refTime: time.Date(2024, 4, 3, 0, 0, 0, 0, time.UTC),
givenSchedule: PatchSchedule{
Schedules: []*Schedule{
{ // Needs multiple updates
Release: "1.30",
Next: &PatchRelease{
Release: "1.30.1",
CherryPickDeadline: "2024-01-05",
TargetDate: "2024-01-09",
},
EndOfLifeDate: "2025-01-01",
MaintenanceModeStartDate: "2024-12-01",
},
{ // EOL
Release: "1.20",
EndOfLifeDate: "2023-01-01",
},
},
},
expectedSchedule: PatchSchedule{
Schedules: []*Schedule{
{
Release: "1.30",
Next: &PatchRelease{
Release: "1.30.4",
CherryPickDeadline: "2024-04-05",
TargetDate: "2024-04-09",
},
EndOfLifeDate: "2025-01-01",
MaintenanceModeStartDate: "2024-12-01",
PreviousPatches: []*PatchRelease{
{
Release: "1.30.3",
CherryPickDeadline: "2024-03-08",
TargetDate: "2024-03-12",
},
{
Release: "1.30.2",
CherryPickDeadline: "2024-02-09",
TargetDate: "2024-02-13",
},
{
Release: "1.30.1",
CherryPickDeadline: "2024-01-05",
TargetDate: "2024-01-09",
},
},
},
{
Release: "1.20",
EndOfLifeDate: "2023-01-01",
},
},
},
},
} {
t.Run(tc.name, func(t *testing.T) {
file, err := os.CreateTemp("", "schedule-")
require.NoError(t, err)
require.NoError(t, file.Close())

require.NoError(t, updatePatchSchedule(tc.refTime, tc.givenSchedule, file.Name()))

yamlBytes, err := os.ReadFile(file.Name())
require.NoError(t, err)
res := PatchSchedule{}
require.NoError(t, yaml.UnmarshalStrict(yamlBytes, &res))

assert.Equal(t, tc.expectedSchedule, res)
})
}
}
28 changes: 14 additions & 14 deletions cmd/schedule-builder/cmd/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,27 @@ limitations under the License.

package cmd

// PatchSchedule main struct to hold the schedules
// PatchSchedule main struct to hold the schedules.
type PatchSchedule struct {
Schedules []Schedule `yaml:"schedules"`
Schedules []*Schedule `json:"schedules,omitempty" yaml:"schedules,omitempty"`
}

// PatchRelease struct to define the patch schedules
// PatchRelease struct to define the patch schedules.
type PatchRelease struct {
Release string `yaml:"release"`
CherryPickDeadline string `yaml:"cherryPickDeadline"`
TargetDate string `yaml:"targetDate"`
Note string `yaml:"note"`
Release string `json:"release,omitempty" yaml:"release,omitempty"`
CherryPickDeadline string `json:"cherryPickDeadline,omitempty" yaml:"cherryPickDeadline,omitempty"`
TargetDate string `json:"targetDate,omitempty" yaml:"targetDate,omitempty"`
Note string `json:"note,omitempty" yaml:"note,omitempty"`
}

// Schedule struct to define the release schedule for a specific version
// Schedule struct to define the release schedule for a specific version.
type Schedule struct {
Release string `yaml:"release"`
ReleaseDate string `yaml:"releaseDate"`
Next *PatchRelease `yaml:"next"`
EndOfLifeDate string `yaml:"endOfLifeDate"`
MaintenanceModeStartDate string `yaml:"maintenanceModeStartDate"`
PreviousPatches []PatchRelease `yaml:"previousPatches"`
Release string `json:"release,omitempty" yaml:"release,omitempty"`
ReleaseDate string `json:"releaseDate,omitempty" yaml:"releaseDate,omitempty"`
Next *PatchRelease `json:"next,omitempty" yaml:"next,omitempty"`
EndOfLifeDate string `json:"endOfLifeDate,omitempty" yaml:"endOfLifeDate,omitempty"`
MaintenanceModeStartDate string `json:"maintenanceModeStartDate,omitempty" yaml:"maintenanceModeStartDate,omitempty"`
PreviousPatches []*PatchRelease `json:"previousPatches,omitempty" yaml:"previousPatches,omitempty"`
}

type ReleaseSchedule struct {
Expand Down
Loading

0 comments on commit e12f3c0

Please sign in to comment.