From 4f20f6c53973f2e011186ab4161a73e73f30b473 Mon Sep 17 00:00:00 2001 From: Jason Johnston Date: Fri, 6 Oct 2023 09:37:27 -0400 Subject: [PATCH] CLI: Add gbm cli tool with checklist rendering command (#146) * Initial commit for the gbm cli * Add Pull Request support to the cli tool (#118) * Add initial gh tools with failng tests * Add repo package * Add GetPr function * Add error return to getOrg * Add CreatePr function * Add UpdatePr * Add AddLabels function * Refactor AddLabels to suppor removing labels * Add RemoveLabels * Update labels when creating or updateing a PR: * Add comment --------- Co-authored-by: jhnstn * Add ability to search prs (#119) Co-authored-by: jhnstn * Add git helpers (#120) Co-authored-by: jhnstn * CLI Add template renderer (#121) * Add template renderer * Hook root template directory into project main --------- Co-authored-by: jhnstn * Add render checklist command (#122) * Add some gbm helpers * Add initial command render checklist * Update render package to accept custom functions * Update root render command message Add add empty bin dir --------- Co-authored-by: jhnstn * Add missing templates * Add ValidateVersion function * Validate the version before running the checklist generator * Fix version in 'Incoming Changes' section * Fix markdown format in quote blocks Also use a group to tighten up the vertical spacing * Add host version flag for patch releases * Update apps infrastructure slack channel name * Add changes from PR #143 * Add check Aztec flow Also reorg the internal library code * Update task partial render helper Also add a checkeck task helper * Hide Aztec steps if Aztec is valid * Move Aztec steps to a seperate file * Add console package for handling cli output and exits * Add standalone aztec steps generator * Don't need the internal packages yet * tidy up go mod * Refactor render to use struct data * Resolve typos with release checklist --------- Co-authored-by: Derek Blank --- cli/bin/.gitkeep | 0 cli/cmd/render/aztec.go | 22 ++ cli/cmd/render/checklist.go | 95 ++++++++ cli/cmd/render/root.go | 31 +++ cli/cmd/render/utils.go | 14 ++ cli/cmd/root.go | 23 ++ cli/go.mod | 17 ++ cli/go.sum | 60 +++++ cli/main.go | 19 ++ cli/pkg/console/console.go | 37 +++ cli/pkg/gbm/aztec.go | 111 +++++++++ cli/pkg/gbm/utils.go | 25 ++ cli/pkg/gbm/utils_test.go | 69 ++++++ cli/pkg/render/render.go | 72 ++++++ cli/pkg/render/render_test.go | 107 +++++++++ cli/pkg/render/testdata/basic_template.txt | 1 + cli/pkg/render/testdata/func_template.txt | 1 + cli/pkg/render/testdata/invalid_template.txt | 1 + cli/pkg/render/testdata/test_template.txt | 1 + cli/templates/.gitkeep | 0 cli/templates/checklist/aztec.html | 26 ++ cli/templates/checklist/checklist.html | 235 +++++++++++++++++++ cli/templates/checklist/task.html | 21 ++ 23 files changed, 988 insertions(+) create mode 100644 cli/bin/.gitkeep create mode 100644 cli/cmd/render/aztec.go create mode 100644 cli/cmd/render/checklist.go create mode 100644 cli/cmd/render/root.go create mode 100644 cli/cmd/render/utils.go create mode 100644 cli/cmd/root.go create mode 100644 cli/go.mod create mode 100644 cli/go.sum create mode 100644 cli/main.go create mode 100644 cli/pkg/console/console.go create mode 100644 cli/pkg/gbm/aztec.go create mode 100644 cli/pkg/gbm/utils.go create mode 100644 cli/pkg/gbm/utils_test.go create mode 100644 cli/pkg/render/render.go create mode 100644 cli/pkg/render/render_test.go create mode 100644 cli/pkg/render/testdata/basic_template.txt create mode 100644 cli/pkg/render/testdata/func_template.txt create mode 100644 cli/pkg/render/testdata/invalid_template.txt create mode 100644 cli/pkg/render/testdata/test_template.txt create mode 100644 cli/templates/.gitkeep create mode 100644 cli/templates/checklist/aztec.html create mode 100644 cli/templates/checklist/checklist.html create mode 100644 cli/templates/checklist/task.html diff --git a/cli/bin/.gitkeep b/cli/bin/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/cli/cmd/render/aztec.go b/cli/cmd/render/aztec.go new file mode 100644 index 00000000..d1bfa5ea --- /dev/null +++ b/cli/cmd/render/aztec.go @@ -0,0 +1,22 @@ +package render + +import ( + "github.com/spf13/cobra" + "github.com/wordpress-mobile/gbm-cli/pkg/console" +) + +var AztecCmd = &cobra.Command{ + Use: "aztec", + Short: "Render the steps for upgrading Aztec", + Run: func(cmd *cobra.Command, args []string) { + result, err := renderAztecSteps(false) + + console.ExitIfError(err) + + if writeToClipboard { + console.Clipboard(result) + } else { + console.Out(result) + } + }, +} diff --git a/cli/cmd/render/checklist.go b/cli/cmd/render/checklist.go new file mode 100644 index 00000000..9f4bd28f --- /dev/null +++ b/cli/cmd/render/checklist.go @@ -0,0 +1,95 @@ +package render + +import ( + "fmt" + "text/template" + + "github.com/spf13/cobra" + + "github.com/wordpress-mobile/gbm-cli/pkg/console" + "github.com/wordpress-mobile/gbm-cli/pkg/gbm" + "github.com/wordpress-mobile/gbm-cli/pkg/render" +) + +var version string +var hostVersion string +var message string +var releaseDate string +var checkAztec bool + +type templateData struct { + Version string + Scheduled bool + Date string + Message string + ReleaseUrl string + HostVersion string + IncludeAztec bool + CheckAztec bool +} + +// checklistCmd represents the checklist command +var ChecklistCmd = &cobra.Command{ + Use: "checklist", + Short: "Render the content for the release checklist", + Long: ` +`, + Run: func(cmd *cobra.Command, args []string) { + + vv := gbm.ValidateVersion(version) + if !vv { + console.ExitError(1, "%v is not a valid version. Versions must have a `Major.Minor.Patch` form", version) + } + + // For now let's assume we should include the Aztec steps unless explicitly checking if the versions are valid. + // We'll render the aztec steps with the optional + includeAztec := true + if checkAztec { + includeAztec = !gbm.ValidateAztecVersions() + + if includeAztec { + console.Info("Aztec is not set to a stable version. Including the Update Aztec steps.") + } + } + + scheduled := gbm.IsScheduledRelease(version) + + if releaseDate == "" { + releaseDate = gbm.NextReleaseDate() + } + + releaseUrl := fmt.Sprintf("https://github.com/wordpress-mobile/gutenberg-mobile/releases/new?tag=v%s&target=release/%s&title=Release+%s", version, version, version) + + t := render.Template{ + Path: "templates/checklist/checklist.html", + Funcs: template.FuncMap{"RenderAztecSteps": renderAztecSteps}, + Data: templateData{ + Version: version, + Scheduled: scheduled, + ReleaseUrl: releaseUrl, + Date: releaseDate, + HostVersion: hostVersion, + IncludeAztec: includeAztec, + CheckAztec: checkAztec, + }, + } + + result, err := render.RenderTasks(t) + console.ExitIfError(err) + + if writeToClipboard { + console.Clipboard(result) + } else { + console.Out(result) + } + }, +} + +func init() { + ChecklistCmd.Flags().StringVarP(&version, "version", "v", "", "release version") + ChecklistCmd.MarkFlagRequired("version") + ChecklistCmd.Flags().StringVarP(&message, "message", "m", "", "release message") + ChecklistCmd.Flags().StringVarP(&releaseDate, "date", "d", "", "release date") + ChecklistCmd.Flags().BoolVar(&checkAztec, "a", false, "Check if Aztec config is valid before adding the optional update Aztec section") + ChecklistCmd.Flags().StringVarP(&hostVersion, "host-version", "V", "X.XX", "host app version") +} diff --git a/cli/cmd/render/root.go b/cli/cmd/render/root.go new file mode 100644 index 00000000..05ec78a3 --- /dev/null +++ b/cli/cmd/render/root.go @@ -0,0 +1,31 @@ +package render + +import ( + "os" + + "github.com/spf13/cobra" +) + +var writeToClipboard bool + +// rootCmd represents the render command +var RenderCmd = &cobra.Command{ + Use: "render", + Short: "Renders various GBM templates", + Long: `Use this command to render: + - Release checklists + - Steps to update Aztec + `, + Run: func(cmd *cobra.Command, args []string) { + if len(args) == 0 { + cmd.Help() + os.Exit(0) + } + }, +} + +func init() { + RenderCmd.AddCommand(ChecklistCmd) + RenderCmd.AddCommand(AztecCmd) + RenderCmd.PersistentFlags().BoolVar(&writeToClipboard, "c", false, "Send output to clipboard") +} diff --git a/cli/cmd/render/utils.go b/cli/cmd/render/utils.go new file mode 100644 index 00000000..52f61bf0 --- /dev/null +++ b/cli/cmd/render/utils.go @@ -0,0 +1,14 @@ +package render + +import ( + "fmt" + + "github.com/wordpress-mobile/gbm-cli/pkg/render" +) + +func renderAztecSteps(conditional bool) (string, error) { + return render.RenderTasks(render.Template{ + Path: "templates/checklist/aztec.html", + Json: fmt.Sprintf(`{"conditional": %v}`, conditional), + }) +} diff --git a/cli/cmd/root.go b/cli/cmd/root.go new file mode 100644 index 00000000..09438bfd --- /dev/null +++ b/cli/cmd/root.go @@ -0,0 +1,23 @@ +package cmd + +import ( + "github.com/spf13/cobra" + "github.com/wordpress-mobile/gbm-cli/cmd/render" + "github.com/wordpress-mobile/gbm-cli/pkg/console" +) + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "gbm", + Short: "Gutenberg Mobile CLI", +} + +func Execute() { + err := rootCmd.Execute() + console.ExitIfError(err) +} + +func init() { + // Add the render command + rootCmd.AddCommand(render.RenderCmd) +} diff --git a/cli/go.mod b/cli/go.mod new file mode 100644 index 00000000..76fc28d7 --- /dev/null +++ b/cli/go.mod @@ -0,0 +1,17 @@ +module github.com/wordpress-mobile/gbm-cli + +go 1.20 + +require ( + github.com/spf13/cobra v1.7.0 + golang.design/x/clipboard v0.7.0 +) + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect + golang.org/x/image v0.6.0 // indirect + golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect + golang.org/x/sys v0.8.0 // indirect +) diff --git a/cli/go.sum b/cli/go.sum new file mode 100644 index 00000000..a73f4591 --- /dev/null +++ b/cli/go.sum @@ -0,0 +1,60 @@ +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.design/x/clipboard v0.7.0 h1:4Je8M/ys9AJumVnl8m+rZnIvstSnYj1fvzqYrU3TXvo= +golang.design/x/clipboard v0.7.0/go.mod h1:PQIvqYO9GP29yINEfsEn5zSQKAz3UgXmZKzDA6dnq2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU= +golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4= +golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c h1:Gk61ECugwEHL6IiyyNLXNzmu8XslmRP2dS0xjIYhbb4= +golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cli/main.go b/cli/main.go new file mode 100644 index 00000000..7892369d --- /dev/null +++ b/cli/main.go @@ -0,0 +1,19 @@ +/* +Copyright © 2023 NAME HERE +*/ +package main + +import ( + "embed" + + "github.com/wordpress-mobile/gbm-cli/cmd" + "github.com/wordpress-mobile/gbm-cli/pkg/render" +) + +//go:embed templates/* +var templatesFS embed.FS + +func main() { + render.TemplateFS = templatesFS + cmd.Execute() +} diff --git a/cli/pkg/console/console.go b/cli/pkg/console/console.go new file mode 100644 index 00000000..e05e4139 --- /dev/null +++ b/cli/pkg/console/console.go @@ -0,0 +1,37 @@ +package console + +import ( + "fmt" + "os" + + "golang.design/x/clipboard" +) + +func ExitIfError(err error) { + if err != nil { + ExitError(1, err.Error()+"\n") + } +} + +func ExitError(code int, format string, args ...interface{}) { + fmt.Fprintf(os.Stderr, format+"\n", args...) + os.Exit(1) +} + +func Clipboard(m string) { + clipboard.Write(clipboard.FmtText, []byte(m)) +} + +/* +Use Out for printing resulting messages that should be piped. For status logging use console.Info +*/ +func Out(m string) { + fmt.Fprintln(os.Stdout, m) +} + +/* +Use Info to log messages from the scripts. Output is sent to stderr to not muddle up pipe-able output +*/ +func Info(format string, args ...interface{}) { + fmt.Fprintf(os.Stderr, format+"\n", args...) +} diff --git a/cli/pkg/gbm/aztec.go b/cli/pkg/gbm/aztec.go new file mode 100644 index 00000000..80a04f55 --- /dev/null +++ b/cli/pkg/gbm/aztec.go @@ -0,0 +1,111 @@ +package gbm + +import ( + "fmt" + "io" + "net/http" + "regexp" +) + +type aztecResult struct { + err error + valid bool + platform string +} + +func ValidateAztecVersions() bool { + + // Since the platform validation functions might reach out to a remote repo, + // let's use a channel to hold the results of the goroutines + res := make(chan aztecResult) + + bothValid := true + + go func() { + res <- ValidateAndroidAztecVersion() + }() + + go func() { + res <- ValidateIosAztecVersion() + }() + + // Wait for each result... + for i := 0; i < 2; i++ { + r := <-res + if r.err != nil { + fmt.Printf("Error validating %s aztec version: %s\n", r.platform, r.err) + bothValid = false + } + + // If the first returned value is false we just need to wait for the second and + // carry on without updating `bothValid`. + if bothValid && !r.valid { + bothValid = false + } + } + + return bothValid +} + +func ValidateAndroidAztecVersion() aztecResult { + branch := "trunk" + org := "WordPress" + path := fmt.Sprintf("https://raw.githubusercontent.com/%s/gutenberg/%s/packages/react-native-aztec/android/build.gradle", org, branch) + + config, err := getConfig(path) + + if err != nil { + return aztecResult{err: err, valid: false, platform: "android"} + } + + regex := regexp.MustCompile(`(?m)^.*aztecVersion.*$`) + valid, err := verifyVersion(config, regex) + + return aztecResult{err: err, valid: valid, platform: "android"} +} + +func ValidateIosAztecVersion() aztecResult { + + branch := "trunk" + org := "wordpress-mobile" + path := fmt.Sprintf("https://raw.githubusercontent.com/%s/gutenberg/%s/packages/react-native-aztec/RNTAztecView.podspec", org, branch) + + config, err := getConfig(path) + + if err != nil { + return aztecResult{err: err, valid: false, platform: "ios"} + } + + regex := regexp.MustCompile(`(?m)^.*WordPress-Aztec-iOS.*$`) + valid, err := verifyVersion(config, regex) + + return aztecResult{err: err, valid: valid, platform: "ios"} +} + +func verifyVersion(config []byte, lnRe *regexp.Regexp) (bool, error) { + versionLine := lnRe.FindAll(config, 1) + if versionLine == nil { + return false, fmt.Errorf("no version line found") + } + semRe := regexp.MustCompile(`(\d+)\.(\d+)\.(\d+)`) + return semRe.Match(versionLine[0]), nil +} + +func getConfig(path string) ([]byte, error) { + var file io.Reader + + resp, err := http.Get(path) + + if err != nil { + return nil, err + } + defer resp.Body.Close() + + file = resp.Body + + if file == nil { + return nil, fmt.Errorf("file not found") + } + + return io.ReadAll(file) +} diff --git a/cli/pkg/gbm/utils.go b/cli/pkg/gbm/utils.go new file mode 100644 index 00000000..4463fcf2 --- /dev/null +++ b/cli/pkg/gbm/utils.go @@ -0,0 +1,25 @@ +package gbm + +import ( + "regexp" + "time" +) + +func ValidateVersion(version string) bool { + re := regexp.MustCompile(`v*(\d+)\.(\d+)\.(\d+)$`) + return re.MatchString(version) +} + +func IsScheduledRelease(version string) bool { + re := regexp.MustCompile(`^v*(\d+)\.(\d+)\.0$`) + return re.MatchString(version) +} + +func NextReleaseDate() string { + weekday := time.Now().Weekday() + daysUntilThursday := 4 - weekday + + nextThursday := time.Now().AddDate(0, 0, int(daysUntilThursday)) + + return nextThursday.Format("Monday 01, 2006") +} diff --git a/cli/pkg/gbm/utils_test.go b/cli/pkg/gbm/utils_test.go new file mode 100644 index 00000000..d7059ed1 --- /dev/null +++ b/cli/pkg/gbm/utils_test.go @@ -0,0 +1,69 @@ +package gbm + +import ( + "strings" + "testing" +) + +func TestNextReleaseDate(t *testing.T) { + + t.Run("It returns the next Thursday", func(t *testing.T) { + got := NextReleaseDate() + + if !strings.Contains(got, "Thursday") { + t.Fatalf("Expected %s to contain %s", got, "Thursday") + } + }) +} + +func TestValidVersion(t *testing.T) { + t.Run("It returns true for a valid scheduled release", func(t *testing.T) { + got := ValidateVersion("1.0.0") + + if !got { + t.Fatalf("Expected %v to be true", got) + } + }) + + t.Run("It returns true for a valid non-scheduled release", func(t *testing.T) { + got := ValidateVersion("1.0.1") + + if !got { + t.Fatalf("Expected %v to be true", got) + } + }) + + t.Run("It returns false when the patch value is missing", func(t *testing.T) { + got := ValidateVersion("1.0") + + if got { + t.Fatalf("Expected %v to be false", got) + } + }) +} + +func TestIsScheduledRelease(t *testing.T) { + + t.Run("It returns true if the release is scheduled", func(t *testing.T) { + got := IsScheduledRelease("v1.0.0") + + if !got { + t.Fatalf("Expected %v to be true", got) + } + }) + + t.Run("It returns false if the release is not scheduled", func(t *testing.T) { + got := IsScheduledRelease("v1.0.1") + + if got { + t.Fatalf("Expected %v to be false", got) + } + }) + + t.Run("It ignores the 'v' prefix", func(t *testing.T) { + got := IsScheduledRelease("1.0.0") + if !got { + t.Fatalf("Expected %v to be true", got) + } + }) +} diff --git a/cli/pkg/render/render.go b/cli/pkg/render/render.go new file mode 100644 index 00000000..cf476bc6 --- /dev/null +++ b/cli/pkg/render/render.go @@ -0,0 +1,72 @@ +package render + +import ( + "bytes" + "embed" + "encoding/json" + "fmt" + "os" + "path/filepath" + "text/template" +) + +var TemplateFS embed.FS + +type Template struct { + Path, Json string + Funcs template.FuncMap + Data interface{} +} +type TaskArgs struct { + Description string +} + +func RenderTasks(t Template) (string, error) { + if t.Funcs == nil { + t.Funcs = template.FuncMap{} + } + t.Funcs["Task"] = Task + + return Render(t) +} + +func RenderJSON(t Template) (string, error) { + if t.Json != "" { + if err := json.Unmarshal([]byte(t.Json), &t.Data); err != nil { + return "", err + } + } + + return Render(t) +} + +func Render(t Template) (string, error) { + tmp, err := template.New(filepath.Base(t.Path)). + Funcs(t.Funcs). + ParseFS(TemplateFS, t.Path) // Parse the template file + + if err != nil { + return "", err + } + + var result bytes.Buffer + if err := tmp.Execute(&result, t.Data); err != nil { + return "", err + } + + return result.String(), nil +} + +func Task(format string, args ...interface{}) string { + t := Template{ + Path: "templates/checklist/task.html", + Data: TaskArgs{Description: fmt.Sprintf(format, args...)}, + } + + res, err := Render(t) + if err != nil { + fmt.Println(err) + os.Exit(13) + } + return res +} diff --git a/cli/pkg/render/render_test.go b/cli/pkg/render/render_test.go new file mode 100644 index 00000000..319e6f77 --- /dev/null +++ b/cli/pkg/render/render_test.go @@ -0,0 +1,107 @@ +package render + +import ( + "embed" + "testing" +) + +//go:embed testdata/* +var templatesFS embed.FS + +func init() { + TemplateFS = templatesFS +} + +func TestRenderJSON(t *testing.T) { + + t.Run("It renders a template with the given JSON", func(t *testing.T) { + tmplt := Template{ + Path: "testdata/test_template.txt", + Json: `{"world": "World"}`, + } + + got, err := RenderJSON(tmplt) + assertNoError(t, err) + + if got != "Hello World" { + t.Fatalf("Expected %s, got %s", "Hello World\n", got) + } + }) + + t.Run("It returns an error if the JSON is invalid", func(t *testing.T) { + tmplt := Template{ + Path: "testdata/test_template.txt", + Json: `{"world": "World`, + } + + _, err := RenderJSON(tmplt) + assertError(t, err) + }) + + t.Run("It returns an error if the template is invalid", func(t *testing.T) { + tmplt := Template{ + Path: "testdata/invalid_template.txt", + Json: `{"world": "World`, + } + + _, err := RenderJSON(tmplt) + assertError(t, err) + }) + + t.Run("It returns an error if the template is missing", func(t *testing.T) { + + tmplt := Template{ + Path: "testdata/missing_template.txt", + Json: `{"world": "World`, + } + _, err := RenderJSON(tmplt) + assertError(t, err) + }) + + t.Run("It renders with custom functions", func(t *testing.T) { + + tmplt := Template{ + Path: "testdata/func_template.txt", + Json: `{}`, + Funcs: map[string]any{ + "echo": func(str string) string { + return str + }, + }, + } + + got, err := RenderJSON(tmplt) + assertNoError(t, err) + + if got != "Hello Custom" { + t.Fatalf("Expected %s, got %s", "Hello Custom\n", got) + } + }) + + t.Run("It renders with no json data", func(t *testing.T) { + tmplt := Template{ + Path: "testdata/basic_template.txt", + } + + got, err := RenderJSON(tmplt) + assertNoError(t, err) + + if got != "hi\n" { + t.Fatalf("Expected %s, got %s", "hi\n", got) + } + }) +} + +func assertError(t *testing.T, err error) { + t.Helper() + if err == nil { + t.Fatalf("Expected an error, got nil") + } +} + +func assertNoError(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } +} diff --git a/cli/pkg/render/testdata/basic_template.txt b/cli/pkg/render/testdata/basic_template.txt new file mode 100644 index 00000000..45b983be --- /dev/null +++ b/cli/pkg/render/testdata/basic_template.txt @@ -0,0 +1 @@ +hi diff --git a/cli/pkg/render/testdata/func_template.txt b/cli/pkg/render/testdata/func_template.txt new file mode 100644 index 00000000..17b632bd --- /dev/null +++ b/cli/pkg/render/testdata/func_template.txt @@ -0,0 +1 @@ +Hello {{ echo "Custom" }} \ No newline at end of file diff --git a/cli/pkg/render/testdata/invalid_template.txt b/cli/pkg/render/testdata/invalid_template.txt new file mode 100644 index 00000000..f5acad76 --- /dev/null +++ b/cli/pkg/render/testdata/invalid_template.txt @@ -0,0 +1 @@ +hello {{ user.name }} \ No newline at end of file diff --git a/cli/pkg/render/testdata/test_template.txt b/cli/pkg/render/testdata/test_template.txt new file mode 100644 index 00000000..84bac85e --- /dev/null +++ b/cli/pkg/render/testdata/test_template.txt @@ -0,0 +1 @@ +Hello {{ .world }} \ No newline at end of file diff --git a/cli/templates/.gitkeep b/cli/templates/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/cli/templates/checklist/aztec.html b/cli/templates/checklist/aztec.html new file mode 100644 index 00000000..fe475796 --- /dev/null +++ b/cli/templates/checklist/aztec.html @@ -0,0 +1,26 @@ + +

Create an Aztec Release {{ if .conditional }}(conditional){{ end }}

+ + +{{ if .conditional }} + +

ℹ️ If gutenberg-mobile/RNTAztecView.podspec and gutenberg-mobile/gutenberg/packages/react-native-aztec/RNTAztecView.podspec + refer to a commit SHA instead of a stable release (e.g. 1.14.1) or refer to different versions, the steps in this section may need to be completed.

+ +{{ else }} + +

ℹ️ Aztec was not pointing to a stable release. Complete the following steps before cutting the release.

+ +{{ end }} +{{ Task `Verify all Aztec PRs attached to the "Next Release" milestone or PRs with changes required for this Gutenberg release have been merged before next steps.` }} + +{{ Task "Open a PR on Aztec repo to update the CHANGELOG.md and README.md files with the new version name." }} + +{{ Task `Create a new release and name it with the tag name from step 1. + For Aztec-iOS, follow this process. + For Aztec-Android, releases are created via the GitHub releases page by hitting the “Draft new release” button, + put the tag name to be created in the tag version field and release title field, and also add the changelog to the release description. + The binary assets (.zip, tar.gz files) are attached automatically after hitting “Publish release”.` }} + +{{ Task `Update Aztec version references within gutenberg-mobile/RNTAztecView.podspec and gutenberg-mobile/gutenberg/packages/react-native-aztec/RNTAztecView.podspec + to the new WordPress-Aztec-iOS version.` }} \ No newline at end of file diff --git a/cli/templates/checklist/checklist.html b/cli/templates/checklist/checklist.html new file mode 100644 index 00000000..d947989a --- /dev/null +++ b/cli/templates/checklist/checklist.html @@ -0,0 +1,235 @@ + + +

Gutenberg Mobile {{ .Version }} – Release Scenario

+ + +{{ if .Message }} + +

{{ .Message }}

+ +{{ end }} + +{{ if .Scheduled }} + +

Before the Release (Tuesday)

+ + + +
+ + {{ Task "Visit all open gutenberg-mobile PRs that are assigned to %s milestone and leave a comment with a message similar to the following:" .Version }} + + +
+

Hey [author]. We will cut the {{ .Version }} release on {{ .Date }}. I plan to circle back and bump this PR to the next milestone then, but please let me know if you'd rather us work to include this PR in {{ .Version }}. Thanks! +

+
+ +
+ +{{ end }} + +{{ if and .CheckAztec .IncludeAztec }} +{{ RenderAztecSteps false}} +{{ end }} + + +

Create the Release{{ if .Scheduled }} (Thursday) {{end}}

+ + +{{ Task `Verify that gutenberg-mobile/RNTAztecView.podspec and gutenberg-mobile/gutenberg/packages/react-native-aztec/RNTAztecView.podspec + refer to the same WordPress-Aztec-iOS version and are pointing to a stable, tagged release (e.g. 1.14.1). If they are not, we may need to create a new Aztec release.` }} + +{{ Task `Clone the release scripts or pull the latest version if you have already cloned it.` }} + +{{ Task `Review the release script instructions. + In your clone of the release scripts, run the script via: ./release_automation.sh. This creates the gutenberg and gutenberg-mobile release PRs as well as WPAndroid and WPiOS integration PRs.` }} + +{{ if .Scheduled }} + +
+ {{ Task "Post a message similar to the following to the #mobile-gutenberg and #mobile-gutenberg-platform Slack channels:" }} + + +
+

⚠️ The gutenberg-mobile {{ .Version }} release branches are now cut. Please do not merge any Gutenberg-related changes into the WPiOS or WPAndroid trunk branches until after + the main apps cut their own releases next week. If you'd like to merge changes now, merge them into the gutenberg/after_{{ .Version }} branches.

+
+ +
+ +{{ end }} + +{{ Task `In both RELEASE-NOTES.txt and gutenberg/packages/react-native-editor/CHANGELOG.md, + replace Unreleased section with the release version and create a new Unreleased section.` }} + +{{ Task `Review and update RELEASE-NOTES.txt file on both WPAndroid and WPiOS PRs so it includes all user-facing changes introduced in the release. + Keep in mind that some changes can be specific to a single platform, so they should only be added to the release notes of the platform that affects + (e.g. a change that only affects Android should only be included in WPAndroid release notes).` }} + +{{ Task `Verify the WPAndroid and WPiOS PR builds succeed. For WPAndroid, if the PR CI tasks include a 403 error related to an inability to resolve the + react-native-bridge dependency, you must wait for the Build Android RN Bridge & Publish to S3 + task to succeed in gutenberg-mobile and then restart the WPAndroid CI tasks. Similarly, for iOS, you must wait for the Build iOS RN XCFramework & Publish to S3 task to succeed.` }} + +{{ Task `Once the installable builds are ready, perform a quick smoke test of the editor on both iOS and Android to verify it launches without crashing. +We will perform additional testing after the main apps cut their releases.` }} + +{{ Task `Fill in the missing parts of the gutenberg-mobile PR description. When filling in the "Changes" section, link to the most descriptive GitHub issue + for any given change and consider adding a short description. Testers rely on this section to gather more details about changes in a release.` }} + +{{ Task "Mark all 4 PRs ready for review and request review from your release wrangler buddy." }} + +{{ if not .Scheduled }} + + +

⚠️ In some release cases (like beta fixes), it's likely that the PRs could have conflicts with trunk. In this case, do not resolve merge conflicts by merging with trunk as this will introduce new and unexpected changes to the release. Instead, leave the conflicts until the release is integrated into the main apps, and then resolve the conflicts when merging the PRs back to trunk. Optionally, a second clone of the release branch can be created to verify the CI checks

+ +{{ end }} + +{{ if not .Scheduled }} +{{ Task `If this is a release for inclusion in the frozen WPiOS and WPAndroid release branches (i.e. this is a beta/hot fix, e.g. X.XX.2), + ping the directly responsible individual handing the release of each platform of the main apps.` }} +{{ end }} + +{{ if and .IncludeAztec .CheckAztec }} +{{ RenderAztecSteps (not .CheckAztec ) }} +{{ end }} + + +

Manage Incoming Changes (conditional)

+ + + +

ℹ️ If additional changes (e.g. bug fixes) were merged into the gutenberg-mobile release/{{ .Version }} or in gutenberg rnmobile/release-{{ .Version }} branches, the steps in this section need to be completed."

+ + +{{ Task `After a merge happened in gutenberg-mobile release/%s or in gutenberg rnmobile/release-%s, +ensure the gutenberg submodule points to the correct hash and the rnmobile/release-%s in the gutenberg repo branch has been updated.` .Version .Version .Version }} + +{{ Task `If there were changes in gutenberg repo, make sure to cherry-pick the changes that landed in the trunk branch back to the release branch +and don't forget to run npm run bundle in gutenberg-mobile again if necessary.` }} + +{{ Task `Add the new change to the "Extra PRs that Landed After the Release Was Cut" section of the gutenberg-mobile PR description.` }} + + +

Integrate the Release{{ if .Scheduled }} (Thursday) {{end}}

+ + +{{ Task `Verify the gutenberg ref within the gutenberg-mobile release branch is pointed to the latest commit in the gutenberg release branch.` }} + +{{ Task `Create and push a rnmobile/%s git tag for the head of gutenberg release branch.` .Version }} + +{{ Task `Ensure that the bundle files are updated to include any changes to the release branch by running npm run bundle in gutenberg-mobile release branch and committing any changes.` }} + +{{ Task `Create a new gutenberg-mobile GitHub Release. Include a list of changes in the release description. Ensure the checkmark "Set as the latest release" is checked, and publish the release with the "Publish release" button.` .ReleaseUrl }} + +{{ Task `Wait until all CI jobs for the published tag finish and succeed.` }} + + +
+{{ Task `Navigate to the Buildkite job that built the JS bundles (Build JS Bundles) for the published tag. Open the job and navigate to the "Artifacts" tab. Locate the composed source maps (they have file name bundle/{platform}/App.composed.js.map) and download them.` }} + +
    +
  • File: bundle/android/App.composed.js.map – Artifact name: android-App.js.map
  • + + + +
  • File: bundle/ios/App.composed.js.map – Artifact name: ios-App.js.map
  • +
+ +
+ + +{{ Task `In WPiOS, update the reference to point to the tag of the Release created in the previous task.` }} + +{{ Task `In WPAndroid, update the gutenbergMobileVersion in build.gradle to point to the tag of the Release used in the previous task.` }} + +{{ Task `Main apps PRs should be ready to merge to their trunk branches now. Merge them or get them merged.`}} + + +
+{{ if not .Scheduled }} +{{ Task `Once everything is merged, send a heads up to our friends in the #apps-infrastructure Slack channel. Since this is a beta/hot fix (e.g. X.XX.2), directly mention the relevant Excellence Wranglers for the release with the following message:` }} + +
+

Hey team. I wanted to let you know that the mobile Gutenberg team has finished integrating the {{ .Version }} Gutenberg release into the WPiOS and WPAndroid release/{{ .HostVersion }} branches, ready for a new beta when you are available. Please let me know if you have any questions. Thanks!

+
+ +{{ else }} +{{ Task `Once everything is merged, send a heads up to our friends in the #apps-infrastructure Slack channel with the following message:` }} + +
+

Hey team. I wanted to let you know that the mobile Gutenberg team has finished integrating the {{ .Version }} Gutenberg release into the WPiOS and WPAndroid trunk branches. The integration is ready for the next release cut/build creation when you are available. Please let me know if you have any questions. Thanks!

+
+ +{{ end }} +
+ + +{{ Task `Close the Gutenberg Mobile milestone that corresponds to this release.` }} + + +

Merge Release Branches

+ + +{{ Task `Resolve any conflicts with trunk and merge the gutenberg PR.` }} + +{{ Task `Update the gutenberg reference on the gutenberg-mobile release branch to point to the Gutenberg PR merge commit` }} + +{{ Task `Merge the gutenberg-mobile PR to trunk. Use "Create a merge commit" option when merging to avoid losing any commit history from the release branch.` }} + + +

Clean Up Pending Work (After main apps cut)

+ + + +

+ ⚠️ This section may only be completed after the main apps cut their own release branches. +

+ + +{{ Task `Update the gutenberg/after_%s branches and open a PR against trunk. If the branches are empty we’ll just delete them. +The PR can actually get created as soon as something gets merged to the gutenberg/after_%s branches. +Merge the gutenberg/after_%s PR(s) only AFTER the main apps have cut their release branches.` .Version .Version .Version}} + + +

Test the Release

+ + + +

ℹ️ Use the main WP apps to complete each the tasks below for both iOS and Android.

+ + +{{ Task `Test the new changes that are included in the release PR.` }} + +{{ Task `Complete the general writing flow test cases.` }} + +{{ Task `Complete the Unsupported Block Editor test cases.` }} + +{{ Task `Complete the functionality tests scheduled for Android` }} + +{{ Task `Complete the functionality tests scheduled for iOS` }} + + +

For the remainder of the main app release period, monitor main app release P2 posts for issues found.

+ + + +

Finish the Release

+ + +{{ Task `Update the Release Incident Spreadsheet with any fixes that occurred after the release branches were cut.` }} + +{{ if not .Scheduled }} +{{ Task `Message the next release wrangler in the #mobile-gutenberg-platform Slack channel providing them with a tentative schedule +for the next release. This will help ensure a smooth hand off and sets expectations for when they should begin their work.` }} +{{ end }} +{{ Task `Celebrate! 🎉` }} + + +

+mobilegutenberg

+ + + +

#gutenberg-mobile

+ diff --git a/cli/templates/checklist/task.html b/cli/templates/checklist/task.html new file mode 100644 index 00000000..3f2f7a15 --- /dev/null +++ b/cli/templates/checklist/task.html @@ -0,0 +1,21 @@ + +
+
+ +
+ +
+
+
+
+
+ {{ .Description }} +
+
+
+
+
+
+
+
+ \ No newline at end of file