Skip to content

Commit 2f5d178

Browse files
committed
feat(scripts): add script to promote mainline to stable
1 parent 215dd7b commit 2f5d178

File tree

3 files changed

+325
-0
lines changed

3 files changed

+325
-0
lines changed

scripts/release/main.go

+214
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"errors"
6+
"os"
7+
"slices"
8+
"strings"
9+
10+
"github.com/google/go-cmp/cmp"
11+
"github.com/google/go-github/v43/github"
12+
"golang.org/x/mod/semver"
13+
"golang.org/x/xerrors"
14+
15+
"cdr.dev/slog"
16+
"cdr.dev/slog/sloggers/sloghuman"
17+
"github.com/coder/coder/v2/cli/cliui"
18+
"github.com/coder/serpent"
19+
)
20+
21+
const (
22+
owner = "coder"
23+
repo = "coder"
24+
)
25+
26+
func main() {
27+
logger := slog.Make(sloghuman.Sink(os.Stderr)).Leveled(slog.LevelDebug)
28+
29+
var dryRun bool
30+
31+
cmd := serpent.Command{
32+
Use: "release <subcommand>",
33+
Short: "Prepare, create and publish releases.",
34+
Options: serpent.OptionSet{
35+
{
36+
Flag: "dry-run",
37+
FlagShorthand: "n",
38+
Description: "Do not make any changes, only print what would be done.",
39+
Value: serpent.BoolOf(&dryRun),
40+
},
41+
},
42+
Children: []*serpent.Command{
43+
{
44+
Use: "promote <version>",
45+
Short: "Promote version to stable.",
46+
Handler: func(inv *serpent.Invocation) error {
47+
ctx := inv.Context()
48+
if len(inv.Args) == 0 {
49+
return xerrors.New("version argument missing")
50+
}
51+
52+
err := promoteVersionToStable(ctx, inv, logger, dryRun, inv.Args[0])
53+
if err != nil {
54+
return err
55+
}
56+
57+
return nil
58+
},
59+
},
60+
},
61+
}
62+
63+
err := cmd.Invoke().WithOS().Run()
64+
if err != nil {
65+
if errors.Is(err, cliui.Canceled) {
66+
os.Exit(1)
67+
}
68+
logger.Error(context.Background(), "release command failed", "err", err)
69+
os.Exit(1)
70+
}
71+
}
72+
73+
//nolint:revive // Allow dryRun control flag.
74+
func promoteVersionToStable(ctx context.Context, inv *serpent.Invocation, logger slog.Logger, dryRun bool, version string) error {
75+
client := github.NewClient(nil)
76+
77+
logger = logger.With(slog.F("dry_run", dryRun), slog.F("version", version))
78+
79+
logger.Info(ctx, "checking current stable release")
80+
81+
// Check if the version is already the latest stable release.
82+
currentStable, _, err := client.Repositories.GetLatestRelease(ctx, "coder", "coder")
83+
if err != nil {
84+
return err
85+
}
86+
87+
logger = logger.With(slog.F("stable_version", currentStable.GetTagName()))
88+
logger.Info(ctx, "found current stable release")
89+
90+
if currentStable.GetTagName() == version {
91+
return xerrors.Errorf("version %q is already the latest stable release", version)
92+
}
93+
94+
// Ensure the version is a valid release.
95+
perPage := 20
96+
latestReleases, _, err := client.Repositories.ListReleases(ctx, owner, repo, &github.ListOptions{
97+
Page: 0,
98+
PerPage: perPage,
99+
})
100+
if err != nil {
101+
return err
102+
}
103+
104+
var releaseVersions []string
105+
var newStable *github.RepositoryRelease
106+
for _, r := range latestReleases {
107+
releaseVersions = append(releaseVersions, r.GetTagName())
108+
if r.GetTagName() == version {
109+
newStable = r
110+
}
111+
}
112+
semver.Sort(releaseVersions)
113+
slices.Reverse(releaseVersions)
114+
115+
switch {
116+
case len(releaseVersions) == 0:
117+
return xerrors.Errorf("no releases found")
118+
case newStable == nil:
119+
return xerrors.Errorf("version %q is not found in the last %d releases", version, perPage)
120+
}
121+
122+
logger = logger.With(slog.F("mainline_version", releaseVersions[0]))
123+
124+
if version != releaseVersions[0] {
125+
logger.Warn(ctx, "selected version is not the latest mainline release")
126+
}
127+
128+
if reply, err := cliui.Prompt(inv, cliui.PromptOptions{
129+
Text: "Are you sure you want to promote this version to stable?",
130+
Default: "no",
131+
IsConfirm: true,
132+
}); err != nil {
133+
if reply == cliui.ConfirmNo {
134+
return nil
135+
}
136+
return err
137+
}
138+
139+
logger.Info(ctx, "promoting selected version to stable")
140+
141+
// Update the release to latest.
142+
updatedNewStable := cloneRelease(newStable)
143+
144+
updatedNewStable.Name = github.String(newStable.GetName() + " (Stable)")
145+
updatedNewStable.Body = github.String(removeMainlineBlurb(newStable.GetBody()))
146+
updatedNewStable.Prerelease = github.Bool(false)
147+
updatedNewStable.Draft = github.Bool(false)
148+
if !dryRun {
149+
_, _, err = client.Repositories.EditRelease(ctx, owner, repo, newStable.GetID(), newStable)
150+
if err != nil {
151+
return err
152+
}
153+
logger.Info(ctx, "selected version promoted to stable")
154+
} else {
155+
logger.Info(ctx, "dry-run: release not updated", "uncommitted_changes", cmp.Diff(newStable, updatedNewStable))
156+
}
157+
158+
logger.Info(ctx, "updating title of the previous stable release (remove stable suffix)")
159+
160+
// Update the previous stable release to a regular release.
161+
updatedOldStable := cloneRelease(currentStable)
162+
currentStable.Name = github.String(strings.TrimSuffix(currentStable.GetName(), " (Stable)"))
163+
164+
if !dryRun {
165+
_, _, err = client.Repositories.EditRelease(ctx, owner, repo, currentStable.GetID(), currentStable)
166+
if err != nil {
167+
return err
168+
}
169+
logger.Info(ctx, "title of the previous stable release updated")
170+
} else {
171+
logger.Info(ctx, "dry-run: previous release not updated", "uncommitted_changes", cmp.Diff(currentStable, updatedOldStable))
172+
}
173+
174+
return nil
175+
}
176+
177+
func cloneRelease(r *github.RepositoryRelease) *github.RepositoryRelease {
178+
rr := *r
179+
return &rr
180+
}
181+
182+
// removeMainlineBlurb removes the mainline blurb from the release body.
183+
//
184+
// Example:
185+
//
186+
// > [!NOTE]
187+
// > 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).
188+
func removeMainlineBlurb(body string) string {
189+
lines := strings.Split(body, "\n")
190+
191+
var newBody, clip []string
192+
var found bool
193+
for _, line := range lines {
194+
if strings.HasPrefix(strings.TrimSpace(line), "> [!NOTE]") {
195+
clip = append(clip, line)
196+
found = true
197+
continue
198+
}
199+
if found {
200+
clip = append(clip, line)
201+
found = strings.HasPrefix(strings.TrimSpace(line), ">")
202+
continue
203+
}
204+
if !found && len(clip) > 0 {
205+
if !strings.Contains(strings.ToLower(strings.Join(clip, "\n")), "this is a mainline coder release") {
206+
newBody = append(newBody, clip...) // This is some other note, restore it.
207+
}
208+
clip = nil
209+
}
210+
newBody = append(newBody, line)
211+
}
212+
213+
return strings.Join(newBody, "\n")
214+
}

scripts/release/main_test.go

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package main
2+
3+
import (
4+
"testing"
5+
6+
"github.com/google/go-cmp/cmp"
7+
)
8+
9+
func Test_removeMainlineBlurb(t *testing.T) {
10+
t.Parallel()
11+
12+
tests := []struct {
13+
name string
14+
body string
15+
want string
16+
}{
17+
{
18+
name: "NoMainlineBlurb",
19+
body: `## Changelog
20+
21+
### Chores
22+
23+
- Add support for additional Azure Instance Identity RSA Certificates (#13028) (@kylecarbs)
24+
25+
Compare: [` + "`" + `v2.10.1...v2.10.2` + "`" + `](https://github.com/coder/coder/compare/v2.10.1...v2.10.2)
26+
27+
## Container image
28+
29+
- ` + "`" + `docker pull ghcr.io/coder/coder:v2.10.2` + "`" + `
30+
31+
## Install/upgrade
32+
33+
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.
34+
`,
35+
want: `## Changelog
36+
37+
### Chores
38+
39+
- Add support for additional Azure Instance Identity RSA Certificates (#13028) (@kylecarbs)
40+
41+
Compare: [` + "`" + `v2.10.1...v2.10.2` + "`" + `](https://github.com/coder/coder/compare/v2.10.1...v2.10.2)
42+
43+
## Container image
44+
45+
- ` + "`" + `docker pull ghcr.io/coder/coder:v2.10.2` + "`" + `
46+
47+
## Install/upgrade
48+
49+
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.
50+
`,
51+
},
52+
{
53+
name: "WithMainlineBlurb",
54+
body: `## Changelog
55+
56+
> [!NOTE]
57+
> 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).
58+
59+
### Chores
60+
61+
- Add support for additional Azure Instance Identity RSA Certificates (#13028) (@kylecarbs)
62+
63+
Compare: [` + "`" + `v2.10.1...v2.10.2` + "`" + `](https://github.com/coder/coder/compare/v2.10.1...v2.10.2)
64+
65+
## Container image
66+
67+
- ` + "`" + `docker pull ghcr.io/coder/coder:v2.10.2` + "`" + `
68+
69+
## Install/upgrade
70+
71+
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.
72+
`,
73+
want: `## Changelog
74+
75+
### Chores
76+
77+
- Add support for additional Azure Instance Identity RSA Certificates (#13028) (@kylecarbs)
78+
79+
Compare: [` + "`" + `v2.10.1...v2.10.2` + "`" + `](https://github.com/coder/coder/compare/v2.10.1...v2.10.2)
80+
81+
## Container image
82+
83+
- ` + "`" + `docker pull ghcr.io/coder/coder:v2.10.2` + "`" + `
84+
85+
## Install/upgrade
86+
87+
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.
88+
`,
89+
},
90+
}
91+
92+
for _, tt := range tests {
93+
tt := tt
94+
t.Run(tt.name, func(t *testing.T) {
95+
t.Parallel()
96+
if diff := cmp.Diff(removeMainlineBlurb(tt.body), tt.want); diff != "" {
97+
t.Errorf("removeMainlineBlurb() mismatch (-want +got):\n%s", diff)
98+
}
99+
})
100+
}
101+
}

scripts/release_promote_stable.sh

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
# shellcheck source=scripts/lib.sh
5+
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
6+
7+
# This script is a convenience wrapper around the release promote command.
8+
#
9+
# Sed hack to make help text look like this script.
10+
exec go run "${SCRIPT_DIR}/release" promote "$@"

0 commit comments

Comments
 (0)