diff --git a/.forgejo/testdata/build-release/Dockerfile b/.forgejo/testdata/build-release/Dockerfile
index 4b6933845..8dccad281 100644
--- a/.forgejo/testdata/build-release/Dockerfile
+++ b/.forgejo/testdata/build-release/Dockerfile
@@ -1,3 +1,4 @@
-FROM public.ecr.aws/docker/library/alpine:3.18
+FROM code.forgejo.org/oci/alpine:3.19
+ARG RELEASE_VERSION=unkown
 RUN mkdir -p /app/gitea
-RUN ( echo '#!/bin/sh' ; echo "echo forgejo v1.2.3" ) > /app/gitea/gitea ; chmod +x /app/gitea/gitea
+RUN ( echo '#!/bin/sh' ; echo "echo forgejo v$RELEASE_VERSION" ) > /app/gitea/gitea ; chmod +x /app/gitea/gitea
diff --git a/.forgejo/workflows/build-release-integration.yml b/.forgejo/workflows/build-release-integration.yml
index 32e67964b..cdcbf3262 100644
--- a/.forgejo/workflows/build-release-integration.yml
+++ b/.forgejo/workflows/build-release-integration.yml
@@ -34,10 +34,10 @@ jobs:
           lxc-ip-prefix: 10.0.9
 
       - name: publish the forgejo release
+        shell: bash
         run: |
           set -x
 
-          version=1.2.3
           cat > /etc/docker/daemon.json <<EOF
             {
               "insecure-registries" : ["${{ steps.forgejo.outputs.host-port }}"]
@@ -53,6 +53,37 @@ jobs:
           url=http://root:admin1234@${{ steps.forgejo.outputs.host-port }}
           export FORGEJO_RUNNER_LOGS="${{ steps.forgejo.outputs.runner-logs }}"
 
+          function sanity_check() {
+            local url=$1 version=$2
+            #
+            # Minimal sanity checks. Since the binary
+            # is a script shell it does not test the sanity of the cross
+            # build, only the sanity of the naming of the binaries.
+            #
+            for arch in amd64 arm64 arm-6 ; do
+              local binary=forgejo-$version-linux-$arch
+              for suffix in '' '.xz' ; do
+                curl --fail -L -sS $url/root/forgejo/releases/download/v$version/$binary$suffix > $binary$suffix
+                if test "$suffix" = .xz ; then
+                  unxz --keep $binary$suffix
+                fi
+                chmod +x $binary
+                ./$binary --version | grep $version
+                curl --fail -L -sS $url/root/forgejo/releases/download/v$version/$binary$suffix.sha256 > $binary$suffix.sha256
+                shasum -a 256 --check $binary$suffix.sha256
+                rm $binary$suffix
+              done
+            done
+
+            local sources=forgejo-src-$version.tar.gz
+            curl --fail -L -sS $url/root/forgejo/releases/download/v$version/$sources > $sources
+            curl --fail -L -sS $url/root/forgejo/releases/download/v$version/$sources.sha256 > $sources.sha256
+            shasum -a 256 --check $sources.sha256
+
+            docker pull ${{ steps.forgejo.outputs.host-port }}/root/forgejo:$version
+            docker pull ${{ steps.forgejo.outputs.host-port }}/root/forgejo:$version-rootless
+          }
+
           #
           # Create a new project with a fake forgejo and the release workflow only
           #
@@ -62,46 +93,41 @@ jobs:
           cp $dir/Dockerfile $dir/Dockerfile.rootless
 
           forgejo-test-helper.sh push $dir $url root forgejo
-          sha=$(forgejo-test-helper.sh branch_tip $url root/forgejo main)
+
+          forgejo-curl.sh api_json -X PUT --data-raw '{"data":"${{ steps.forgejo.outputs.token }}"}' $url/api/v1/repos/root/forgejo/actions/secrets/TOKEN
+          forgejo-curl.sh api_json -X PUT --data-raw '{"data":"root"}' $url/api/v1/repos/root/forgejo/actions/secrets/DOER
+          forgejo-curl.sh api_json -X PUT --data-raw '{"data":"true"}' $url/api/v1/repos/root/forgejo/actions/secrets/VERBOSE
 
           #
           # Push a tag to trigger the release workflow and wait for it to complete
           #
+          version=1.2.3
+          sha=$(forgejo-test-helper.sh branch_tip $url root/forgejo main)
           forgejo-curl.sh api_json --data-raw '{"tag_name": "v'$version'", "target": "'$sha'"}' $url/api/v1/repos/root/forgejo/tags
-          forgejo-curl.sh api_json -X PUT --data-raw '{"data":"${{ steps.forgejo.outputs.token }}"}' $url/api/v1/repos/root/forgejo/actions/secrets/TOKEN
-          forgejo-curl.sh api_json -X PUT --data-raw '{"data":"root"}' $url/api/v1/repos/root/forgejo/actions/secrets/DOER
           LOOPS=180 forgejo-test-helper.sh wait_success "$url" root/forgejo $sha
+          sanity_check $url $version
 
           #
-          # uncomment to see the logs even when everything is reported to be working ok
+          # Push a commit to a branch that triggers the build of a test release
           #
-          #cat $FORGEJO_RUNNER_LOGS
+          version=forgejo-test
+          (
+            git clone $url/root/forgejo /tmp/forgejo
+            cd /tmp/forgejo
+            date > DATE
+            git config user.email root@example.com
+            git config user.name username
+            git add .
+            git commit -m 'update'
+            git push $url/root/forgejo main:forgejo
+          )
+          sha=$(forgejo-test-helper.sh branch_tip $url root/forgejo forgejo)
+          LOOPS=180 forgejo-test-helper.sh wait_success "$url" root/forgejo $sha
+          sanity_check $url $version
 
-          #
-          # Minimal sanity checks. e2e test is for the setup-forgejo
-          # action and the infrastructure playbook. Since the binary
-          # is a script shell it does not test the sanity of the cross
-          # build, only the sanity of the naming of the binaries.
-          #
-          for arch in amd64 arm64 arm-6 ; do
-            binary=forgejo-$version-linux-$arch
-            for suffix in '' '.xz' ; do
-              curl --fail -L -sS $url/root/forgejo/releases/download/v$version/$binary$suffix > $binary$suffix
-              if test "$suffix" = .xz ; then
-                unxz --keep $binary$suffix
-              fi
-              chmod +x $binary
-              ./$binary --version | grep $version
-              curl --fail -L -sS $url/root/forgejo/releases/download/v$version/$binary$suffix.sha256 > $binary$suffix.sha256
-              shasum -a 256 --check $binary$suffix.sha256
-              rm $binary$suffix
-            done
-          done
-
-          sources=forgejo-src-$version.tar.gz
-          curl --fail -L -sS $url/root/forgejo/releases/download/v$version/$sources > $sources
-          curl --fail -L -sS $url/root/forgejo/releases/download/v$version/$sources.sha256 > $sources.sha256
-          shasum -a 256 --check $sources.sha256
-
-          docker pull ${{ steps.forgejo.outputs.host-port }}/root/forgejo:$version
-          docker pull ${{ steps.forgejo.outputs.host-port }}/root/forgejo:$version-rootless
+      - name: full logs
+        if: always()
+        run: |
+          sed -e 's/^/[RUNNER LOGS] /' ${{ steps.forgejo.outputs.runner-logs }}
+          docker logs forgejo | sed -e 's/^/[FORGEJO LOGS]/'
+          sleep 5 # hack to avoid mixing outputs in Forgejo v1.21
diff --git a/.forgejo/workflows/build-release.yml b/.forgejo/workflows/build-release.yml
index ebd99ea62..ddf5dfdeb 100644
--- a/.forgejo/workflows/build-release.yml
+++ b/.forgejo/workflows/build-release.yml
@@ -18,7 +18,10 @@ name: Build release
 
 on:
   push:
-    tags: 'v*'
+    tags: 'v[0-9]+.[0-9]+.*'
+    branches:
+      - 'forgejo'
+      - 'v*/forgejo'
 
 jobs:
   release:
@@ -43,17 +46,34 @@ jobs:
           go-version: "1.21"
           check-latest: true
 
-      - name: version from ref_name
-        id: tag-version
+      - name: version from ref
+        id: release-info
+        shell: bash
         run: |
-          version="${{ github.ref_name }}"
-          version=${version##*v}
-          echo "value=$version" >> "$GITHUB_OUTPUT"
+          set -x
+          ref="${{ github.ref }}"
+          if [[ $ref =~ ^refs/heads/ ]] ; then
+            version=${ref#refs/heads/}
+            version=${version%/forgejo}-test
+            override=true
+          fi
+          if [[ $ref =~ ^refs/tags/ ]] ; then
+            version=${ref#refs/tags/}
+            override=false
+          fi
+          if test -z "$version" ; then
+            echo failed to figure out the release version from the reference=$ref
+            exit 1
+          fi
+          version=${version#v}
+          echo "sha=${{ github.sha }}" >> "$GITHUB_OUTPUT"
+          echo "version=$version" >> "$GITHUB_OUTPUT"
+          echo "override=$override" >> "$GITHUB_OUTPUT"
 
       - name: release notes
         id: release-notes
         run: |
-          anchor=${{ steps.tag-version.outputs.value }}
+          anchor=${{ steps.release-info.outputs.version }}
           anchor=${anchor//./-}
           cat >> "$GITHUB_OUTPUT" <<EOF
           value<<ENDVAR
@@ -65,7 +85,7 @@ jobs:
         run: |
           set -x
           apt-get -qq install -y make
-          version=${{ steps.tag-version.outputs.value }}
+          version=${{ steps.release-info.outputs.version }}
           #
           # Make sure all files are owned by the current user.
           # When run as root `npx webpack` will assume the identity
@@ -122,34 +142,38 @@ jobs:
 
       - name: build container & release
         if: ${{ secrets.TOKEN != '' }}
-        uses: https://code.forgejo.org/forgejo/forgejo-build-publish/build@v1
+        uses: https://code.forgejo.org/forgejo/forgejo-build-publish/build@v3
         with:
           forgejo: "${{ env.GITHUB_SERVER_URL }}"
           owner: "${{ env.GITHUB_REPOSITORY_OWNER }}"
           repository: "${{ steps.repository.outputs.value }}"
           doer: "${{ secrets.DOER }}"
-          tag-version: "${{ steps.tag-version.outputs.value }}"
+          release-version: "${{ steps.release-info.outputs.version }}"
+          sha: "${{ steps.release-info.outputs.sha }}"
           token: "${{ secrets.TOKEN }}"
           platforms: linux/amd64,linux/arm64,linux/arm/v6
           release-notes: "${{ steps.release-notes.outputs.value }}"
           binary-name: forgejo
           binary-path: /app/gitea/gitea
-          verbose: ${{ vars.VERBOSE || 'false' }}
+          override: "${{ steps.release-info.outputs.override }}"
+          verbose: ${{ vars.VERBOSE || secrets.VERBOSE || 'false' }}
 
       - name: build rootless container
         if: ${{ secrets.TOKEN != '' }}
-        uses: https://code.forgejo.org/forgejo/forgejo-build-publish/build@v1
+        uses: https://code.forgejo.org/forgejo/forgejo-build-publish/build@v3
         with:
           forgejo: "${{ env.GITHUB_SERVER_URL }}"
           owner: "${{ env.GITHUB_REPOSITORY_OWNER }}"
           repository: "${{ steps.repository.outputs.value }}"
           doer: "${{ secrets.DOER }}"
-          tag-version: "${{ steps.tag-version.outputs.value }}"
+          release-version: "${{ steps.release-info.outputs.version }}"
+          sha: "${{ steps.release-info.outputs.sha }}"
           token: "${{ secrets.TOKEN }}"
           platforms: linux/amd64,linux/arm64,linux/arm/v6
           suffix: -rootless
           dockerfile: Dockerfile.rootless
-          verbose: ${{ vars.VERBOSE || 'false' }}
+          override: "${{ steps.release-info.outputs.override }}"
+          verbose: ${{ vars.VERBOSE || secrets.VERBOSE || 'false' }}
 
       - name: end-to-end tests
         if: ${{ secrets.TOKEN != '' && vars.ROLE == 'forgejo-integration' }}
diff --git a/.forgejo/workflows/mirror.yml b/.forgejo/workflows/mirror.yml
index 3a4099db5..599c8c01f 100644
--- a/.forgejo/workflows/mirror.yml
+++ b/.forgejo/workflows/mirror.yml
@@ -1,10 +1,8 @@
 name: mirror
 
 on:
-  push:
-    branches:
-      - 'forgejo'
-      - 'v*/forgejo'
+  schedule:
+    - cron: '@daily'
 
 jobs:
   mirror: