Skip to content

Commit 6185c48

Browse files
dan-mangesclaude
andauthored
Update base.config package reference on update/resolve (#483)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 177c0af commit 6185c48

File tree

5 files changed

+372
-35
lines changed

5 files changed

+372
-35
lines changed

.rwx/integration/resolve-update-test.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,39 @@ tasks:
103103
env:
104104
RWX_ACCESS_TOKEN: ${{ secrets.RWX_ACCESS_TOKEN }}
105105

106+
- key: test-update-packages-bumps-base-config
107+
use: rwx-cli
108+
run: |
109+
set -euo pipefail
110+
111+
tmpdir=$(mktemp -d)
112+
mkdir -p "$tmpdir/.rwx"
113+
cat > "$tmpdir/.rwx/ci.yml" << 'EOF'
114+
base:
115+
image: ubuntu:24.04
116+
config: rwx/base 1.0.0
117+
tasks:
118+
- key: noop
119+
run: echo hello
120+
EOF
121+
122+
rwx update packages -d "$tmpdir"
123+
if grep -q "config: rwx/base 1.0.0" "$tmpdir/.rwx/ci.yml"; then
124+
echo "update packages did not bump base.config from 1.0.0"
125+
cat "$tmpdir/.rwx/ci.yml"
126+
rm -rf "$tmpdir"
127+
exit 1
128+
fi
129+
if ! grep -qE "config: rwx/base [0-9]+\.[0-9]+\.[0-9]+" "$tmpdir/.rwx/ci.yml"; then
130+
echo "update packages did not leave base.config with a pinned version"
131+
cat "$tmpdir/.rwx/ci.yml"
132+
rm -rf "$tmpdir"
133+
exit 1
134+
fi
135+
rm -rf "$tmpdir"
136+
env:
137+
RWX_ACCESS_TOKEN: ${{ secrets.RWX_ACCESS_TOKEN }}
138+
106139
- key: test-update-base-updates-base-image
107140
use: rwx-cli
108141
run: |

cmd/rwx/update.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ var (
3838
Long: "Update base images in RWX run configurations.\n" +
3939
"Adds a base image to run configurations that don't have one, and migrates\n" +
4040
"deprecated 'os' and 'tag' fields to the new 'image' and 'config' format.",
41-
Use: "base [flags] [files...]",
41+
Use: "base [flags] [files...]",
42+
Hidden: true,
4243
}
4344

4445
updatePackagesCmd = &cobra.Command{

internal/cli/service_packages.go

Lines changed: 66 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,56 @@ func (s Service) parsePackageVersion(str string) PackageVersion {
194194
}
195195
}
196196

197+
func (s Service) updatePackageReferenceAtPath(
198+
doc *YAMLDoc,
199+
yamlPath string,
200+
original string,
201+
update bool,
202+
packageVersions *api.PackageVersionsResult,
203+
versionPicker func(versions api.PackageVersionsResult, rwxPackage string, major string) (string, error),
204+
replacements map[string]string,
205+
) (bool, error) {
206+
// Expressions can't be statically resolved, so skip them
207+
if strings.Contains(original, "${{") {
208+
return false, nil
209+
}
210+
211+
packageVersion := s.parsePackageVersion(original)
212+
if packageVersion.Name == "" {
213+
return false, nil
214+
} else if !update && packageVersion.MajorVersion != "" {
215+
return false, nil
216+
}
217+
218+
newName := packageVersions.Renames[packageVersion.Name]
219+
if newName == "" {
220+
newName = packageVersion.Name
221+
}
222+
223+
targetPackageVersion, err := versionPicker(*packageVersions, newName, packageVersion.MajorVersion)
224+
if err != nil {
225+
fmt.Fprintln(s.Stderr, err.Error())
226+
return false, nil
227+
}
228+
229+
newPackage := fmt.Sprintf("%s %s", newName, targetPackageVersion)
230+
if newPackage == original {
231+
return false, nil
232+
}
233+
234+
if err := doc.ReplaceAtPath(yamlPath, newPackage); err != nil {
235+
return false, err
236+
}
237+
238+
if newName != packageVersion.Name {
239+
replacements[packageVersion.Original] = fmt.Sprintf("%s %s", newName, targetPackageVersion)
240+
} else {
241+
replacements[packageVersion.Original] = targetPackageVersion
242+
}
243+
244+
return true, nil
245+
}
246+
197247
func (s Service) resolveOrUpdatePackagesForFiles(mintFiles []*MintYAMLFile, update bool, versionPicker func(versions api.PackageVersionsResult, rwxPackage string, major string) (string, error)) (map[string]string, error) {
198248
packageVersions, err := s.APIClient.GetPackageVersions()
199249
if err != nil {
@@ -216,50 +266,32 @@ func (s Service) resolveOrUpdatePackagesForFiles(mintFiles []*MintYAMLFile, upda
216266
}
217267

218268
err = file.Doc.ForEachNode(nodePath, func(node ast.Node) error {
219-
// Expressions can't be statically resolved, so skip them
220-
if strings.Contains(node.String(), "${{") {
221-
return nil
222-
}
223-
224-
packageVersion := s.parsePackageVersion(node.String())
225-
if packageVersion.Name == "" {
226-
return nil
227-
} else if !update && packageVersion.MajorVersion != "" {
228-
return nil
229-
}
230-
231-
newName := packageVersions.Renames[packageVersion.Name]
232-
if newName == "" {
233-
newName = packageVersion.Name
234-
}
235-
236-
targetPackageVersion, err := versionPicker(*packageVersions, newName, packageVersion.MajorVersion)
269+
changed, err := s.updatePackageReferenceAtPath(file.Doc, node.GetPath(), node.String(), update, packageVersions, versionPicker, replacements)
237270
if err != nil {
238-
fmt.Fprintln(s.Stderr, err.Error())
239-
return nil
240-
}
241-
242-
newPackage := fmt.Sprintf("%s %s", newName, targetPackageVersion)
243-
if newPackage == node.String() {
244-
return nil
245-
}
246-
247-
if err = file.Doc.ReplaceAtPath(node.GetPath(), newPackage); err != nil {
248271
return err
249272
}
250-
251-
if newName != packageVersion.Name {
252-
replacements[packageVersion.Original] = fmt.Sprintf("%s %s", newName, targetPackageVersion)
253-
} else {
254-
replacements[packageVersion.Original] = targetPackageVersion
273+
if changed {
274+
hasChange = true
255275
}
256-
hasChange = true
257276
return nil
258277
})
259278
if err != nil {
260279
return nil, errors.Wrap(err, "unable to replace package references")
261280
}
262281

282+
if file.Doc.IsRunDefinition() {
283+
baseConfig := file.Doc.TryReadStringAtPath("$.base.config")
284+
if baseConfig != "" && baseConfig != "none" {
285+
changed, err := s.updatePackageReferenceAtPath(file.Doc, "$.base.config", baseConfig, update, packageVersions, versionPicker, replacements)
286+
if err != nil {
287+
return nil, errors.Wrap(err, "unable to replace base.config package reference")
288+
}
289+
if changed {
290+
hasChange = true
291+
}
292+
}
293+
}
294+
263295
if hasChange {
264296
docs[file.Entry.OriginalPath] = file.Doc
265297
}

internal/cli/service_resolve_packages_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,4 +290,100 @@ tasks:
290290
})
291291
})
292292
})
293+
294+
t.Run("resolves base.config package reference", func(t *testing.T) {
295+
t.Run("pins a versionless base.config to the latest version", func(t *testing.T) {
296+
s := setupTest(t)
297+
298+
s.mockAPI.MockGetPackageVersions = func() (*api.PackageVersionsResult, error) {
299+
return &api.PackageVersionsResult{
300+
LatestMajor: map[string]string{"rwx/base": "1.2.3"},
301+
}, nil
302+
}
303+
304+
originalContents := `base:
305+
image: ubuntu:24.04
306+
config: rwx/base
307+
308+
tasks:
309+
- key: foo
310+
run: echo hello
311+
`
312+
err := os.WriteFile(filepath.Join(s.tmp, "foo.yaml"), []byte(originalContents), 0o644)
313+
require.NoError(t, err)
314+
315+
_, err = s.service.ResolvePackages(cli.ResolvePackagesConfig{
316+
Files: []string{filepath.Join(s.tmp, "foo.yaml")},
317+
LatestVersionPicker: cli.PickLatestMajorVersion,
318+
})
319+
require.NoError(t, err)
320+
321+
contents, err := os.ReadFile(filepath.Join(s.tmp, "foo.yaml"))
322+
require.NoError(t, err)
323+
require.Contains(t, string(contents), "config: rwx/base 1.2.3")
324+
})
325+
326+
t.Run("leaves an already-pinned base.config untouched", func(t *testing.T) {
327+
s := setupTest(t)
328+
329+
s.mockAPI.MockGetPackageVersions = func() (*api.PackageVersionsResult, error) {
330+
return &api.PackageVersionsResult{
331+
LatestMajor: map[string]string{"rwx/base": "1.2.3"},
332+
}, nil
333+
}
334+
335+
originalContents := `base:
336+
image: ubuntu:24.04
337+
config: rwx/base 1.0.0
338+
339+
tasks:
340+
- key: foo
341+
run: echo hello
342+
`
343+
err := os.WriteFile(filepath.Join(s.tmp, "foo.yaml"), []byte(originalContents), 0o644)
344+
require.NoError(t, err)
345+
346+
_, err = s.service.ResolvePackages(cli.ResolvePackagesConfig{
347+
Files: []string{filepath.Join(s.tmp, "foo.yaml")},
348+
LatestVersionPicker: cli.PickLatestMajorVersion,
349+
})
350+
require.NoError(t, err)
351+
352+
contents, err := os.ReadFile(filepath.Join(s.tmp, "foo.yaml"))
353+
require.NoError(t, err)
354+
require.Equal(t, originalContents, string(contents))
355+
})
356+
357+
t.Run("leaves config: none untouched", func(t *testing.T) {
358+
s := setupTest(t)
359+
360+
s.mockAPI.MockGetPackageVersions = func() (*api.PackageVersionsResult, error) {
361+
return &api.PackageVersionsResult{
362+
LatestMajor: map[string]string{"nodejs/install": "1.2.3"},
363+
}, nil
364+
}
365+
366+
originalContents := `base:
367+
image: ubuntu:24.04
368+
config: none
369+
370+
tasks:
371+
- key: foo
372+
call: nodejs/install
373+
`
374+
err := os.WriteFile(filepath.Join(s.tmp, "foo.yaml"), []byte(originalContents), 0o644)
375+
require.NoError(t, err)
376+
377+
_, err = s.service.ResolvePackages(cli.ResolvePackagesConfig{
378+
Files: []string{filepath.Join(s.tmp, "foo.yaml")},
379+
LatestVersionPicker: cli.PickLatestMajorVersion,
380+
})
381+
require.NoError(t, err)
382+
383+
contents, err := os.ReadFile(filepath.Join(s.tmp, "foo.yaml"))
384+
require.NoError(t, err)
385+
require.Contains(t, string(contents), "config: none")
386+
require.Contains(t, string(contents), "call: nodejs/install 1.2.3")
387+
})
388+
})
293389
}

0 commit comments

Comments
 (0)