mirror of https://github.com/coder/coder.git
chore(scripts): add script to promote mainline to stable (#13054)
Fixes #12459 Example dry-run: <img width="1229" alt="Screenshot 2024-04-23 at 21 16 55" src="https://github.com/coder/coder/assets/147409/7018d322-501b-41e2-bf47-af3fc39fb3d2"> Example dry-run for non-latest version: <img width="1228" alt="Screenshot 2024-04-23 at 21 17 52" src="https://github.com/coder/coder/assets/147409/a05fcd44-560f-4e44-81b5-76c071c591b4"> **Note:** This PR does not yet update docs to reflect the promoted version. This will be part of #12465.
This commit is contained in:
parent
b82a782619
commit
c933c75aa7
1
go.mod
1
go.mod
|
@ -218,6 +218,7 @@ require (
|
|||
github.com/benbjohnson/clock v1.3.5
|
||||
github.com/coder/serpent v0.7.0
|
||||
github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47
|
||||
github.com/google/go-github/v61 v61.0.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
|
2
go.sum
2
go.sum
|
@ -469,6 +469,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
|||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-github/v43 v43.0.1-0.20220414155304-00e42332e405 h1:DdHws/YnnPrSywrjNYu2lEHqYHWp/LnEx56w59esd54=
|
||||
github.com/google/go-github/v43 v43.0.1-0.20220414155304-00e42332e405/go.mod h1:4RgUDSnsxP19d65zJWqvqJ/poJxBCvmna50eXmIvoR8=
|
||||
github.com/google/go-github/v61 v61.0.0 h1:VwQCBwhyE9JclCI+22/7mLB1PuU9eowCXKY5pNlu1go=
|
||||
github.com/google/go-github/v61 v61.0.0/go.mod h1:0WR+KmsWX75G2EbpyGsGmradjo3IiciuI4BmdVCobQY=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
|
|
|
@ -0,0 +1,223 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-github/v61/github"
|
||||
"golang.org/x/mod/semver"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"cdr.dev/slog/sloggers/sloghuman"
|
||||
"github.com/coder/coder/v2/cli/cliui"
|
||||
"github.com/coder/serpent"
|
||||
)
|
||||
|
||||
const (
|
||||
owner = "coder"
|
||||
repo = "coder"
|
||||
)
|
||||
|
||||
func main() {
|
||||
logger := slog.Make(sloghuman.Sink(os.Stderr)).Leveled(slog.LevelDebug)
|
||||
|
||||
var ghToken string
|
||||
var dryRun bool
|
||||
|
||||
cmd := serpent.Command{
|
||||
Use: "release <subcommand>",
|
||||
Short: "Prepare, create and publish releases.",
|
||||
Options: serpent.OptionSet{
|
||||
{
|
||||
Flag: "gh-token",
|
||||
Description: "GitHub personal access token.",
|
||||
Env: "GH_TOKEN",
|
||||
Value: serpent.StringOf(&ghToken),
|
||||
},
|
||||
{
|
||||
Flag: "dry-run",
|
||||
FlagShorthand: "n",
|
||||
Description: "Do not make any changes, only print what would be done.",
|
||||
Value: serpent.BoolOf(&dryRun),
|
||||
},
|
||||
},
|
||||
Children: []*serpent.Command{
|
||||
{
|
||||
Use: "promote <version>",
|
||||
Short: "Promote version to stable.",
|
||||
Handler: func(inv *serpent.Invocation) error {
|
||||
ctx := inv.Context()
|
||||
if len(inv.Args) == 0 {
|
||||
return xerrors.New("version argument missing")
|
||||
}
|
||||
if !dryRun && ghToken == "" {
|
||||
return xerrors.New("GitHub personal access token is required, use --gh-token or GH_TOKEN")
|
||||
}
|
||||
|
||||
err := promoteVersionToStable(ctx, inv, logger, ghToken, dryRun, inv.Args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := cmd.Invoke().WithOS().Run()
|
||||
if err != nil {
|
||||
if errors.Is(err, cliui.Canceled) {
|
||||
os.Exit(1)
|
||||
}
|
||||
logger.Error(context.Background(), "release command failed", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:revive // Allow dryRun control flag.
|
||||
func promoteVersionToStable(ctx context.Context, inv *serpent.Invocation, logger slog.Logger, ghToken string, dryRun bool, version string) error {
|
||||
client := github.NewClient(nil)
|
||||
if ghToken != "" {
|
||||
client = client.WithAuthToken(ghToken)
|
||||
}
|
||||
|
||||
logger = logger.With(slog.F("dry_run", dryRun), slog.F("version", version))
|
||||
|
||||
logger.Info(ctx, "checking current stable release")
|
||||
|
||||
// Check if the version is already the latest stable release.
|
||||
currentStable, _, err := client.Repositories.GetLatestRelease(ctx, "coder", "coder")
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get latest release failed: %w", err)
|
||||
}
|
||||
|
||||
logger = logger.With(slog.F("stable_version", currentStable.GetTagName()))
|
||||
logger.Info(ctx, "found current stable release")
|
||||
|
||||
if currentStable.GetTagName() == version {
|
||||
return xerrors.Errorf("version %q is already the latest stable release", version)
|
||||
}
|
||||
|
||||
// Ensure the version is a valid release.
|
||||
perPage := 20
|
||||
latestReleases, _, err := client.Repositories.ListReleases(ctx, owner, repo, &github.ListOptions{
|
||||
Page: 0,
|
||||
PerPage: perPage,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("list releases failed: %w", err)
|
||||
}
|
||||
|
||||
var releaseVersions []string
|
||||
var newStable *github.RepositoryRelease
|
||||
for _, r := range latestReleases {
|
||||
releaseVersions = append(releaseVersions, r.GetTagName())
|
||||
if r.GetTagName() == version {
|
||||
newStable = r
|
||||
}
|
||||
}
|
||||
semver.Sort(releaseVersions)
|
||||
slices.Reverse(releaseVersions)
|
||||
|
||||
switch {
|
||||
case len(releaseVersions) == 0:
|
||||
return xerrors.Errorf("no releases found")
|
||||
case newStable == nil:
|
||||
return xerrors.Errorf("version %q is not found in the last %d releases", version, perPage)
|
||||
}
|
||||
|
||||
logger = logger.With(slog.F("mainline_version", releaseVersions[0]))
|
||||
|
||||
if version != releaseVersions[0] {
|
||||
logger.Warn(ctx, "selected version is not the latest mainline release")
|
||||
}
|
||||
|
||||
if reply, err := cliui.Prompt(inv, cliui.PromptOptions{
|
||||
Text: "Are you sure you want to promote this version to stable?",
|
||||
Default: "no",
|
||||
IsConfirm: true,
|
||||
}); err != nil {
|
||||
if reply == cliui.ConfirmNo {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info(ctx, "promoting selected version to stable")
|
||||
|
||||
// Update the release to latest.
|
||||
updatedNewStable := cloneRelease(newStable)
|
||||
|
||||
updatedBody := removeMainlineBlurb(newStable.GetBody())
|
||||
updatedBody = addStableSince(time.Now().UTC(), updatedBody)
|
||||
updatedNewStable.Body = github.String(updatedBody)
|
||||
updatedNewStable.Prerelease = github.Bool(false)
|
||||
updatedNewStable.Draft = github.Bool(false)
|
||||
if !dryRun {
|
||||
_, _, err = client.Repositories.EditRelease(ctx, owner, repo, newStable.GetID(), newStable)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("edit release failed: %w", err)
|
||||
}
|
||||
logger.Info(ctx, "selected version promoted to stable", "url", newStable.GetHTMLURL())
|
||||
} else {
|
||||
logger.Info(ctx, "dry-run: release not updated", "uncommitted_changes", cmp.Diff(newStable, updatedNewStable))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func cloneRelease(r *github.RepositoryRelease) *github.RepositoryRelease {
|
||||
rr := *r
|
||||
return &rr
|
||||
}
|
||||
|
||||
// addStableSince adds a stable since note to the release body.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// > ## Stable (since April 23, 2024)
|
||||
func addStableSince(date time.Time, body string) string {
|
||||
return fmt.Sprintf("> ## Stable (since %s)\n\n", date.Format("January 02, 2006")) + body
|
||||
}
|
||||
|
||||
// removeMainlineBlurb removes the mainline blurb from the release body.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// > [!NOTE]
|
||||
// > This is a mainline Coder release. We advise enterprise customers without a staging environment to install our [latest stable release](https://github.com/coder/coder/releases/latest) while we refine this version. Learn more about our [Release Schedule](https://coder.com/docs/v2/latest/install/releases).
|
||||
func removeMainlineBlurb(body string) string {
|
||||
lines := strings.Split(body, "\n")
|
||||
|
||||
var newBody, clip []string
|
||||
var found bool
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(strings.TrimSpace(line), "> [!NOTE]") {
|
||||
clip = append(clip, line)
|
||||
found = true
|
||||
continue
|
||||
}
|
||||
if found {
|
||||
clip = append(clip, line)
|
||||
found = strings.HasPrefix(strings.TrimSpace(line), ">")
|
||||
continue
|
||||
}
|
||||
if !found && len(clip) > 0 {
|
||||
if !strings.Contains(strings.ToLower(strings.Join(clip, "\n")), "this is a mainline coder release") {
|
||||
newBody = append(newBody, clip...) // This is some other note, restore it.
|
||||
}
|
||||
clip = nil
|
||||
}
|
||||
newBody = append(newBody, line)
|
||||
}
|
||||
|
||||
return strings.Join(newBody, "\n")
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func Test_removeMainlineBlurb(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "NoMainlineBlurb",
|
||||
body: `## Changelog
|
||||
|
||||
### Chores
|
||||
|
||||
- Add support for additional Azure Instance Identity RSA Certificates (#13028) (@kylecarbs)
|
||||
|
||||
Compare: [` + "`" + `v2.10.1...v2.10.2` + "`" + `](https://github.com/coder/coder/compare/v2.10.1...v2.10.2)
|
||||
|
||||
## Container image
|
||||
|
||||
- ` + "`" + `docker pull ghcr.io/coder/coder:v2.10.2` + "`" + `
|
||||
|
||||
## Install/upgrade
|
||||
|
||||
Refer to our docs to [install](https://coder.com/docs/v2/latest/install) or [upgrade](https://coder.com/docs/v2/latest/admin/upgrade) Coder, or use a release asset below.
|
||||
`,
|
||||
want: `## Changelog
|
||||
|
||||
### Chores
|
||||
|
||||
- Add support for additional Azure Instance Identity RSA Certificates (#13028) (@kylecarbs)
|
||||
|
||||
Compare: [` + "`" + `v2.10.1...v2.10.2` + "`" + `](https://github.com/coder/coder/compare/v2.10.1...v2.10.2)
|
||||
|
||||
## Container image
|
||||
|
||||
- ` + "`" + `docker pull ghcr.io/coder/coder:v2.10.2` + "`" + `
|
||||
|
||||
## Install/upgrade
|
||||
|
||||
Refer to our docs to [install](https://coder.com/docs/v2/latest/install) or [upgrade](https://coder.com/docs/v2/latest/admin/upgrade) Coder, or use a release asset below.
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "WithMainlineBlurb",
|
||||
body: `## Changelog
|
||||
|
||||
> [!NOTE]
|
||||
> This is a mainline Coder release. We advise enterprise customers without a staging environment to install our [latest stable release](https://github.com/coder/coder/releases/latest) while we refine this version. Learn more about our [Release Schedule](https://coder.com/docs/v2/latest/install/releases).
|
||||
|
||||
### Chores
|
||||
|
||||
- Add support for additional Azure Instance Identity RSA Certificates (#13028) (@kylecarbs)
|
||||
|
||||
Compare: [` + "`" + `v2.10.1...v2.10.2` + "`" + `](https://github.com/coder/coder/compare/v2.10.1...v2.10.2)
|
||||
|
||||
## Container image
|
||||
|
||||
- ` + "`" + `docker pull ghcr.io/coder/coder:v2.10.2` + "`" + `
|
||||
|
||||
## Install/upgrade
|
||||
|
||||
Refer to our docs to [install](https://coder.com/docs/v2/latest/install) or [upgrade](https://coder.com/docs/v2/latest/admin/upgrade) Coder, or use a release asset below.
|
||||
`,
|
||||
want: `## Changelog
|
||||
|
||||
### Chores
|
||||
|
||||
- Add support for additional Azure Instance Identity RSA Certificates (#13028) (@kylecarbs)
|
||||
|
||||
Compare: [` + "`" + `v2.10.1...v2.10.2` + "`" + `](https://github.com/coder/coder/compare/v2.10.1...v2.10.2)
|
||||
|
||||
## Container image
|
||||
|
||||
- ` + "`" + `docker pull ghcr.io/coder/coder:v2.10.2` + "`" + `
|
||||
|
||||
## Install/upgrade
|
||||
|
||||
Refer to our docs to [install](https://coder.com/docs/v2/latest/install) or [upgrade](https://coder.com/docs/v2/latest/admin/upgrade) Coder, or use a release asset below.
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "EntireQuotedBlurbIsRemoved",
|
||||
body: `## Changelog
|
||||
|
||||
> [!NOTE]
|
||||
> This is a mainline Coder release. We advise enterprise customers without a staging environment to install our [latest stable release](https://github.com/coder/coder/releases/latest) while we refine this version. Learn more about our [Release Schedule](https://coder.com/docs/v2/latest/install/releases).
|
||||
> This is an extended note.
|
||||
> This is another extended note.
|
||||
|
||||
### Best release yet!
|
||||
|
||||
Enjoy.
|
||||
`,
|
||||
want: `## Changelog
|
||||
|
||||
### Best release yet!
|
||||
|
||||
Enjoy.
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if diff := cmp.Diff(removeMainlineBlurb(tt.body), tt.want); diff != "" {
|
||||
t.Errorf("removeMainlineBlurb() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_addStableSince(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
date := time.Date(2024, time.April, 23, 0, 0, 0, 0, time.UTC)
|
||||
body := "## Changelog"
|
||||
|
||||
expected := "> ## Stable (since April 23, 2024)\n\n## Changelog"
|
||||
result := addStableSince(date, body)
|
||||
|
||||
if diff := cmp.Diff(expected, result); diff != "" {
|
||||
t.Errorf("addStableSince() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
# shellcheck source=scripts/lib.sh
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
|
||||
|
||||
# This script is a convenience wrapper around the release promote command.
|
||||
#
|
||||
# Sed hack to make help text look like this script.
|
||||
exec go run "${SCRIPT_DIR}/release" promote "$@"
|
Loading…
Reference in New Issue