Repository: rancher/fleet
Pull request #2296 (2024-04-05 12:00:28)

Add CI workflow releasing Fleet charts against a test repo
This adds new workflow `release-against-test-charts.yml`, which, when triggered manually:
1. Builds Fleet images and Rancher-compatible Fleet charts from the provided ref
2. Pushes Docker images to [ttl.sh](http://ttl.sh/) and charts to a feature branch on the specified test charts repo, using the specified `rancher/charts` branch as its base.

Created Fleet charts use hard-coded version `999.9.9+up9.9.9` to prevent collisions with existing versions on the base `rancher/charts` branch.

Refers to #1640.

This has been tested by:
1. Running this workflow against `weyfonk/fleet`'s `master`, with default `rancher/charts` branch `dev-v2.9` and default test charts repo `fleetrepoci/charts` (see [output](https://github.com/weyfonk/fleet/actions/runs/8566572045/job/23484117363))
2. Building Rancher 2.9 locally from branch `release/v2.9`
3. Installing Rancher in a local k3d cluster as follows:
```
helm upgrade --install rancher rancher-latest/rancher --devel \
     --namespace cattle-system --create-namespace \
     --set hostname=<url> \
     --set bootstrapPassword=admin \
     --set replicas=1 \
     --set rancherImage=test-tan/rancher --set rancherImageTag=dev \
     --set "extraEnv[0].name=CATTLE_CHART_DEFAULT_URL" --set "extraEnv[0].value=https://github.com/fleetrepoci/charts" \
     --set "extraEnv[1].name=CATTLE_CHART_DEFAULT_BRANCH" --set "extraEnv[1].value=fleetci-dev-v2.8-20240405112731" \
     --set "extraEnv[2].name=CATTLE_FLEET_VERSION" --set "extraEnv[2].value=999.9.9+up9.9.9"
```


Patch:
diff --git a/.github/scripts/upgrade-rancher-fleet-to-dev-fleet.sh b/.github/scripts/upgrade-rancher-fleet-to-dev-fleet.sh
index bdb70dd8..fc36433e 100755
--- a/.github/scripts/upgrade-rancher-fleet-to-dev-fleet.sh
+++ b/.github/scripts/upgrade-rancher-fleet-to-dev-fleet.sh
@@ -29,6 +29,7 @@ until helm -n cattle-fleet-system status fleet | grep -q "STATUS: deployed"; do
 sed -i 's/^version: 0/version: 9000/' charts/fleet/Chart.yaml
 
 helm upgrade fleet charts/fleet \
+  --reset-then-reuse-values \
   --wait -n cattle-fleet-system \
   --create-namespace \
   --set image.repository="$fleetRepo" \
diff --git a/.github/workflows/rancher-upgrade-fleet-to-head.yml b/.github/workflows/rancher-upgrade-fleet-to-head.yml
index 00259758..b88984bd 100644
--- a/.github/workflows/rancher-upgrade-fleet-to-head.yml
+++ b/.github/workflows/rancher-upgrade-fleet-to-head.yml
@@ -179,7 +179,7 @@ jobs:
         name: E2E tests for examples
         env:
           FLEET_E2E_NS: fleet-local
-          FLEET_E2E_NS_DOWNSTREAM: fleet-local
+          FLEET_E2E_NS_DOWNSTREAM: fleet-default
         run: |
           kubectl config use-context k3d-upstream
           ginkgo e2e/multi-cluster
diff --git a/.github/workflows/release-against-charts.yml b/.github/workflows/release-against-charts.yml
index 348aea9c..96592aa1 100644
--- a/.github/workflows/release-against-charts.yml
+++ b/.github/workflows/release-against-charts.yml
@@ -9,7 +9,7 @@ on:
       charts_ref:
         description: "Submit PR against the following rancher/charts branch (e.g. dev-v2.7)"
         required: true
-        default: "dev-v2.8"
+        default: "dev-v2.9"
       prev_fleet:
         description: "Previous Fleet version (e.g. 0.6.0-rc.3)"
         required: true
@@ -58,7 +58,7 @@ jobs:
         with:
           token: ${{secrets.PUSH_TO_FORKS_SUBMIT_PRS}}
           push-to-fork: rancherbot/charts
-          title: 'Update Fleet to v${{github.event.inputs.new_fleet}}'
+          title: '[${{github.event.inputs.charts_ref}}] fleet ${{github.event.inputs.new_chart}}+up${{github.event.inputs.new_fleet}} update'
           body: |
             Update Fleet to v${{github.event.inputs.new_fleet}}
 
diff --git a/.github/workflows/release-against-rancher.yml b/.github/workflows/release-against-rancher.yml
index 26ce5144..88c5a8f2 100644
--- a/.github/workflows/release-against-rancher.yml
+++ b/.github/workflows/release-against-rancher.yml
@@ -60,7 +60,7 @@ jobs:
         with:
           token: ${{secrets.PUSH_TO_FORKS_SUBMIT_PRS}}
           push-to-fork: rancherbot/rancher
-          title: ' Update Fleet to v${{github.event.inputs.new_fleet}}'
+          title: '[${{github.event.inputs.rancher_ref}}] fleet ${{github.event.inputs.new_chart}}+up${{github.event.inputs.new_fleet}} update'
           body: |
             Update Fleet to v${{github.event.inputs.new_fleet}}
 
diff --git a/.github/workflows/release-fleet.yml b/.github/workflows/release-fleet.yml
index 01ad7dc9..c2e2215a 100644
--- a/.github/workflows/release-fleet.yml
+++ b/.github/workflows/release-fleet.yml
@@ -39,11 +39,13 @@ jobs:
           args: --timeout=10m --config=.golangci.json
 
       - name: Run FOSSA scan
+        if: contains(github.ref, 'rc') != true
         uses: fossas/fossa-action@v1.3.3
         with:
           api-key: ${{ secrets.FOSSA_API_KEY }}
 
       - name: Run FOSSA tests
+        if: contains(github.ref, 'rc') != true
         uses: fossas/fossa-action@v1.3.3
         with:
           api-key: ${{ secrets.FOSSA_API_KEY }}
@@ -78,6 +80,7 @@ jobs:
 
       - name: Run GoReleaser
         uses: goreleaser/goreleaser-action@v5
+        id: goreleaser
         with:
           distribution: goreleaser
           version: v1.24.0
@@ -89,8 +92,10 @@ jobs:
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           repo: "ghcr.io/rancher"
-          tag: "${GITHUB_REF_NAME}"
+          tag: ${{ steps.goreleaser.outputs.metadata.tag }}
+          version: ${{ steps.goreleaser.outputs.metadata.version }}
         run: |
+          echo "publishing helm chart for (repo: $repo, tag: $tag, version: $version)"
           # Replace rancher/fleet, rancher/fleet-agent and rancher/gitjob image names, but not eg. rancher/kubectl
           sed -i \
               -e "s@repository: rancher/\(fleet.*\|gitjob\).*@repository: $repo/\\1@" \
@@ -102,12 +107,10 @@ jobs:
               -e "s/tag: dev/tag: $tag/" \
               charts/fleet-agent/values.yaml
 
-          version=${tag#v}
-
           helm package --version="$version" --app-version="$version" -d ./dist ./charts/fleet
           helm package --version="$version" --app-version="$version" -d ./dist ./charts/fleet-crd
           helm package --version="$version" --app-version="$version" -d ./dist ./charts/fleet-agent
 
-          for f in $(find dist/artifacts/ -name '*.tgz'); do
+          for f in $(find dist/ -name '*.tgz'); do
             gh release upload $tag $f
           done
diff --git a/.goreleaser.yml b/.goreleaser.yml
index d7fbdd72..cd25a1aa 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -127,6 +127,9 @@ dockers:
     # IDs to filter the binaries/packages.
     ids:
     - fleet-controller
+    - fleet-cli
+    - fleet-gitcloner
+    - fleet-gitjob
 
     # Templates of the Docker image names.
     image_templates:
@@ -146,12 +149,16 @@ dockers:
     - "--build-arg=ARCH=amd64"
     - "--build-arg=BUILD_ENV=goreleaser"
     - "--platform=linux/amd64"
+    extra_files: [ "package/log.sh" ]
   -
     use: buildx
     goos: linux
     goarch: arm64
     ids:
     - fleet-controller
+    - fleet-cli
+    - fleet-gitcloner
+    - fleet-gitjob
     image_templates:
     - "ghcr.io/rancher/fleet:{{ .Tag }}-linux-arm64"
     dockerfile: package/Dockerfile
@@ -165,6 +172,7 @@ dockers:
     - "--build-arg=ARCH=arm64"
     - "--build-arg=BUILD_ENV=goreleaser"
     - "--platform=linux/arm64/v8"
+    extra_files: [ "package/log.sh" ]
   # fleet-agent images
   -
     use: buildx
@@ -172,7 +180,6 @@ dockers:
     goarch: amd64
     ids:
     - fleet-agent
-    - fleet-cli
     image_templates:
     - "ghcr.io/rancher/fleet-agent:{{ .Tag }}-linux-amd64"
     dockerfile: package/Dockerfile.agent
@@ -186,14 +193,12 @@ dockers:
     - "--build-arg=ARCH=amd64"
     - "--build-arg=BUILD_ENV=goreleaser"
     - "--platform=linux/amd64"
-    extra_files: [ "package/log.sh" ]
   -
     use: buildx
     goos: linux
     goarch: arm64
     ids:
     - fleet-agent
-    - fleet-cli
     image_templates:
     - "ghcr.io/rancher/fleet-agent:{{ .Tag }}-linux-arm64"
     dockerfile: package/Dockerfile.agent
@@ -207,7 +212,6 @@ dockers:
     - "--build-arg=ARCH=arm64"
     - "--build-arg=BUILD_ENV=goreleaser"
     - "--platform=linux/arm64/v8"
-    extra_files: [ "package/log.sh" ]
 
 docker_manifests:
   # https://goreleaser.com/customization/docker_manifest/
diff --git a/go.mod b/go.mod
index 990d9dd2..b977d8ae 100644
--- a/go.mod
+++ b/go.mod
@@ -23,25 +23,25 @@ require (
 	github.com/gogits/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
 	github.com/golang/mock v1.6.0
 	github.com/google/go-cmp v0.6.0
-	github.com/google/go-containerregistry v0.17.0
+	github.com/google/go-containerregistry v0.19.1
 	github.com/gorilla/mux v1.8.1
 	github.com/hashicorp/go-getter v1.7.3
 	github.com/jpillora/backoff v1.0.0
-	github.com/onsi/ginkgo/v2 v2.15.0
+	github.com/onsi/ginkgo/v2 v2.17.1
 	github.com/onsi/gomega v1.32.0
 	github.com/otiai10/copy v1.14.0
 	github.com/pkg/errors v0.9.1
-	github.com/rancher/fleet/pkg/apis v0.0.0-00010101000000-000000000000
+	github.com/rancher/fleet/pkg/apis v0.10.0-rc.4
 	github.com/rancher/lasso v0.0.0-20240325194215-0064abcb8aee
 	github.com/rancher/wrangler/v2 v2.1.4
 	github.com/reugn/go-quartz v0.11.2
 	github.com/sirupsen/logrus v1.9.3
 	github.com/spf13/cobra v1.8.0
 	github.com/stretchr/testify v1.9.0
-	github.com/testcontainers/testcontainers-go v0.28.0
+	github.com/testcontainers/testcontainers-go v0.30.0
 	go.uber.org/mock v0.4.0
-	golang.org/x/crypto v0.21.0
-	golang.org/x/sync v0.6.0
+	golang.org/x/crypto v0.22.0
+	golang.org/x/sync v0.7.0
 	gopkg.in/go-playground/webhooks.v5 v5.17.0
 	gopkg.in/yaml.v2 v2.4.0
 	gotest.tools v2.2.0+incompatible
@@ -106,7 +106,7 @@ require (
 	github.com/evanphx/json-patch/v5 v5.9.0 // indirect
 	github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
 	github.com/fatih/color v1.16.0 // indirect
-	github.com/felixge/httpsnoop v1.0.3 // indirect
+	github.com/felixge/httpsnoop v1.0.4 // indirect
 	github.com/fsnotify/fsnotify v1.7.0 // indirect
 	github.com/ghodss/yaml v1.0.0 // indirect
 	github.com/go-errors/errors v1.4.2 // indirect
@@ -180,7 +180,7 @@ require (
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
 	github.com/opencontainers/go-digest v1.0.0 // indirect
-	github.com/opencontainers/image-spec v1.1.0-rc6 // indirect
+	github.com/opencontainers/image-spec v1.1.0 // indirect
 	github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
 	github.com/pjbgf/sha1cd v0.3.0 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
@@ -211,19 +211,19 @@ require (
 	github.com/xlab/treeprint v1.2.0 // indirect
 	github.com/yusufpapurcu/wmi v1.2.3 // indirect
 	go.opencensus.io v0.24.0 // indirect
-	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 // indirect
-	go.opentelemetry.io/otel v1.19.0 // indirect
-	go.opentelemetry.io/otel/metric v1.19.0 // indirect
-	go.opentelemetry.io/otel/trace v1.19.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
+	go.opentelemetry.io/otel v1.24.0 // indirect
+	go.opentelemetry.io/otel/metric v1.24.0 // indirect
+	go.opentelemetry.io/otel/trace v1.24.0 // indirect
 	go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	go.uber.org/zap v1.26.0 // indirect
 	golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
-	golang.org/x/mod v0.15.0 // indirect
+	golang.org/x/mod v0.16.0 // indirect
 	golang.org/x/net v0.22.0 // indirect
 	golang.org/x/oauth2 v0.17.0 // indirect
-	golang.org/x/sys v0.18.0 // indirect
-	golang.org/x/term v0.18.0 // indirect
+	golang.org/x/sys v0.19.0 // indirect
+	golang.org/x/term v0.19.0 // indirect
 	golang.org/x/text v0.14.0 // indirect
 	golang.org/x/time v0.5.0 // indirect
 	golang.org/x/tools v0.18.0 // indirect
diff --git a/go.sum b/go.sum
index fcf5ded3..b53f6f3f 100644
--- a/go.sum
+++ b/go.sum
@@ -345,8 +345,8 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv
 github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
 github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
 github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
-github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
-github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/foxcpp/go-mockdns v1.0.0 h1:7jBqxd3WDWwi/6WhDvacvH1XsN3rOLXyHM1uhvIx6FI=
 github.com/foxcpp/go-mockdns v1.0.0/go.mod h1:lgRN6+KxQBawyIghpnl5CezHFGS9VLzvtVlwxvzXTQ4=
 github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
@@ -475,8 +475,8 @@ github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/go-containerregistry v0.17.0 h1:5p+zYs/R4VGHkhyvgWurWrpJ2hW4Vv9fQI+GzdcwXLk=
-github.com/google/go-containerregistry v0.17.0/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ=
+github.com/google/go-containerregistry v0.19.1 h1:yMQ62Al6/V0Z7CqIrrS1iYoA5/oQCm88DeNujc7C1KY=
+github.com/google/go-containerregistry v0.19.1/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
@@ -698,14 +698,14 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
 github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
 github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
 github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
-github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY=
-github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM=
+github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8=
+github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs=
 github.com/onsi/gomega v1.32.0 h1:JRYU78fJ1LPxlckP6Txi/EYqJvjtMrDC04/MM5XRHPk=
 github.com/onsi/gomega v1.32.0/go.mod h1:a4x4gW6Pz2yK1MAmvluYme5lvYTn61afQ2ETw/8n4Lg=
 github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
-github.com/opencontainers/image-spec v1.1.0-rc6 h1:XDqvyKsJEbRtATzkgItUqBA7QHk58yxX1Ov9HERHNqU=
-github.com/opencontainers/image-spec v1.1.0-rc6/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
+github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
+github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
 github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
 github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
 github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=
@@ -806,8 +806,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
-github.com/testcontainers/testcontainers-go v0.28.0 h1:1HLm9qm+J5VikzFDYhOd+Zw12NtOl+8drH2E8nTY1r8=
-github.com/testcontainers/testcontainers-go v0.28.0/go.mod h1:COlDpUXbwW3owtpMkEB1zo9gwb1CoKVKlyrVPejF4AU=
+github.com/testcontainers/testcontainers-go v0.30.0 h1:jmn/XS22q4YRrcMwWg0pAwlClzs/abopbsBzrepyc4E=
+github.com/testcontainers/testcontainers-go v0.30.0/go.mod h1:K+kHNGiM5zjklKjgTtcrEetF3uhWbMUyqAQoyoh8Pf0=
 github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
 github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
 github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
@@ -851,20 +851,20 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
 go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
 go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
 go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 h1:x8Z78aZx8cOF0+Kkazoc7lwUNMGy0LrzEMxTm4BbTxg=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0/go.mod h1:62CPTSry9QZtOaSsE3tOzhx6LzDhHnXJ6xHeMNNiM6Q=
-go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs=
-go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
+go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
+go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
-go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE=
-go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8=
+go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
+go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
 go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o=
 go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A=
-go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg=
-go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo=
+go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
+go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
 go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
 go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
@@ -889,8 +889,8 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0
 golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
 golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
 golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
-golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
-golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
+golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
+golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -930,8 +930,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
-golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
+golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1029,8 +1029,8 @@ golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
-golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1112,8 +1112,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
-golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
+golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -1121,8 +1121,8 @@ golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
-golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
-golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
+golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
+golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
diff --git a/internal/bundlereader/loaddirectory.go b/internal/bundlereader/loaddirectory.go
index 38eeacda..3944de9e 100644
--- a/internal/bundlereader/loaddirectory.go
+++ b/internal/bundlereader/loaddirectory.go
@@ -17,7 +17,6 @@ import (
 	"unicode/utf8"
 
 	"github.com/hashicorp/go-getter"
-	"github.com/pkg/errors"
 	"github.com/rancher/fleet/internal/content"
 	"github.com/rancher/fleet/internal/helmupdater"
 	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
@@ -300,7 +299,7 @@ func GetContent(ctx context.Context, base, source, version string, auth Auth, di
 		return nil
 	})
 	if err != nil {
-		return nil, errors.Wrapf(err, "failed to read %s relative to %s", orgSource, base)
+		return nil, fmt.Errorf("failed to read %s relative to %s: %w", orgSource, base, err)
 	}
 
 	return files, nil
@@ -316,6 +315,11 @@ func downloadOCIChart(name, version, path string, auth Auth) (string, error) {
 		return "", err
 	}
 
+	registryClient, err = registry.NewClient()
+	if err != nil {
+		return "", err
+	}
+
 	// Helm does not support direct authentication for private OCI registries when a chart is downloaded
 	// so it is necessary to login before via Helm which stores the registry token in a configuration
 	// file on the system
@@ -325,11 +329,6 @@ func downloadOCIChart(name, version, path string, auth Auth) (string, error) {
 			addr = fmt.Sprintf("%s:%s", addr, port)
 		}
 
-		registryClient, err = registry.NewClient()
-		if err != nil {
-			return "", err
-		}
-
 		err = registryClient.Login(
 			addr,
 			registry.LoginOptInsecure(false),
@@ -348,7 +347,7 @@ func downloadOCIChart(name, version, path string, auth Auth) (string, error) {
 
 	saved, _, err := c.DownloadTo(name, version, path)
 	if err != nil {
-		return "", fmt.Errorf("Helm chart download: %v", err)
+		return "", fmt.Errorf("helm chart download: %w", err)
 	}
 
 	// Logout to remove the token configuration file from the system again


Test patch:
diff --git a/.github/scripts/release-against-test-charts.sh b/.github/scripts/release-against-test-charts.sh
new file mode 100755
index 00000000..7feb1b86
--- /dev/null
+++ b/.github/scripts/release-against-test-charts.sh
@@ -0,0 +1,75 @@
+#!/bin/bash
+#
+# Submit new Fleet version against a test fork of rancher/charts
+
+set -ue
+
+PREV_FLEET_VERSION="$1"   # e.g. 0.5.2-rc.3
+NEW_FLEET_VERSION="$2"
+PREV_CHART_VERSION="$3"   # e.g. 101.2.0
+NEW_CHART_VERSION="$4"
+UUID="$5" 		  # for ttl.sh image resolution
+
+if [ -z "${GITHUB_WORKSPACE:-}" ]; then
+    CHARTS_DIR="$(dirname -- "$0")/../../../charts"
+else
+    CHARTS_DIR="${GITHUB_WORKSPACE}/charts"
+fi
+
+pushd "${CHARTS_DIR}" > /dev/null
+
+if [ ! -e ~/.gitconfig ]; then
+    git config --global user.name "fleet-bot"
+    git config --global user.email fleet@suse.de
+fi
+
+if [ ! -f bin/charts-build-scripts ]; then
+    make pull-scripts
+fi
+
+if grep -q "version: ${PREV_CHART_VERSION}" ./packages/fleet/fleet/package.yaml && grep -q "${PREV_FLEET_VERSION}" ./packages/fleet/fleet/package.yaml; then
+
+    find ./packages/fleet/ -type f -exec sed -i -e "s/${PREV_FLEET_VERSION}/${NEW_FLEET_VERSION}/g" {} \;
+    find ./packages/fleet/ -type f -exec sed -i -e "s/version: ${PREV_CHART_VERSION}/version: ${NEW_CHART_VERSION}/g" {} \;
+else
+    echo "Previous Fleet version references do not exist in ./packages/fleet/ so replacing it with the new version is not possible. Exiting..."
+    exit 1
+fi
+
+for i in fleet fleet-crd fleet-agent; do
+    yq --inplace "del( .${i}.[] | select(. == \"${PREV_CHART_VERSION}+up${PREV_FLEET_VERSION}\") )" release.yaml
+    yq --inplace ".${i} += [\"${NEW_CHART_VERSION}+up${NEW_FLEET_VERSION}\"]" release.yaml
+done
+
+git add packages/fleet release.yaml
+git commit -m "Updating to Fleet v${NEW_FLEET_VERSION}"
+
+NEW_FULL_VERSION=${NEW_CHART_VERSION}+up${NEW_FLEET_VERSION}
+
+for suffix in '' '-agent' '-crd'; do
+    chart_dir=charts/fleet$suffix/${NEW_FULL_VERSION}
+    mkdir -p $chart_dir
+    cp -r ../fleet/charts/fleet$suffix/* $chart_dir
+    cd $chart_dir
+
+    # Replace rancher/fleet and rancher/fleet-agent image names, but not eg. rancher/kubectl
+    sed -i \
+        -e "s@repository: rancher/\(fleet.*\).*@repository: ttl.sh/rancher-\\1-$UUID@" \
+        -e "s/tag: dev/tag: 1h/" \
+        values.yaml
+
+    cd -
+
+    helm package \
+        --version="$NEW_FULL_VERSION" \
+        --app-version="$NEW_FLEET_VERSION" \
+        -d ./assets/fleet$suffix \
+        $chart_dir
+done
+
+make index # Merge new chart entries into `index.yaml`
+
+git add assets/fleet* charts/fleet* index.yaml
+git commit -m "Autogenerated changes for Fleet v${NEW_FLEET_VERSION}"
+
+popd > /dev/null
diff --git a/.github/workflows/release-against-test-charts.yml b/.github/workflows/release-against-test-charts.yml
new file mode 100644
index 00000000..6ecc3d39
--- /dev/null
+++ b/.github/workflows/release-against-test-charts.yml
@@ -0,0 +1,164 @@
+name: Release Fleet against test Rancher charts repo
+on:
+  workflow_dispatch:
+    inputs:
+      ref:
+        description: "Branch to use for GitHub action workflow"
+        required: true
+        default: "master"
+      charts_base_branch:
+        description: "Use the following rancher/charts branch as a base (e.g. dev-v2.7)"
+        required: true
+        default: "dev-v2.9"
+      charts_repo:
+        description: "Push to the following Rancher charts repo (which must exist)"
+        required: true
+        default: "fleetrepoci/charts"
+      charts_target_branch:
+        description: "Use the following branch as a destination when pushing to the charts repo"
+
+env:
+  GOARCH: amd64
+
+permissions:
+  contents: write
+
+jobs:
+  push-test-rancher-charts:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Set environment variables
+        run: |
+          tmp=${{github.event.inputs.ref}}
+          ref=${tmp:-${{github.ref}}}
+
+          tmp=${{github.event.inputs.charts_base_branch}}
+          charts_base_branch=${tmp:-'dev-v2.8'}
+
+          tmp=${{github.event.inputs.charts_repo}}
+          charts_repo=${tmp:-fleetrepoci/charts}
+
+          echo "ref=$ref" >> "$GITHUB_ENV"
+          echo "charts_base_branch=$charts_base_branch" >> "$GITHUB_ENV"
+          echo "charts_repo=$charts_repo" >> "$GITHUB_ENV"
+
+      - name: Checkout rancher/fleet
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+          ref: ${{github.event.inputs.ref}}
+          path: fleet
+
+      - uses: actions/setup-go@v4
+        with:
+          go-version-file: 'fleet/go.mod'
+          check-latest: true
+
+      - name: Build fleet binaries
+        run: |
+          cd ./fleet
+          ./.github/scripts/build-fleet-binaries.sh
+          cd -
+
+      - name: Get UUID
+        id: uuid
+        run: echo "uuid=$(uuidgen)" >> $GITHUB_OUTPUT
+
+      - name: Extract metadata for Fleet controller image
+        id: meta-fleet
+        uses: docker/metadata-action@v5
+        with:
+          images: |
+            ttl.sh/rancher-fleet-${{ steps.uuid.outputs.uuid }}
+          tags: type=raw,value=1h
+
+      - name: Build and push Fleet controller image
+        uses: docker/build-push-action@v5
+        with:
+          context: ./fleet
+          file: ./fleet/package/Dockerfile
+          build-args: |
+            ARCH=${{ env.GOARCH }}
+          push: true
+          tags: ${{ steps.meta-fleet.outputs.tags }}
+          labels: ${{ steps.meta-fleet.outputs.labels }}
+
+      - name: Extract metadata for Fleet agent image
+        id: meta-fleet-agent
+        uses: docker/metadata-action@v5
+        with:
+          images: |
+            ttl.sh/rancher-fleet-agent-${{ steps.uuid.outputs.uuid }}
+          tags: type=raw,value=1h
+
+      - name: Build and push Fleet agent image
+        uses: docker/build-push-action@v5
+        with:
+          context: ./fleet
+          file: ./fleet/package/Dockerfile.agent
+          build-args: |
+            ARCH=${{ env.GOARCH }}
+          push: true
+          tags: ${{ steps.meta-fleet-agent.outputs.tags }}
+          labels: ${{ steps.meta-fleet-agent.outputs.labels }}
+
+      - name: Checkout test charts repo
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+          token: ${{secrets.CI_PUSH_TO_FLEETREPOCI}}
+          repository: ${{ env.charts_repo }}
+          path: charts
+
+      - name: Compute target branch name
+        run: |
+          target_branch_name=${{github.event.inputs.charts_target_branch}}
+
+          if [ ${{github.event_name}} = 'pull_request' ]; then
+            target_branch_name=${{github.event.pull_request.head.ref}}
+          elif [ -z ${{github.event.inputs.charts_target_branch}} ]; then
+            target_branch_name=${{env.charts_base_branch}}-$(date +%Y%m%d%H%M%S)
+          fi
+
+          # Append prefix if not present
+          prefix='fleetci-'
+          if [ "${target_branch_name/$prefix/}" = $target_branch_name ]; then
+            target_branch_name=$prefix$target_branch_name
+          fi
+
+          echo "target_branch=$target_branch_name" >> "$GITHUB_ENV"
+
+      - name: Set up test charts repo with latest upstream state
+        run: |
+          cd charts
+          # Add upstream remote and fetch from it
+          git remote add -f upstream https://github.com/rancher/charts
+
+          git checkout -b $target_branch upstream/${{env.charts_base_branch}}
+          if [ $? -eq 128 ]; then # branch already exists
+            git checkout $target_branch
+            git rebase upstream/${{env.charts_base_branch}}
+          fi
+
+      - name: Install dependencies
+        run: sudo snap install yq --channel=v4/stable
+
+      - name: Run release script
+        run: |
+          fleet_version=9.9.9
+
+          prev_chart=$(ls -1 ./charts/charts/fleet | tail -n 1)
+          prev_fleet=${prev_chart#*+up}
+          prev_chart=${prev_chart/+*}
+
+          ./fleet/.github/scripts/release-against-test-charts.sh \
+            $prev_fleet \
+            $fleet_version \
+            $prev_chart \
+            999.9.9 \
+            ${{ steps.uuid.outputs.uuid }}
+
+      - name: Push to custom branch
+        run: |
+          cd charts
+          git push -u origin $target_branch
diff --git a/e2e/multi-cluster/depends_on_test.go b/e2e/multi-cluster/depends_on_test.go
index 3756181f..2f24f73d 100644
--- a/e2e/multi-cluster/depends_on_test.go
+++ b/e2e/multi-cluster/depends_on_test.go
@@ -44,6 +44,10 @@ var _ = Describe("Bundle Depends On", Label("difficult"), func() {
 	})
 
 	AfterEach(func() {
+		if namespace == "" {
+			// test failed in BeforeEach
+			return
+		}
 		out, err := k.Delete("ns", namespace)
 		Expect(err).ToNot(HaveOccurred(), out)
 
diff --git a/e2e/testenv/infra/go.mod b/e2e/testenv/infra/go.mod
index d99b47ea..1eb63832 100644
--- a/e2e/testenv/infra/go.mod
+++ b/e2e/testenv/infra/go.mod
@@ -9,7 +9,7 @@ replace k8s.io/client-go => k8s.io/client-go v0.29.0
 require (
 	github.com/rancher/fleet v0.10.0-rc.5
 	github.com/spf13/cobra v1.8.0
-	golang.org/x/crypto v0.19.0
+	golang.org/x/crypto v0.22.0
 	helm.sh/helm/v3 v3.14.3
 )
 
@@ -24,7 +24,7 @@ require (
 	github.com/distribution/reference v0.5.0 // indirect
 	github.com/docker/cli v25.0.1+incompatible // indirect
 	github.com/docker/distribution v2.8.3+incompatible // indirect
-	github.com/docker/docker v25.0.5+incompatible // indirect
+	github.com/docker/docker v26.0.0+incompatible // indirect
 	github.com/docker/docker-credential-helpers v0.7.0 // indirect
 	github.com/docker/go-connections v0.5.0 // indirect
 	github.com/docker/go-metrics v0.0.1 // indirect
@@ -54,7 +54,7 @@ require (
 	go.opentelemetry.io/otel/trace v1.19.0 // indirect
 	golang.org/x/net v0.21.0 // indirect
 	golang.org/x/sync v0.6.0 // indirect
-	golang.org/x/sys v0.17.0 // indirect
+	golang.org/x/sys v0.19.0 // indirect
 	golang.org/x/tools v0.18.0 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
 	google.golang.org/grpc v1.58.3 // indirect
diff --git a/e2e/testenv/infra/go.sum b/e2e/testenv/infra/go.sum
index 87a00f9d..6902d3fa 100644
--- a/e2e/testenv/infra/go.sum
+++ b/e2e/testenv/infra/go.sum
@@ -44,8 +44,8 @@ github.com/docker/cli v25.0.1+incompatible h1:mFpqnrS6Hsm3v1k7Wa/BO23oz0k121MTbT
 github.com/docker/cli v25.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
 github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
 github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
-github.com/docker/docker v25.0.5+incompatible h1:UmQydMduGkrD5nQde1mecF/YnSbTOaPeFIeP5C4W+DE=
-github.com/docker/docker v25.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker v26.0.0+incompatible h1:Ng2qi+gdKADUa/VM+6b6YaY2nlZhk/lVJiKR/2bMudU=
+github.com/docker/docker v26.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=
 github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=
 github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
@@ -194,8 +194,8 @@ go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1
 go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
-golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
+golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
 golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
 golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -211,8 +211,8 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
-golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
+golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
diff --git a/internal/bundlereader/loaddirectory_test.go b/internal/bundlereader/loaddirectory_test.go
index 4f88968e..b459f0df 100644
--- a/internal/bundlereader/loaddirectory_test.go
+++ b/internal/bundlereader/loaddirectory_test.go
@@ -344,6 +344,91 @@ func TestGetContent(t *testing.T) {
 	}
 }
 
+func TestGetContentOCI(t *testing.T) {
+	cases := []struct {
+		name    string
+		source  string
+		version string
+
+		result      []string
+		expectedErr string
+	}{
+		// Note: These tests rely on external hosts and DNS resolution.
+		// We could just test the helm registry client is initialized
+		// correctly, however for now these tests document different
+		// scenarios nicely.
+		{
+			name:   "OCI URL without version",
+			source: "oci://ghcr.io/rancher/fleet-test-configmap-chart",
+			result: []string{
+				"fleet-test-configmap-chart/Chart.yaml",
+				"fleet-test-configmap-chart/values.yaml",
+				"fleet-test-configmap-chart/templates/configmap.yaml",
+			},
+		},
+		{
+			name:    "OCI URL with version",
+			source:  "oci://ghcr.io/rancher/fleet-test-configmap-chart",
+			version: "0.1.0",
+			result: []string{
+				"fleet-test-configmap-chart/Chart.yaml",
+				"fleet-test-configmap-chart/values.yaml",
+				"fleet-test-configmap-chart/templates/configmap.yaml",
+			},
+		},
+		{
+			name:        "OCI URL with invalid version",
+			source:      "oci://ghcr.io/rancher/fleet-test-configmap-chart",
+			version:     "latest",
+			expectedErr: "helm chart download: improper constraint: latest",
+		},
+		{
+			name:        "Non-existing OCI URL without version",
+			source:      "oci://non-existing-hostname/charts/chart",
+			expectedErr: "dial tcp: lookup non-existing-hostname",
+		},
+		{
+			name:        "Non-existing OCI URL with invalid version",
+			source:      "oci://non-existing-hostname/charts/chart",
+			version:     "latest",
+			expectedErr: "dial tcp: lookup non-existing-hostname",
+		},
+		{
+			name:        "Invalid OCI URL which includes version too",
+			source:      "oci://non-existing-hostname/charts/chart:1.0",
+			version:     "1.0",
+			expectedErr: "helm chart download: invalid_reference: invalid tag",
+		},
+		{
+			name:        "Non-existing OCI URL with valid semver",
+			source:      "oci://non-existing-hostname/charts/chart",
+			version:     "1.0",
+			expectedErr: "helm chart download: failed to do request",
+		},
+	}
+
+	assert := assert.New(t)
+
+	base, err := os.MkdirTemp("", "test-fleet")
+	require.NoError(t, err)
+
+	defer os.RemoveAll(base)
+
+	for _, c := range cases {
+		t.Run(c.name, func(t *testing.T) {
+			result, err := bundlereader.GetContent(context.Background(), base, c.source, c.version, bundlereader.Auth{}, false)
+			if c.expectedErr == "" {
+				assert.NoError(err)
+				for k := range result {
+					assert.Contains(c.result, k)
+				}
+			} else {
+				assert.ErrorContains(err, c.expectedErr, c.name)
+			}
+		})
+	}
+}
+
 // createDirStruct generates and populates a directory structure which root is node, placing it at basePath.
 func createDirStruct(t *testing.T, basePath string, node fsNode) string {
 	path := filepath.Join(basePath, node.name)