mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
Replace the local interactive release CLI and legacy shell scripts with a non-interactive Go tool (`scripts/release-action/`) and a rewritten `release.yaml` workflow. Release managers trigger releases from the GitHub Actions UI by selecting a branch, picking a release type (`rc`, `release`, or `create-release-branch`), and optionally providing a commit SHA. The Go tool has four subcommands: `calculate-version` (computes next version from git state), `generate-notes` (release notes from commit log and PR metadata), `publish` (creates GitHub release with checksums), and the workflow handles tag creation, branch creation, building, and downstream publishing. `scripts/version.sh` fallback now uses `git describe` (nearest ancestor tag) instead of global latest so dev builds on release branches show the correct version series.
428 lines
9.5 KiB
Go
428 lines
9.5 KiB
Go
package main
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func Test_versionIsLess(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
a, b version
|
|
want bool
|
|
}{
|
|
{
|
|
name: "major_less",
|
|
a: version{major: 1, minor: 0, patch: 0, rc: -1, original: "v1.0.0"},
|
|
b: version{major: 2, minor: 0, patch: 0, rc: -1, original: "v2.0.0"},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "major_greater",
|
|
a: version{major: 3, minor: 0, patch: 0, rc: -1, original: "v3.0.0"},
|
|
b: version{major: 2, minor: 0, patch: 0, rc: -1, original: "v2.0.0"},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "minor_less",
|
|
a: version{major: 2, minor: 1, patch: 0, rc: -1, original: "v2.1.0"},
|
|
b: version{major: 2, minor: 2, patch: 0, rc: -1, original: "v2.2.0"},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "minor_greater",
|
|
a: version{major: 2, minor: 5, patch: 0, rc: -1, original: "v2.5.0"},
|
|
b: version{major: 2, minor: 2, patch: 0, rc: -1, original: "v2.2.0"},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "patch_less",
|
|
a: version{major: 2, minor: 1, patch: 0, rc: -1, original: "v2.1.0"},
|
|
b: version{major: 2, minor: 1, patch: 3, rc: -1, original: "v2.1.3"},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "patch_greater",
|
|
a: version{major: 2, minor: 1, patch: 5, rc: -1, original: "v2.1.5"},
|
|
b: version{major: 2, minor: 1, patch: 3, rc: -1, original: "v2.1.3"},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "rc_less_than_non_rc",
|
|
a: version{major: 2, minor: 1, patch: 0, rc: 5, original: "v2.1.0-rc.5"},
|
|
b: version{major: 2, minor: 1, patch: 0, rc: -1, original: "v2.1.0"},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "non_rc_not_less_than_rc",
|
|
a: version{major: 2, minor: 1, patch: 0, rc: -1, original: "v2.1.0"},
|
|
b: version{major: 2, minor: 1, patch: 0, rc: 5, original: "v2.1.0-rc.5"},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "equal_non_rc",
|
|
a: version{major: 2, minor: 1, patch: 0, rc: -1, original: "v2.1.0"},
|
|
b: version{major: 2, minor: 1, patch: 0, rc: -1, original: "v2.1.0"},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "equal_rc",
|
|
a: version{major: 2, minor: 1, patch: 0, rc: 3, original: "v2.1.0-rc.3"},
|
|
b: version{major: 2, minor: 1, patch: 0, rc: 3, original: "v2.1.0-rc.3"},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "rc_ordering",
|
|
a: version{major: 2, minor: 1, patch: 0, rc: 1, original: "v2.1.0-rc.1"},
|
|
b: version{major: 2, minor: 1, patch: 0, rc: 3, original: "v2.1.0-rc.3"},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "rc_ordering_reverse",
|
|
a: version{major: 2, minor: 1, patch: 0, rc: 3, original: "v2.1.0-rc.3"},
|
|
b: version{major: 2, minor: 1, patch: 0, rc: 1, original: "v2.1.0-rc.1"},
|
|
want: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
require.Equal(t, tt.want, versionIsLess(tt.a, tt.b))
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_findLatestRC(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
tags []version
|
|
want version
|
|
}{
|
|
{
|
|
name: "empty_list",
|
|
tags: nil,
|
|
want: version{},
|
|
},
|
|
{
|
|
name: "no_rcs",
|
|
tags: []version{
|
|
{major: 2, minor: 1, patch: 0, rc: -1, original: "v2.1.0"},
|
|
{major: 2, minor: 2, patch: 0, rc: -1, original: "v2.2.0"},
|
|
},
|
|
want: version{},
|
|
},
|
|
{
|
|
name: "multiple_rcs_across_series",
|
|
tags: []version{
|
|
{major: 2, minor: 1, patch: 0, rc: 0, original: "v2.1.0-rc.0"},
|
|
{major: 2, minor: 2, patch: 0, rc: 3, original: "v2.2.0-rc.3"},
|
|
{major: 2, minor: 2, patch: 0, rc: 1, original: "v2.2.0-rc.1"},
|
|
{major: 2, minor: 1, patch: 0, rc: -1, original: "v2.1.0"},
|
|
},
|
|
want: version{major: 2, minor: 2, patch: 0, rc: 3, original: "v2.2.0-rc.3"},
|
|
},
|
|
{
|
|
name: "single_rc",
|
|
tags: []version{
|
|
{major: 1, minor: 0, patch: 0, rc: 0, original: "v1.0.0-rc.0"},
|
|
},
|
|
want: version{major: 1, minor: 0, patch: 0, rc: 0, original: "v1.0.0-rc.0"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
got := findLatestRC(tt.tags)
|
|
require.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_findLatestNonRC(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
tags []version
|
|
want version
|
|
}{
|
|
{
|
|
name: "empty_list",
|
|
tags: nil,
|
|
want: version{},
|
|
},
|
|
{
|
|
name: "no_non_rcs",
|
|
tags: []version{
|
|
{major: 2, minor: 1, patch: 0, rc: 0, original: "v2.1.0-rc.0"},
|
|
{major: 2, minor: 2, patch: 0, rc: 3, original: "v2.2.0-rc.3"},
|
|
},
|
|
want: version{},
|
|
},
|
|
{
|
|
name: "multiple_releases",
|
|
tags: []version{
|
|
{major: 2, minor: 1, patch: 0, rc: -1, original: "v2.1.0"},
|
|
{major: 2, minor: 2, patch: 0, rc: -1, original: "v2.2.0"},
|
|
{major: 2, minor: 2, patch: 0, rc: 3, original: "v2.2.0-rc.3"},
|
|
{major: 2, minor: 1, patch: 1, rc: -1, original: "v2.1.1"},
|
|
},
|
|
want: version{major: 2, minor: 2, patch: 0, rc: -1, original: "v2.2.0"},
|
|
},
|
|
{
|
|
name: "single_release",
|
|
tags: []version{
|
|
{major: 1, minor: 0, patch: 0, rc: -1, original: "v1.0.0"},
|
|
},
|
|
want: version{major: 1, minor: 0, patch: 0, rc: -1, original: "v1.0.0"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
got := findLatestNonRC(tt.tags)
|
|
require.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_findPreviousTag(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tags := []version{
|
|
{major: 2, minor: 1, patch: 0, rc: -1, original: "v2.1.0"},
|
|
{major: 2, minor: 2, patch: 0, rc: 0, original: "v2.2.0-rc.0"},
|
|
{major: 2, minor: 2, patch: 0, rc: 1, original: "v2.2.0-rc.1"},
|
|
{major: 2, minor: 2, patch: 0, rc: -1, original: "v2.2.0"},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
newVer version
|
|
want string
|
|
}{
|
|
{
|
|
name: "normal_case",
|
|
newVer: version{major: 2, minor: 2, patch: 0, rc: 2, original: "v2.2.0-rc.2"},
|
|
want: "v2.2.0-rc.1",
|
|
},
|
|
{
|
|
name: "no_previous",
|
|
newVer: version{major: 1, minor: 0, patch: 0, rc: 0, original: "v1.0.0-rc.0"},
|
|
want: "",
|
|
},
|
|
{
|
|
name: "exact_match_excluded",
|
|
newVer: version{major: 2, minor: 2, patch: 0, rc: -1, original: "v2.2.0"},
|
|
want: "v2.2.0-rc.1",
|
|
},
|
|
{
|
|
name: "picks_highest_lesser",
|
|
newVer: version{major: 3, minor: 0, patch: 0, rc: -1, original: "v3.0.0"},
|
|
want: "v2.2.0",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
got := findPreviousTag(tags, tt.newVer)
|
|
require.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_filterTagsForSeries(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tags := []version{
|
|
{major: 2, minor: 1, patch: 0, rc: -1, original: "v2.1.0"},
|
|
{major: 2, minor: 2, patch: 0, rc: 0, original: "v2.2.0-rc.0"},
|
|
{major: 2, minor: 2, patch: 0, rc: -1, original: "v2.2.0"},
|
|
{major: 3, minor: 2, patch: 0, rc: -1, original: "v3.2.0"},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
major int
|
|
minor int
|
|
wantCount int
|
|
wantFirst string
|
|
wantSecond string
|
|
}{
|
|
{
|
|
name: "matching_tags",
|
|
major: 2,
|
|
minor: 2,
|
|
wantCount: 2,
|
|
wantFirst: "v2.2.0-rc.0",
|
|
wantSecond: "v2.2.0",
|
|
},
|
|
{
|
|
name: "no_matching_tags",
|
|
major: 4,
|
|
minor: 0,
|
|
wantCount: 0,
|
|
},
|
|
{
|
|
name: "single_match",
|
|
major: 2,
|
|
minor: 1,
|
|
wantCount: 1,
|
|
wantFirst: "v2.1.0",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
got := filterTagsForSeries(tags, tt.major, tt.minor)
|
|
require.Len(t, got, tt.wantCount)
|
|
if tt.wantCount > 0 {
|
|
require.Equal(t, tt.wantFirst, got[0].original)
|
|
}
|
|
if tt.wantCount > 1 {
|
|
require.Equal(t, tt.wantSecond, got[1].original)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_isStable(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
major int
|
|
minor int
|
|
tags []version
|
|
want bool
|
|
}{
|
|
{
|
|
name: "latest_is_minor_plus_one_stable",
|
|
major: 2,
|
|
minor: 20,
|
|
tags: []version{
|
|
{major: 2, minor: 21, patch: 0, rc: -1, original: "v2.21.0"},
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "latest_is_same_minor_not_stable",
|
|
major: 2,
|
|
minor: 21,
|
|
tags: []version{
|
|
{major: 2, minor: 21, patch: 0, rc: -1, original: "v2.21.0"},
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "latest_is_minor_plus_two_not_stable",
|
|
major: 2,
|
|
minor: 19,
|
|
tags: []version{
|
|
{major: 2, minor: 21, patch: 0, rc: -1, original: "v2.21.0"},
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "no_tags",
|
|
major: 2,
|
|
minor: 20,
|
|
tags: nil,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "only_rcs_no_releases",
|
|
major: 2,
|
|
minor: 20,
|
|
tags: []version{
|
|
{major: 2, minor: 21, patch: 0, rc: 0, original: "v2.21.0-rc.0"},
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "different_major_not_stable",
|
|
major: 2,
|
|
minor: 20,
|
|
tags: []version{
|
|
{major: 3, minor: 21, patch: 0, rc: -1, original: "v3.21.0"},
|
|
},
|
|
want: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
require.Equal(t, tt.want, isStable(tt.major, tt.minor, tt.tags))
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_isHexSHA(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
s string
|
|
want bool
|
|
}{
|
|
{
|
|
name: "valid_short_sha",
|
|
s: "abc1234",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "valid_long_sha",
|
|
s: "abc1234def5678901234567890abcdef12345678",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "valid_uppercase",
|
|
s: "ABCDEF1234567",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "too_short",
|
|
s: "abc12",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "exactly_six_chars",
|
|
s: "abc123",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "non_hex_chars",
|
|
s: "xyz1234",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "empty",
|
|
s: "",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "seven_chars_valid",
|
|
s: "abcdef1",
|
|
want: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
require.Equal(t, tt.want, isHexSHA(tt.s))
|
|
})
|
|
}
|
|
}
|