Skip to content

Commit bf667ba

Browse files
authored
add CI validation for server dependency version (#933)
## Summary - Adds a new CI job that validates the `go.temporal.io/server` dependency - Ensures the dependency is a tagged version (not a pseudo-version with commit hash) - Ensures the version doesn't exceed the next server release (major.minor+1 from latest GitHub release) ## Test plan - [x] CI job runs successfully on this PR - [x] Verify validation passes for current `v1.30.0-148.4` (latest release is `v1.29.2`)
1 parent 3f45bc8 commit bf667ba

4 files changed

Lines changed: 213 additions & 1 deletion

File tree

.github/workflows/ci.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,24 @@ permissions:
1010
actions: write
1111

1212
jobs:
13+
validate-server-version:
14+
name: Validate Server Version
15+
if: github.event_name == 'push' || github.base_ref == 'main'
16+
runs-on: ubuntu-latest
17+
steps:
18+
- name: Checkout repository
19+
uses: actions/checkout@v4
20+
21+
- name: Setup Go
22+
uses: actions/setup-go@v5
23+
with:
24+
go-version-file: go.mod
25+
26+
- name: Validate server dependency version
27+
run: go run ./internal/cmd/validate-server-version
28+
env:
29+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30+
1331
build-test:
1432
strategy:
1533
fail-fast: false

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ require (
2020
go.temporal.io/api v1.60.1
2121
go.temporal.io/sdk v1.38.0
2222
go.temporal.io/sdk/contrib/envconfig v0.1.0
23+
golang.org/x/mod v0.31.0
2324
go.temporal.io/server v1.30.0
2425
golang.org/x/term v0.38.0
2526
golang.org/x/tools v0.40.0
@@ -156,7 +157,6 @@ require (
156157
go.uber.org/zap v1.27.0 // indirect
157158
golang.org/x/crypto v0.46.0 // indirect
158159
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
159-
golang.org/x/mod v0.31.0 // indirect
160160
golang.org/x/net v0.48.0 // indirect
161161
golang.org/x/oauth2 v0.33.0 // indirect
162162
golang.org/x/sync v0.19.0 // indirect
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
"os"
8+
9+
"golang.org/x/mod/modfile"
10+
"golang.org/x/mod/module"
11+
"golang.org/x/mod/semver"
12+
)
13+
14+
const (
15+
serverModule = "go.temporal.io/server"
16+
releaseURL = "https://api.github.com/repos/temporalio/temporal/releases/latest"
17+
)
18+
19+
func main() {
20+
if err := run(); err != nil {
21+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
22+
os.Exit(1)
23+
}
24+
}
25+
26+
func run() error {
27+
serverVersion, err := getServerVersion()
28+
if err != nil {
29+
return err
30+
}
31+
fmt.Printf("Found server dependency: %s@%s\n", serverModule, serverVersion)
32+
33+
if module.IsPseudoVersion(serverVersion) {
34+
return fmt.Errorf("server dependency must be a tagged version, not a pseudo-version: %s", serverVersion)
35+
}
36+
fmt.Println("✓ Version is a valid tagged version")
37+
38+
latestRelease, err := fetchLatestRelease()
39+
if err != nil {
40+
return err
41+
}
42+
fmt.Printf("Latest GitHub release: %s\n", latestRelease)
43+
44+
if err := validateVersionConstraint(serverVersion, latestRelease); err != nil {
45+
return err
46+
}
47+
48+
fmt.Println("✓ Server dependency version validation passed!")
49+
return nil
50+
}
51+
52+
func getServerVersion() (string, error) {
53+
data, err := os.ReadFile("go.mod")
54+
if err != nil {
55+
return "", fmt.Errorf("failed to read go.mod: %w", err)
56+
}
57+
58+
f, err := modfile.Parse("go.mod", data, nil)
59+
if err != nil {
60+
return "", fmt.Errorf("failed to parse go.mod: %w", err)
61+
}
62+
63+
for _, req := range f.Require {
64+
if req.Mod.Path == serverModule {
65+
return req.Mod.Version, nil
66+
}
67+
}
68+
69+
return "", fmt.Errorf("server dependency %s not found in go.mod", serverModule)
70+
}
71+
72+
func fetchLatestRelease() (string, error) {
73+
req, err := http.NewRequest("GET", releaseURL, nil)
74+
if err != nil {
75+
return "", err
76+
}
77+
78+
req.Header.Set("Accept", "application/vnd.github.v3+json")
79+
if token := os.Getenv("GITHUB_TOKEN"); token != "" {
80+
req.Header.Set("Authorization", "token "+token)
81+
}
82+
83+
resp, err := http.DefaultClient.Do(req)
84+
if err != nil {
85+
return "", err
86+
}
87+
defer resp.Body.Close()
88+
89+
if resp.StatusCode != http.StatusOK {
90+
return "", fmt.Errorf("GitHub API returned %s", resp.Status)
91+
}
92+
93+
var release struct {
94+
TagName string `json:"tag_name"`
95+
}
96+
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
97+
return "", err
98+
}
99+
100+
return release.TagName, nil
101+
}
102+
103+
// validateVersionConstraint ensures serverVersion is at most one minor version ahead of latestRelease.
104+
func validateVersionConstraint(serverVersion, latestRelease string) error {
105+
if !semver.IsValid(serverVersion) {
106+
return fmt.Errorf("invalid server version: %s", serverVersion)
107+
}
108+
if !semver.IsValid(latestRelease) {
109+
return fmt.Errorf("invalid latest release version: %s", latestRelease)
110+
}
111+
112+
serverMM := semver.MajorMinor(serverVersion)
113+
latestMM := semver.MajorMinor(latestRelease)
114+
115+
var latestMajor, latestMinor int
116+
fmt.Sscanf(latestMM, "v%d.%d", &latestMajor, &latestMinor)
117+
maxAllowedMM := fmt.Sprintf("v%d.%d", latestMajor, latestMinor+1)
118+
119+
fmt.Printf(" Server version: %s.x\n", serverMM)
120+
fmt.Printf(" Latest release: %s.x\n", latestMM)
121+
fmt.Printf(" Max allowed: %s.x\n", maxAllowedMM)
122+
123+
if semver.Compare(serverMM, maxAllowedMM) > 0 {
124+
return fmt.Errorf(
125+
"server dependency version %s exceeds allowed range\n"+
126+
" Max allowed: %s.x (latest release + 1 minor)\n"+
127+
" Latest release: %s",
128+
serverVersion, maxAllowedMM, latestRelease,
129+
)
130+
}
131+
132+
return nil
133+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package main
2+
3+
import (
4+
"testing"
5+
6+
"golang.org/x/mod/module"
7+
)
8+
9+
func TestIsPseudoVersion(t *testing.T) {
10+
t.Parallel()
11+
12+
tests := []struct {
13+
version string
14+
want bool
15+
}{
16+
{"v1.30.0-148.4", false},
17+
{"v1.29.2", false},
18+
{"v1.30.0-rc.1", false},
19+
{"v0.0.0-20240101120000-abcdef123456", true},
20+
{"v1.29.1-0.20240101120000-abcdef123456", true},
21+
}
22+
23+
for _, tt := range tests {
24+
t.Run(tt.version, func(t *testing.T) {
25+
t.Parallel()
26+
if got := module.IsPseudoVersion(tt.version); got != tt.want {
27+
t.Errorf("IsPseudoVersion(%q) = %v, want %v", tt.version, got, tt.want)
28+
}
29+
})
30+
}
31+
}
32+
33+
func TestValidateVersionConstraint(t *testing.T) {
34+
t.Parallel()
35+
36+
tests := []struct {
37+
name string
38+
serverVersion string
39+
latestRelease string
40+
wantErr bool
41+
}{
42+
{"same minor", "v1.29.0-142.0", "v1.29.2", false},
43+
{"one minor ahead", "v1.30.0-148.4", "v1.29.2", false},
44+
{"two minors ahead", "v1.31.0-150.0", "v1.29.2", true},
45+
{"major ahead", "v2.0.0-1.0", "v1.29.2", true},
46+
{"exact match", "v1.29.2", "v1.29.2", false},
47+
{"invalid server", "invalid", "v1.29.2", true},
48+
{"invalid release", "v1.30.0", "invalid", true},
49+
}
50+
51+
for _, tt := range tests {
52+
t.Run(tt.name, func(t *testing.T) {
53+
t.Parallel()
54+
err := validateVersionConstraint(tt.serverVersion, tt.latestRelease)
55+
if (err != nil) != tt.wantErr {
56+
t.Errorf("validateVersionConstraint(%q, %q) error = %v, wantErr %v",
57+
tt.serverVersion, tt.latestRelease, err, tt.wantErr)
58+
}
59+
})
60+
}
61+
}

0 commit comments

Comments
 (0)