Skip to content
Open
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
18 changes: 18 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,24 @@ permissions:
actions: write

jobs:
validate-server-version:
name: Validate Server Version
if: github.event_name == 'push' || github.base_ref == 'main'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: Validate server dependency version
run: go run ./internal/cmd/validate-server-version
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

build-test:
strategy:
fail-fast: false
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ require (
go.temporal.io/api v1.60.1
go.temporal.io/sdk v1.38.0
go.temporal.io/sdk/contrib/envconfig v0.1.0
golang.org/x/mod v0.31.0
go.temporal.io/server v1.30.0
golang.org/x/term v0.38.0
golang.org/x/tools v0.40.0
Expand Down Expand Up @@ -156,7 +157,6 @@ require (
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/oauth2 v0.33.0 // indirect
golang.org/x/sync v0.19.0 // indirect
Expand Down
133 changes: 133 additions & 0 deletions internal/cmd/validate-server-version/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package main

import (
"encoding/json"
"fmt"
"net/http"
"os"

"golang.org/x/mod/modfile"
"golang.org/x/mod/module"
"golang.org/x/mod/semver"
)

const (
serverModule = "go.temporal.io/server"
releaseURL = "https://api.github.com/repos/temporalio/temporal/releases/latest"
)

func main() {
if err := run(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}

func run() error {
serverVersion, err := getServerVersion()
if err != nil {
return err
}
fmt.Printf("Found server dependency: %s@%s\n", serverModule, serverVersion)

if module.IsPseudoVersion(serverVersion) {
return fmt.Errorf("server dependency must be a tagged version, not a pseudo-version: %s", serverVersion)
}
fmt.Println("✓ Version is a valid tagged version")

latestRelease, err := fetchLatestRelease()
if err != nil {
return err
}
fmt.Printf("Latest GitHub release: %s\n", latestRelease)

if err := validateVersionConstraint(serverVersion, latestRelease); err != nil {
return err
}

fmt.Println("✓ Server dependency version validation passed!")
return nil
}

func getServerVersion() (string, error) {
data, err := os.ReadFile("go.mod")
if err != nil {
return "", fmt.Errorf("failed to read go.mod: %w", err)
}

f, err := modfile.Parse("go.mod", data, nil)
if err != nil {
return "", fmt.Errorf("failed to parse go.mod: %w", err)
}

for _, req := range f.Require {
if req.Mod.Path == serverModule {
return req.Mod.Version, nil
}
}

return "", fmt.Errorf("server dependency %s not found in go.mod", serverModule)
}

func fetchLatestRelease() (string, error) {
req, err := http.NewRequest("GET", releaseURL, nil)
if err != nil {
return "", err
}

req.Header.Set("Accept", "application/vnd.github.v3+json")
if token := os.Getenv("GITHUB_TOKEN"); token != "" {
req.Header.Set("Authorization", "token "+token)
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("GitHub API returned %s", resp.Status)
}

var release struct {
TagName string `json:"tag_name"`
}
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
return "", err
}

return release.TagName, nil
}

// validateVersionConstraint ensures serverVersion is at most one minor version ahead of latestRelease.
func validateVersionConstraint(serverVersion, latestRelease string) error {
if !semver.IsValid(serverVersion) {
return fmt.Errorf("invalid server version: %s", serverVersion)
}
if !semver.IsValid(latestRelease) {
return fmt.Errorf("invalid latest release version: %s", latestRelease)
}

serverMM := semver.MajorMinor(serverVersion)
latestMM := semver.MajorMinor(latestRelease)

var latestMajor, latestMinor int
fmt.Sscanf(latestMM, "v%d.%d", &latestMajor, &latestMinor)
maxAllowedMM := fmt.Sprintf("v%d.%d", latestMajor, latestMinor+1)

fmt.Printf(" Server version: %s.x\n", serverMM)
fmt.Printf(" Latest release: %s.x\n", latestMM)
fmt.Printf(" Max allowed: %s.x\n", maxAllowedMM)

if semver.Compare(serverMM, maxAllowedMM) > 0 {
return fmt.Errorf(
"server dependency version %s exceeds allowed range\n"+
" Max allowed: %s.x (latest release + 1 minor)\n"+
" Latest release: %s",
serverVersion, maxAllowedMM, latestRelease,
)
}

return nil
}
61 changes: 61 additions & 0 deletions internal/cmd/validate-server-version/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package main

import (
"testing"

"golang.org/x/mod/module"
)

func TestIsPseudoVersion(t *testing.T) {
t.Parallel()

tests := []struct {
version string
want bool
}{
{"v1.30.0-148.4", false},
{"v1.29.2", false},
{"v1.30.0-rc.1", false},
{"v0.0.0-20240101120000-abcdef123456", true},
{"v1.29.1-0.20240101120000-abcdef123456", true},
}

for _, tt := range tests {
t.Run(tt.version, func(t *testing.T) {
t.Parallel()
if got := module.IsPseudoVersion(tt.version); got != tt.want {
t.Errorf("IsPseudoVersion(%q) = %v, want %v", tt.version, got, tt.want)
}
})
}
}

func TestValidateVersionConstraint(t *testing.T) {
t.Parallel()

tests := []struct {
name string
serverVersion string
latestRelease string
wantErr bool
}{
{"same minor", "v1.29.0-142.0", "v1.29.2", false},
{"one minor ahead", "v1.30.0-148.4", "v1.29.2", false},
{"two minors ahead", "v1.31.0-150.0", "v1.29.2", true},
{"major ahead", "v2.0.0-1.0", "v1.29.2", true},
{"exact match", "v1.29.2", "v1.29.2", false},
{"invalid server", "invalid", "v1.29.2", true},
{"invalid release", "v1.30.0", "invalid", true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := validateVersionConstraint(tt.serverVersion, tt.latestRelease)
if (err != nil) != tt.wantErr {
t.Errorf("validateVersionConstraint(%q, %q) error = %v, wantErr %v",
tt.serverVersion, tt.latestRelease, err, tt.wantErr)
}
})
}
}