diff --git a/.forgejo/upgrades/test-upgrade.sh b/.forgejo/upgrades/test-upgrade.sh
index 53d0cf7fb9..287a92a591 100755
--- a/.forgejo/upgrades/test-upgrade.sh
+++ b/.forgejo/upgrades/test-upgrade.sh
@@ -44,14 +44,20 @@ function dependencies() {
     if ! which curl daemon jq git-lfs > /dev/null ; then
         $SUDO apt-get install -y -qq curl daemon git-lfs jq
     fi
-    if ! which minio mc > /dev/null ; then
-        $SUDO curl -sS https://dl.min.io/client/mc/release/linux-amd64/mc -o /usr/local/bin/mc
+
+    if ! test -f /usr/local/bin/mc || ! test -f /usr/local/bin/minio  > /dev/null ; then
+        $SUDO curl --fail -sS https://dl.min.io/client/mc/release/linux-amd64/mc -o /usr/local/bin/mc
+        $SUDO curl --fail -sS https://dl.min.io/server/minio/release/linux-amd64/archive/minio.RELEASE.2023-08-23T10-07-06Z -o /usr/local/bin/minio
+    fi
+    if ! test -x /usr/local/bin/mc || ! test -x /usr/local/bin/minio  > /dev/null ; then
         $SUDO chmod +x /usr/local/bin/mc
-        $SUDO curl -sS https://dl.min.io/server/minio/release/linux-amd64/archive/minio.RELEASE.2023-08-23T10-07-06Z -o /usr/local/bin/minio
         $SUDO chmod +x /usr/local/bin/minio
     fi
-    if ! which garage > /dev/null ; then
-        $SUDO curl -sS https://garagehq.deuxfleurs.fr/_releases/v0.8.2/x86_64-unknown-linux-musl/garage -o /usr/local/bin/garage
+
+    if ! test -f /usr/local/bin/garage > /dev/null ; then
+        $SUDO curl --fail -sS https://garagehq.deuxfleurs.fr/_releases/v0.8.2/x86_64-unknown-linux-musl/garage -o /usr/local/bin/garage
+    fi
+    if ! test -x /usr/local/bin/garage  > /dev/null ; then
         $SUDO chmod +x /usr/local/bin/garage
     fi
 }
diff --git a/.forgejo/workflows/testing.yml b/.forgejo/workflows/testing.yml
index 23261c9db1..fcba3ee681 100644
--- a/.forgejo/workflows/testing.yml
+++ b/.forgejo/workflows/testing.yml
@@ -161,24 +161,3 @@ jobs:
           RACE_ENABLED: true
           TEST_TAGS: gogit sqlite sqlite_unlock_notify
           USE_REPO_TEST_DIR: 1
-  upgrade:
-    needs: [test-sqlite]
-    runs-on: docker
-    container:
-      image: codeberg.org/forgejo/test_env:main
-    steps:
-      - uses: https://code.forgejo.org/actions/checkout@v3
-      - uses: https://code.forgejo.org/actions/setup-go@v4
-        with:
-          go-version: "1.21"
-      - run: |
-          git config --add safe.directory '*'
-          chown -R gitea:gitea . /go
-      - run: |
-          su gitea -c 'make deps-backend'
-      - run: |
-          script=$(pwd)/.forgejo/upgrades/test-upgrade.sh
-          $script run dependencies
-          $script clobber
-          su gitea -c "$script test_upgrades"
-
diff --git a/.forgejo/workflows/upgrade.yml b/.forgejo/workflows/upgrade.yml
new file mode 100644
index 0000000000..e07a349944
--- /dev/null
+++ b/.forgejo/workflows/upgrade.yml
@@ -0,0 +1,45 @@
+name: upgrade
+
+on:
+  pull_request_review:
+  push:
+    branches:
+      - 'forgejo*'
+      - 'v*/forgejo*'
+
+jobs:
+  upgrade:
+    runs-on: docker
+    container:
+      image: codeberg.org/forgejo/test_env:main
+    steps:
+      - run: apt-get install -y -qq zstd
+
+      - name: cache S3 binaries
+        id: S3
+        uses: https://code.forgejo.org/actions/cache@v3
+        with:
+          path: |
+            /usr/local/bin/minio
+            /usr/local/bin/mc
+            /usr/local/bin/garage
+          key: S3
+
+      - name: skip if S3 cache hit
+        if: steps.S3.outputs.cache-hit != 'true'
+        run: echo no hit
+
+      - uses: https://code.forgejo.org/actions/checkout@v3
+      - uses: https://code.forgejo.org/actions/setup-go@v4
+        with:
+          go-version: "1.21"
+      - run: |
+          git config --add safe.directory '*'
+          chown -R gitea:gitea . /go
+      - run: |
+          su gitea -c 'make deps-backend'
+      - run: |
+          script=$(pwd)/.forgejo/upgrades/test-upgrade.sh
+          $script run dependencies
+          $script clobber
+          su gitea -c "$script test_upgrades"