Skip to content

chore(scripts): add script to promote mainline to stable #13054

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
223 changes: 223 additions & 0 deletions scripts/release/main.go
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!!

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")
}
136 changes: 136 additions & 0 deletions scripts/release/main_test.go
Original file line number Diff line number Diff line change
@@ -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).
Copy link
Contributor

@dannykopping dannykopping Apr 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be illustrative / more complete to add a test which shows that multiple lines subsequent to [!NOTE] prefixed with > are removed; we'll likely amend this note in the future.


### 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)
}
}
10 changes: 10 additions & 0 deletions scripts/release_promote_stable.sh
Original file line number Diff line number Diff line change
@@ -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