Skip to content

Commit d67db69

Browse files
committed
fix: bundled clawmachine version is incorrect
1 parent c4813ad commit d67db69

File tree

12 files changed

+404
-62
lines changed

12 files changed

+404
-62
lines changed

.config/scripts/check-image-contract.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ if [[ -z "${release_tag}" ]]; then
4141
exit 1
4242
fi
4343

44+
expected_release_tag="${EXPECTED_RELEASE_TAG:-}"
45+
if [[ -n "${expected_release_tag}" && "${release_tag}" != "${expected_release_tag}" ]]; then
46+
echo "${clawmachine_chart} appVersion=${release_tag}, want ${expected_release_tag} (EXPECTED_RELEASE_TAG)"
47+
exit 1
48+
fi
49+
4450
for repo in "${expected_repos[@]}"; do
4551
if ! grep -Fq -- "- ${repo}" "$goreleaser_file"; then
4652
echo "❌ Missing Goreleaser image output for ${repo}"

.github/workflows/release.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,16 @@ jobs:
3434
${{ runner.os }}-go-release-
3535
${{ runner.os }}-go-
3636
37+
- name: Compute release version
38+
id: version
39+
run: |
40+
version="${GITHUB_REF_NAME#v}"
41+
echo "value=${version}" >> "$GITHUB_OUTPUT"
42+
3743
- name: Validate release image contract
3844
run: mise run release:image-contract
45+
env:
46+
EXPECTED_RELEASE_TAG: ${{ steps.version.outputs.value }}
3947

4048
- name: Run GoReleaser (artifacts only)
4149
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6

control-plane/cmd/clawmachine/install.go

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/spf13/cobra"
1010
"helm.sh/helm/v4/pkg/action"
11+
"helm.sh/helm/v4/pkg/chart"
1112
"helm.sh/helm/v4/pkg/chart/loader"
1213
"helm.sh/helm/v4/pkg/kube"
1314

@@ -26,6 +27,23 @@ type installPlan struct {
2627

2728
const defaultKindClusterName = "claw-machine"
2829

30+
var (
31+
loadControlPlaneChartForInstall = func() (chart.Charter, error) {
32+
return loader.LoadArchive(bytes.NewReader(service.GetControlPlaneChart()))
33+
}
34+
initActionConfigForInstall = initActionConfig
35+
runHelmInstallForInstall = func(actionConfig *action.Configuration, releaseName, namespace string, chrt chart.Charter, vals map[string]any) error {
36+
client := action.NewInstall(actionConfig)
37+
client.ReleaseName = releaseName
38+
client.Namespace = namespace
39+
client.CreateNamespace = true
40+
client.WaitStrategy = kube.StatusWatcherStrategy
41+
client.Timeout = 5 * time.Minute
42+
_, err := client.Run(chrt, vals)
43+
return err
44+
}
45+
)
46+
2947
func newInstallCmd() *cobra.Command {
3048
cmd := &cobra.Command{
3149
Use: "install",
@@ -97,26 +115,27 @@ func runInstall(cmd *cobra.Command) error {
97115
}
98116
}
99117

100-
chartBytes := service.GetControlPlaneChart()
101-
chrt, err := loader.LoadArchive(bytes.NewReader(chartBytes))
118+
chrt, err := loadControlPlaneChartForInstall()
102119
if err != nil {
103120
return fmt.Errorf("loading embedded chart: %w", err)
104121
}
105122

106-
actionConfig, err := initActionConfig(settings, namespace)
123+
repo, tag, fallback, err := resolveBundledImage(chrt, version)
107124
if err != nil {
108125
return err
109126
}
110127

111-
client := action.NewInstall(actionConfig)
112-
client.ReleaseName = plan.ReleaseName
113-
client.Namespace = plan.Namespace
114-
client.CreateNamespace = true
115-
client.WaitStrategy = kube.StatusWatcherStrategy
116-
client.Timeout = 5 * time.Minute
128+
if fallback {
129+
styledPrintf(dimStyle, "CLI version %q resolved to dev mode; using chart appVersion %q.", version, tag)
130+
}
131+
132+
actionConfig, err := initActionConfigForInstall(settings, namespace)
133+
if err != nil {
134+
return err
135+
}
117136

118-
styledPrintf(accentStyle, "Installing ClawMachine into namespace %q...", plan.Namespace)
119-
if _, err = client.Run(chrt, nil); err != nil {
137+
styledPrintf(accentStyle, "Installing ClawMachine into namespace %q using %s:%s...", plan.Namespace, repo, tag)
138+
if err := runHelmInstallForInstall(actionConfig, plan.ReleaseName, plan.Namespace, chrt, upgradeOverrides(repo, tag)); err != nil {
120139
return fmt.Errorf("installing release: %w", err)
121140
}
122141

control-plane/cmd/clawmachine/install_test.go

Lines changed: 154 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
package main
22

3-
import "testing"
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"helm.sh/helm/v4/pkg/action"
8+
"helm.sh/helm/v4/pkg/chart"
9+
chartv2 "helm.sh/helm/v4/pkg/chart/v2"
10+
"helm.sh/helm/v4/pkg/cli"
11+
)
412

513
func TestNewInstallCmd_DoesNotExposeOnePasswordConnectFlag(t *testing.T) {
614
cmd := newInstallCmd()
@@ -27,3 +35,148 @@ func TestNewInstallCmd_HasExpectedFlags(t *testing.T) {
2735
}
2836
}
2937
}
38+
39+
func TestRunInstall_UsesRuntimeImageOverrides(t *testing.T) {
40+
restore := stubInstallDeps()
41+
defer restore()
42+
43+
origVersion := version
44+
version = "0.1.6"
45+
defer func() { version = origVersion }()
46+
47+
var (
48+
gotValues map[string]any
49+
gotRelease string
50+
gotNamespace string
51+
)
52+
runHelmInstallForInstall = func(_ *action.Configuration, releaseName, namespace string, chrt chart.Charter, vals map[string]any) error {
53+
gotValues = vals
54+
gotRelease = releaseName
55+
gotNamespace = namespace
56+
if chrt == nil {
57+
t.Fatal("chart should not be nil")
58+
}
59+
return nil
60+
}
61+
62+
cmd := newInstallCmd()
63+
if err := cmd.Flags().Set("interactive", "false"); err != nil {
64+
t.Fatalf("set interactive flag: %v", err)
65+
}
66+
67+
if err := runInstall(cmd); err != nil {
68+
t.Fatalf("runInstall() error = %v", err)
69+
}
70+
71+
if gotRelease != "clawmachine" {
72+
t.Fatalf("releaseName = %q, want %q", gotRelease, "clawmachine")
73+
}
74+
if gotNamespace != "claw-machine" {
75+
t.Fatalf("namespace = %q, want %q", gotNamespace, "claw-machine")
76+
}
77+
image, ok := gotValues["image"].(map[string]any)
78+
if !ok {
79+
t.Fatalf("image values missing: %#v", gotValues)
80+
}
81+
if image["repository"] != "ghcr.io/zackerydev/theclawmachine" {
82+
t.Fatalf("image.repository = %v, want %q", image["repository"], "ghcr.io/zackerydev/theclawmachine")
83+
}
84+
if image["tag"] != "0.1.6" {
85+
t.Fatalf("image.tag = %v, want %q", image["tag"], "0.1.6")
86+
}
87+
}
88+
89+
func TestRunInstall_UsesFallbackTagForDev(t *testing.T) {
90+
restore := stubInstallDeps()
91+
defer restore()
92+
93+
origVersion := version
94+
version = "dev"
95+
defer func() { version = origVersion }()
96+
97+
var gotValues map[string]any
98+
runHelmInstallForInstall = func(_ *action.Configuration, _, _ string, _ chart.Charter, vals map[string]any) error {
99+
gotValues = vals
100+
return nil
101+
}
102+
103+
cmd := newInstallCmd()
104+
if err := cmd.Flags().Set("interactive", "false"); err != nil {
105+
t.Fatalf("set interactive flag: %v", err)
106+
}
107+
108+
if err := runInstall(cmd); err != nil {
109+
t.Fatalf("runInstall() error = %v", err)
110+
}
111+
112+
image, ok := gotValues["image"].(map[string]any)
113+
if !ok {
114+
t.Fatalf("image values missing: %#v", gotValues)
115+
}
116+
if image["tag"] != "0.1.0" {
117+
t.Fatalf("image.tag = %v, want %q", image["tag"], "0.1.0")
118+
}
119+
}
120+
121+
func TestRunInstall_InvalidRuntimeVersionFails(t *testing.T) {
122+
restore := stubInstallDeps()
123+
defer restore()
124+
125+
origVersion := version
126+
version = "dirty-build"
127+
defer func() { version = origVersion }()
128+
129+
called := false
130+
runHelmInstallForInstall = func(_ *action.Configuration, _, _ string, _ chart.Charter, _ map[string]any) error {
131+
called = true
132+
return nil
133+
}
134+
135+
cmd := newInstallCmd()
136+
if err := cmd.Flags().Set("interactive", "false"); err != nil {
137+
t.Fatalf("set interactive flag: %v", err)
138+
}
139+
140+
err := runInstall(cmd)
141+
if err == nil {
142+
t.Fatal("expected error, got nil")
143+
}
144+
if !strings.Contains(err.Error(), "runtime version") {
145+
t.Fatalf("expected invalid runtime version error, got: %v", err)
146+
}
147+
if called {
148+
t.Fatal("install should not execute helm install when runtime version is invalid")
149+
}
150+
}
151+
152+
func stubInstallDeps() func() {
153+
origLoad := loadControlPlaneChartForInstall
154+
origInit := initActionConfigForInstall
155+
origRun := runHelmInstallForInstall
156+
157+
loadControlPlaneChartForInstall = func() (chart.Charter, error) {
158+
return &chartv2.Chart{
159+
Metadata: &chartv2.Metadata{
160+
Name: "clawmachine",
161+
AppVersion: "0.1.0",
162+
},
163+
Values: map[string]any{
164+
"image": map[string]any{
165+
"repository": "ghcr.io/zackerydev/theclawmachine",
166+
},
167+
},
168+
}, nil
169+
}
170+
initActionConfigForInstall = func(_ *cli.EnvSettings, _ string) (*action.Configuration, error) {
171+
return &action.Configuration{}, nil
172+
}
173+
runHelmInstallForInstall = func(_ *action.Configuration, _, _ string, _ chart.Charter, _ map[string]any) error {
174+
return nil
175+
}
176+
177+
return func() {
178+
loadControlPlaneChartForInstall = origLoad
179+
initActionConfigForInstall = origInit
180+
runHelmInstallForInstall = origRun
181+
}
182+
}

control-plane/cmd/clawmachine/upgrade.go

Lines changed: 7 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import (
66
"strings"
77
"time"
88

9-
"github.com/Masterminds/semver/v3"
109
"github.com/charmbracelet/huh"
1110
"github.com/spf13/cobra"
1211
"github.com/zackerydev/clawmachine/control-plane/internal/service"
12+
versionutil "github.com/zackerydev/clawmachine/control-plane/internal/version"
1313
"helm.sh/helm/v4/pkg/action"
1414
"helm.sh/helm/v4/pkg/chart"
1515
"helm.sh/helm/v4/pkg/chart/loader"
@@ -104,7 +104,7 @@ func runUpgrade(cmd *cobra.Command) error {
104104
}
105105

106106
if fallback {
107-
styledPrintf(dimStyle, "CLI version %q is not a release tag; using chart appVersion %q.", version, tag)
107+
styledPrintf(dimStyle, "CLI version %q resolved to dev mode; using chart appVersion %q.", version, tag)
108108
}
109109

110110
styledPrintf(accentStyle, "Upgrading ClawMachine %q in namespace %q to %s:%s...", releaseName, namespace, repo, tag)
@@ -160,9 +160,9 @@ func resolveBundledImage(chrt chart.Charter, cliVersion string) (repo, tag strin
160160
appVersion = strings.TrimSpace(fmt.Sprintf("%v", metadata["AppVersion"]))
161161
}
162162
}
163-
tag, usedFallback = normalizeImageTag(cliVersion, appVersion)
164-
if tag == "" {
165-
return "", "", false, fmt.Errorf("unable to resolve image tag from CLI version %q or chart appVersion", cliVersion)
163+
tag, usedFallback, err = normalizeImageTag(cliVersion, appVersion)
164+
if err != nil {
165+
return "", "", false, fmt.Errorf("resolving image tag: %w", err)
166166
}
167167

168168
return repo, tag, usedFallback, nil
@@ -195,28 +195,8 @@ func chartImageRepository(values map[string]any) (string, error) {
195195
return repo, nil
196196
}
197197

198-
func normalizeImageTag(cliVersion, chartAppVersion string) (tag string, usedFallback bool) {
199-
candidate := strings.TrimSpace(cliVersion)
200-
if isReleaseSemver(candidate) {
201-
return strings.TrimPrefix(candidate, "v"), false
202-
}
203-
204-
fallback := strings.TrimSpace(chartAppVersion)
205-
if fallback == "" {
206-
return "", true
207-
}
208-
return fallback, true
209-
}
210-
211-
func isReleaseSemver(v string) bool {
212-
if strings.TrimSpace(v) == "" {
213-
return false
214-
}
215-
parsed, err := semver.NewVersion(strings.TrimPrefix(v, "v"))
216-
if err != nil {
217-
return false
218-
}
219-
return parsed.Prerelease() == ""
198+
func normalizeImageTag(cliVersion, chartAppVersion string) (tag string, usedFallback bool, err error) {
199+
return versionutil.ResolveRuntimeOrFallbackImageTag(cliVersion, chartAppVersion)
220200
}
221201

222202
func upgradeOverrides(repo, tag string) map[string]any {

control-plane/cmd/clawmachine/upgrade_test.go

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,22 @@ func TestNormalizeImageTag(t *testing.T) {
3131
appVersion string
3232
wantTag string
3333
wantFallback bool
34+
wantErr bool
3435
}{
3536
{name: "release version", cliVersion: "0.2.1", appVersion: "0.1.0", wantTag: "0.2.1", wantFallback: false},
3637
{name: "release version with v prefix", cliVersion: "v0.2.1", appVersion: "0.1.0", wantTag: "0.2.1", wantFallback: false},
38+
{name: "prerelease version", cliVersion: "v0.2.1-rc.1", appVersion: "0.1.0", wantTag: "0.2.1-rc.1", wantFallback: false},
3739
{name: "dev fallback", cliVersion: "dev", appVersion: "0.1.0", wantTag: "0.1.0", wantFallback: true},
3840
{name: "empty fallback", cliVersion: "", appVersion: "0.1.0", wantTag: "0.1.0", wantFallback: true},
39-
{name: "prerelease fallback", cliVersion: "0.2.1-rc.1", appVersion: "0.1.0", wantTag: "0.1.0", wantFallback: true},
41+
{name: "invalid runtime version", cliVersion: "dirty-build", appVersion: "0.1.0", wantErr: true},
4042
}
4143

4244
for _, tt := range tests {
4345
t.Run(tt.name, func(t *testing.T) {
44-
gotTag, gotFallback := normalizeImageTag(tt.cliVersion, tt.appVersion)
46+
gotTag, gotFallback, err := normalizeImageTag(tt.cliVersion, tt.appVersion)
47+
if (err != nil) != tt.wantErr {
48+
t.Fatalf("normalizeImageTag() error = %v, wantErr %t", err, tt.wantErr)
49+
}
4550
if gotTag != tt.wantTag {
4651
t.Fatalf("normalizeImageTag() tag = %q, want %q", gotTag, tt.wantTag)
4752
}
@@ -183,6 +188,32 @@ func TestRunUpgrade_UpgradeError(t *testing.T) {
183188
}
184189
}
185190

191+
func TestRunUpgrade_InvalidRuntimeVersion(t *testing.T) {
192+
restore := stubUpgradeDeps()
193+
defer restore()
194+
195+
origVersion := version
196+
version = "dirty-build"
197+
defer func() { version = origVersion }()
198+
199+
listReleasesForUpgrade = func(_ *action.Configuration, _ string) ([]release.Releaser, error) {
200+
return []release.Releaser{&releasev1.Release{Name: "clawmachine"}}, nil
201+
}
202+
203+
cmd := newUpgradeCmd()
204+
if err := cmd.Flags().Set("yes", "true"); err != nil {
205+
t.Fatalf("set flag yes: %v", err)
206+
}
207+
208+
err := runUpgrade(cmd)
209+
if err == nil {
210+
t.Fatal("expected error, got nil")
211+
}
212+
if !strings.Contains(err.Error(), "runtime version") {
213+
t.Fatalf("expected invalid runtime version error, got: %v", err)
214+
}
215+
}
216+
186217
func stubUpgradeDeps() func() {
187218
origLoad := loadControlPlaneChartForUpgrade
188219
origInit := initActionConfigForUpgrade

0 commit comments

Comments
 (0)