Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 6f82ddd

Browse files
committed
chore(scripts): add release autoversion to bump releases in docs
1 parent 6898381 commit 6f82ddd

File tree

3 files changed

+232
-15
lines changed

3 files changed

+232
-15
lines changed

docs/install/kubernetes.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ locally in order to log in and manage templates.
128128

129129
For the **mainline** Coder release:
130130

131+
<!-- autoversion(mainline): "--version [version]" -->
131132
```shell
132133
helm install coder coder-v2/coder \
133134
--namespace coder \
@@ -137,6 +138,7 @@ locally in order to log in and manage templates.
137138

138139
For the **stable** Coder release:
139140

141+
<!-- autoversion(stable): "--version [version]" -->
140142
```shell
141143
helm install coder coder-v2/coder \
142144
--namespace coder \

scripts/release/main.go

Lines changed: 180 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"io/fs"
78
"os"
9+
"path/filepath"
10+
"regexp"
811
"slices"
912
"strings"
1013
"time"
1114

1215
"github.com/google/go-cmp/cmp"
1316
"github.com/google/go-github/v61/github"
17+
"github.com/spf13/afero"
1418
"golang.org/x/mod/semver"
1519
"golang.org/x/xerrors"
1620

@@ -26,42 +30,75 @@ const (
2630
)
2731

2832
func main() {
29-
logger := slog.Make(sloghuman.Sink(os.Stderr)).Leveled(slog.LevelDebug)
33+
r := &releaseCommand{
34+
fs: afero.NewOsFs(),
35+
logger: slog.Make(sloghuman.Sink(os.Stderr)).Leveled(slog.LevelInfo),
36+
}
3037

31-
var ghToken string
32-
var dryRun bool
38+
var channel string
3339

3440
cmd := serpent.Command{
3541
Use: "release <subcommand>",
3642
Short: "Prepare, create and publish releases.",
3743
Options: serpent.OptionSet{
44+
{
45+
Flag: "debug",
46+
Description: "Enable debug logging.",
47+
Value: serpent.BoolOf(&r.debug),
48+
},
3849
{
3950
Flag: "gh-token",
4051
Description: "GitHub personal access token.",
4152
Env: "GH_TOKEN",
42-
Value: serpent.StringOf(&ghToken),
53+
Value: serpent.StringOf(&r.ghToken),
4354
},
4455
{
4556
Flag: "dry-run",
4657
FlagShorthand: "n",
4758
Description: "Do not make any changes, only print what would be done.",
48-
Value: serpent.BoolOf(&dryRun),
59+
Value: serpent.BoolOf(&r.dryRun),
4960
},
5061
},
5162
Children: []*serpent.Command{
5263
{
53-
Use: "promote <version>",
54-
Short: "Promote version to stable.",
64+
Use: "promote <version>",
65+
Short: "Promote version to stable.",
66+
Middleware: r.debugMiddleware, // Serpent doesn't support this on parent.
5567
Handler: func(inv *serpent.Invocation) error {
5668
ctx := inv.Context()
5769
if len(inv.Args) == 0 {
5870
return xerrors.New("version argument missing")
5971
}
60-
if !dryRun && ghToken == "" {
72+
if !r.dryRun && r.ghToken == "" {
6173
return xerrors.New("GitHub personal access token is required, use --gh-token or GH_TOKEN")
6274
}
6375

64-
err := promoteVersionToStable(ctx, inv, logger, ghToken, dryRun, inv.Args[0])
76+
err := r.promoteVersionToStable(ctx, inv, inv.Args[0])
77+
if err != nil {
78+
return err
79+
}
80+
81+
return nil
82+
},
83+
},
84+
{
85+
Use: "autoversion <version>",
86+
Short: "Automatically update the provided channel to version in markdown files.",
87+
Options: serpent.OptionSet{
88+
{
89+
Flag: "channel",
90+
Description: "Channel to update.",
91+
Value: serpent.EnumOf(&channel, "mainline", "stable"),
92+
},
93+
},
94+
Middleware: r.debugMiddleware, // Serpent doesn't support this on parent.
95+
Handler: func(inv *serpent.Invocation) error {
96+
ctx := inv.Context()
97+
if len(inv.Args) == 0 {
98+
return xerrors.New("version argument missing")
99+
}
100+
101+
err := r.autoversion(ctx, channel, inv.Args[0])
65102
if err != nil {
66103
return err
67104
}
@@ -77,19 +114,36 @@ func main() {
77114
if errors.Is(err, cliui.Canceled) {
78115
os.Exit(1)
79116
}
80-
logger.Error(context.Background(), "release command failed", "err", err)
117+
r.logger.Error(context.Background(), "release command failed", "err", err)
81118
os.Exit(1)
82119
}
83120
}
84121

122+
type releaseCommand struct {
123+
fs afero.Fs
124+
logger slog.Logger
125+
debug bool
126+
ghToken string
127+
dryRun bool
128+
}
129+
130+
func (r *releaseCommand) debugMiddleware(next serpent.HandlerFunc) serpent.HandlerFunc {
131+
return func(inv *serpent.Invocation) error {
132+
if r.debug {
133+
r.logger = r.logger.Leveled(slog.LevelDebug)
134+
}
135+
return next(inv)
136+
}
137+
}
138+
85139
//nolint:revive // Allow dryRun control flag.
86-
func promoteVersionToStable(ctx context.Context, inv *serpent.Invocation, logger slog.Logger, ghToken string, dryRun bool, version string) error {
140+
func (r *releaseCommand) promoteVersionToStable(ctx context.Context, inv *serpent.Invocation, version string) error {
87141
client := github.NewClient(nil)
88-
if ghToken != "" {
89-
client = client.WithAuthToken(ghToken)
142+
if r.ghToken != "" {
143+
client = client.WithAuthToken(r.ghToken)
90144
}
91145

92-
logger = logger.With(slog.F("dry_run", dryRun), slog.F("version", version))
146+
logger := r.logger.With(slog.F("dry_run", r.dryRun), slog.F("version", version))
93147

94148
logger.Info(ctx, "checking current stable release")
95149

@@ -161,7 +215,7 @@ func promoteVersionToStable(ctx context.Context, inv *serpent.Invocation, logger
161215
updatedNewStable.Body = github.String(updatedBody)
162216
updatedNewStable.Prerelease = github.Bool(false)
163217
updatedNewStable.Draft = github.Bool(false)
164-
if !dryRun {
218+
if !r.dryRun {
165219
_, _, err = client.Repositories.EditRelease(ctx, owner, repo, newStable.GetID(), newStable)
166220
if err != nil {
167221
return xerrors.Errorf("edit release failed: %w", err)
@@ -221,3 +275,114 @@ func removeMainlineBlurb(body string) string {
221275

222276
return strings.Join(newBody, "\n")
223277
}
278+
279+
// autoversion automatically updates the provided channel to version in markdown
280+
// files.
281+
func (r *releaseCommand) autoversion(ctx context.Context, channel, version string) error {
282+
var files []string
283+
284+
// For now, scope this to docs, perhaps we include README.md in the future.
285+
if err := afero.Walk(r.fs, "docs", func(path string, _ fs.FileInfo, err error) error {
286+
if err != nil {
287+
return err
288+
}
289+
if strings.EqualFold(filepath.Ext(path), ".md") {
290+
files = append(files, path)
291+
}
292+
return nil
293+
}); err != nil {
294+
return xerrors.Errorf("walk failed: %w", err)
295+
}
296+
297+
for _, file := range files {
298+
err := r.autoversionFile(ctx, file, channel, version)
299+
if err != nil {
300+
return xerrors.Errorf("autoversion file failed: %w", err)
301+
}
302+
}
303+
304+
return nil
305+
}
306+
307+
// autoversionMarkdownPragmaRe matches the autoversion pragma in markdown files.
308+
//
309+
// Example:
310+
//
311+
// <!-- autoversion(stable): "--version [version]"" -->
312+
//
313+
// The channel is the first capture group and the match string is the second
314+
// capture group. The string "[version]" is replaced with the new version.
315+
var autoversionMarkdownPragmaRe = regexp.MustCompile(`<!-- ?autoversion\(([^)]+)\): ?"([^"]+)" ?-->`)
316+
317+
func (r *releaseCommand) autoversionFile(ctx context.Context, file, channel, version string) error {
318+
version = strings.TrimPrefix(version, "v")
319+
logger := r.logger.With(slog.F("file", file), slog.F("channel", channel), slog.F("version", version))
320+
321+
logger.Debug(ctx, "checking file for autoversion pragma")
322+
323+
contents, err := afero.ReadFile(r.fs, file)
324+
if err != nil {
325+
return xerrors.Errorf("read file failed: %w", err)
326+
}
327+
328+
lines := strings.Split(string(contents), "\n")
329+
var matchRe *regexp.Regexp
330+
for i, line := range lines {
331+
if autoversionMarkdownPragmaRe.MatchString(line) {
332+
matches := autoversionMarkdownPragmaRe.FindStringSubmatch(line)
333+
matchChannel := matches[1]
334+
match := matches[2]
335+
336+
logger := logger.With(slog.F("line_number", i+1), slog.F("match_channel", matchChannel), slog.F("match", match))
337+
338+
logger.Debug(ctx, "autoversion pragma detected")
339+
340+
if matchChannel != channel {
341+
logger.Debug(ctx, "channel mismatch, skipping")
342+
continue
343+
}
344+
345+
logger.Info(ctx, "autoversion pragma found with channel match")
346+
347+
match = strings.Replace(match, "[version]", `(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`, 1)
348+
logger.Debug(ctx, "compiling match regexp", "match", match)
349+
matchRe, err = regexp.Compile(match)
350+
if err != nil {
351+
return xerrors.Errorf("regexp compile failed: %w", err)
352+
}
353+
}
354+
if matchRe != nil {
355+
// Apply matchRe and find the group named "version", then replace it with the new version.
356+
// Utilize the index where the match was found to replace the correct part. The only
357+
// match group is the version.
358+
if match := matchRe.FindStringSubmatchIndex(line); match != nil {
359+
logger.Info(ctx, "updating version number", "line_number", i+1, "match", match)
360+
lines[i] = line[:match[2]] + version + line[match[3]:]
361+
matchRe = nil
362+
break
363+
}
364+
}
365+
}
366+
if matchRe != nil {
367+
return xerrors.Errorf("match not found in file")
368+
}
369+
370+
updated := strings.Join(lines, "\n")
371+
372+
// Only update the file if there are changes.
373+
diff := cmp.Diff(string(contents), updated)
374+
if diff == "" {
375+
return nil
376+
}
377+
378+
if !r.dryRun {
379+
if err := afero.WriteFile(r.fs, file, []byte(updated), 0o644); err != nil {
380+
return xerrors.Errorf("write file failed: %w", err)
381+
}
382+
logger.Info(ctx, "file autoversioned")
383+
} else {
384+
logger.Info(ctx, "dry-run: file not updated", "uncommitted_changes", diff)
385+
}
386+
387+
return nil
388+
}

scripts/release/main_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package main
22

33
import (
4+
"context"
5+
"path/filepath"
46
"testing"
57
"time"
68

79
"github.com/google/go-cmp/cmp"
10+
"github.com/spf13/afero"
811
)
912

1013
func Test_removeMainlineBlurb(t *testing.T) {
@@ -134,3 +137,50 @@ func Test_addStableSince(t *testing.T) {
134137
t.Errorf("addStableSince() mismatch (-want +got):\n%s", diff)
135138
}
136139
}
140+
141+
func Test_release_autoversion(t *testing.T) {
142+
t.Parallel()
143+
144+
ctx := context.Background()
145+
dir := filepath.Join("testdata", "autoversion")
146+
147+
fs := afero.NewCopyOnWriteFs(afero.NewOsFs(), afero.NewMemMapFs())
148+
r := releaseCommand{
149+
fs: afero.NewBasePathFs(fs, dir),
150+
}
151+
152+
err := r.autoversion(ctx, "mainline", "v2.11.1")
153+
if err != nil {
154+
t.Fatal(err)
155+
}
156+
157+
err = r.autoversion(ctx, "stable", "v2.9.4")
158+
if err != nil {
159+
t.Fatal(err)
160+
}
161+
162+
files, err := filepath.Glob(filepath.Join(dir, "docs", "*.md"))
163+
if err != nil {
164+
t.Fatal(err)
165+
}
166+
167+
for _, file := range files {
168+
file := file
169+
t.Run(file, func(t *testing.T) {
170+
t.Parallel()
171+
172+
got, err := afero.ReadFile(fs, file)
173+
if err != nil {
174+
t.Fatal(err)
175+
}
176+
want, err := afero.ReadFile(fs, file+".golden")
177+
if err != nil {
178+
t.Fatal(err)
179+
}
180+
181+
if diff := cmp.Diff(string(got), string(want)); diff != "" {
182+
t.Errorf("mismatch (-want +got):\n%s", diff)
183+
}
184+
})
185+
}
186+
}

0 commit comments

Comments
 (0)