diff --git a/.forgejo/actions/build-release/action.yml b/.forgejo/actions/build-release/action.yml
new file mode 100644
index 0000000000..01fdfdedfc
--- /dev/null
+++ b/.forgejo/actions/build-release/action.yml
@@ -0,0 +1,154 @@
+name: 'Build release'
+author: 'Forgejo authors'
+description: |
+  Build release
+
+inputs:
+  forgejo:
+    description: 'URL of the Forgejo instance where the release is uploaded'
+    required: true
+  owner:
+    description: 'User or organization where the release is uploaded, relative to the Forgejo instance'
+    required: true
+  repository:
+    description: 'Repository where the release is uploaded, relative to the owner'
+    required: true
+  doer:
+    description: 'Name of the user authoring the release'
+    required: true
+  tag-version:
+    description: 'Version of the release derived from the tag withint the leading v'
+    required: true
+  suffix:
+    description: 'Suffix to add to the image tag'
+  token:
+    description: 'token'
+    required: true
+  dockerfile:
+    description: 'path to the dockerfile'
+    default: 'Dockerfile'
+  platforms:
+    description: 'Coma separated list of platforms'
+    default: 'linux/amd64,linux/arm64'
+  release-notes:
+    description: 'Full text of the release notes'
+    default: 'Release notes placeholder'
+  binary-name:
+    description: 'Name of the binary'
+  binary-path:
+    description: 'Path of the binary within the container to extract into binary-name'
+  verbose:
+    description: 'Increase the verbosity level'
+    default: 'false'
+
+runs:
+  using: "composite"
+  steps:
+    - run: echo "${{ github.action_path }}" >> $GITHUB_PATH
+      shell: bash
+
+    - name: Install dependencies
+      run: |
+        apt-get install -y -qq xz-utils
+
+    - name: set -x if verbose is required
+      id: verbose
+      run: |
+        if ${{ inputs.verbose }} ; then
+          echo "shell=set -x" >> "$GITHUB_OUTPUT"
+        fi
+
+    - name: Create the insecure and buildx-config variables for the container registry
+      id: registry
+      run: |
+        ${{ steps.verbose.outputs.shell }}
+        url="${{ inputs.forgejo }}"
+        hostport=${url##http*://}
+        hostport=${hostport%%/}
+        echo "host-port=${hostport}" >> "$GITHUB_OUTPUT"
+        if ! [[ $url =~ ^http:// ]] ; then
+           exit 0
+        fi
+        cat >> "$GITHUB_OUTPUT" <<EOF
+        insecure=true
+        buildx-config<<ENDVAR
+        [registry."${hostport}"]
+          http = true
+        ENDVAR
+        EOF
+
+    - name: Allow docker pull/push to forgejo
+      if: ${{ steps.registry.outputs.insecure }}
+      run: |-
+        mkdir -p /etc/docker
+        cat > /etc/docker/daemon.json <<EOF
+          {
+            "insecure-registries" : ["${{ steps.registry.outputs.host-port }}"],
+            "bip": "172.26.0.1/16"
+          }
+        EOF
+
+    - name: Install docker
+      run: |
+        echo deb http://deb.debian.org/debian bullseye-backports main | tee /etc/apt/sources.list.d/backports.list && apt-get -qq update
+        DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -qq -y -t bullseye-backports docker.io
+
+    - uses: https://github.com/docker/setup-buildx-action@v2
+      with:
+        config-inline: |
+         ${{ steps.registry.outputs.buildx-config }}
+
+    - name: Login to the container registry
+      run: |
+        BASE64_AUTH=`echo -n "${{ inputs.doer }}:${{ inputs.token }}" | base64 -w0`
+        mkdir -p ~/.docker
+        echo "{\"auths\": {\"$CI_REGISTRY\": {\"auth\": \"$BASE64_AUTH\"}}}" > ~/.docker/config.json
+      env:
+        CI_REGISTRY: "${{ steps.registry.outputs.host-port }}"
+
+    - name: Build the container image for each architecture
+      uses: https://github.com/docker/build-push-action@v4
+      # workaround until https://github.com/docker/build-push-action/commit/d8823bfaed2a82c6f5d4799a2f8e86173c461aba is in @v4 or @v5 is released
+      env:
+        ACTIONS_RUNTIME_TOKEN: ''
+      with:
+        context: .
+        push: true
+        file: ${{ inputs.dockerfile }}
+        platforms: ${{ inputs.platforms }}
+        tags: ${{ steps.registry.outputs.host-port }}/${{ inputs.owner }}/${{ inputs.repository }}:${{ inputs.tag-version }}${{ inputs.suffix }}
+
+    - name: Extract the binary from the container images into the release directory
+      if: inputs.binary-name != ''
+      run: |
+        ${{ steps.verbose.outputs.shell }}
+        mkdir -p release
+        cd release
+        for platform in $(echo ${{ inputs.platforms }} | tr ',' ' '); do
+          arch=$(echo $platform | sed -e 's|linux/||g' -e 's|arm/v6|arm-6|g')
+          docker create --platform $platform --name forgejo-$arch ${{ steps.registry.outputs.host-port }}/${{ inputs.owner }}/${{ inputs.repository }}:${{ inputs.tag-version }}${{ inputs.suffix }}
+          binary="${{ inputs.binary-name }}-${{ inputs.tag-version }}-linux"
+          docker cp forgejo-$arch:${{ inputs.binary-path }} $binary-$arch
+          chmod +x $binary-$arch
+          # the displayed version has a + instead of the first -, deal with it
+          pattern=$(echo "${{ inputs.tag-version }}" | tr - .)
+          if ! ./$binary-$arch --version | grep "$pattern" ; then
+            echo "ERROR: expected version pattern $pattern not found in the output of $binary-$arch --version"
+            ./$binary-$arch --version
+            exit 1
+          fi
+          xz --keep -9 $binary-$arch
+          shasum -a 256 $binary-$arch > $binary-$arch.sha256
+          shasum -a 256 $binary-$arch.xz > $binary-$arch.xz.sha256
+          docker rm forgejo-$arch
+        done
+
+    - name: publish release
+      if: inputs.binary-name != ''
+      uses: https://code.forgejo.org/actions/forgejo-release@v1
+      with:
+        direction: upload
+        release-dir: release
+        release-notes: "${{ inputs.release-notes }}"
+        token: ${{ inputs.token }}
+        verbose: ${{ steps.verbose.outputs.value }}
diff --git a/.forgejo/actions/publish-release/action.yml b/.forgejo/actions/publish-release/action.yml
new file mode 100644
index 0000000000..42b6097ee0
--- /dev/null
+++ b/.forgejo/actions/publish-release/action.yml
@@ -0,0 +1,110 @@
+name: 'Publish release'
+author: 'Forgejo authors'
+description: |
+  Publish release
+
+inputs:
+  forgejo:
+    description: 'URL of the Forgejo instance where the release is uploaded (e.g. https://codeberg.org)'
+    required: true
+  from-owner:
+    description: 'the owner from which a release is to be copied (e.g forgejo-integration)'
+    required: true
+  to-owner:
+    description: 'the owner to which a release is to be copied (e.g. forgejo-experimental). It has be an organization in which doer has the required permissions. Or be the same as the doer'
+    required: true
+  repo:
+    description: 'the repository from which a release is to be copied relative to from-owner and to-owner'
+    default: 'forgejo'
+  ref-name:
+    description: 'ref_name of the tag of the release to be copied (e.g. github.ref_name)'
+    required: true
+  doer:
+    description: 'Name of the user authoring the release (e.g. release-team). The user must be authorized to create packages in to-owner and releases in to-owner/repo'
+    required: true
+  token:
+    description: 'application token created on forgejo by the doer, with a scope allowing it to create packages in to-owner and releases in to-owner/repo'
+    required: true
+  gpg-private-key:
+    description: 'GPG Private Key to sign the release artifacts'
+  gpg-passphrase:
+    description: 'Passphrase of the GPG Private Key'
+  verbose:
+    description: 'Increase the verbosity level'
+    default: 'false'
+
+runs:
+  using: "composite"
+  steps:
+    - id: hostport
+      run: |
+         url="${{ inputs.forgejo }}"
+         hostport=${url##http*://}
+         hostport=${hostport%%/}
+         echo "value=$hostport" >> "$GITHUB_OUTPUT"    
+
+    - id: tag-version
+      run: |
+        version="${{ inputs.ref-name }}"
+        version=${version##*v}
+        echo "value=$version" >> "$GITHUB_OUTPUT"
+
+    - name: Create the release notes
+      id: release-notes
+      run: |
+          anchor=${{ steps.tag-version.outputs.value }}
+          anchor=${anchor//./-}
+          cat >> "$GITHUB_OUTPUT" <<EOF
+          value<<ENDVAR
+          See https://codeberg.org/forgejo/forgejo/src/branch/forgejo/RELEASE-NOTES.md#$anchor
+          ENDVAR
+          EOF
+
+    - name: apt-get install docker.io
+      run: |
+        DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -qq -y docker.io
+
+    - name: download release
+      uses: https://code.forgejo.org/actions/forgejo-release@v1
+      with:
+        url: ${{ inputs.forgejo }}
+        repo: ${{ inputs.from-owner }}/${{ inputs.repo }}
+        direction: download
+        release-dir: release
+        download-retry: 60
+        token: ${{ inputs.token }}
+        verbose: ${{ inputs.verbose }}
+
+    - name: upload release
+      uses: https://code.forgejo.org/actions/forgejo-release@v1
+      with:
+        url: ${{ inputs.forgejo }}
+        repo: ${{ inputs.to-owner }}/${{ inputs.repo }}
+        direction: upload
+        release-dir: release
+        release-notes: ${{ steps.release-notes.outputs.value }}
+        token: ${{ inputs.token }}
+        gpg-private-key: ${{ inputs.gpg-private-key }}
+        gpg-passphrase: ${{ inputs.gpg-passphrase }}
+        verbose: ${{ inputs.verbose }}
+
+    - name: login to the registry
+      uses: https://github.com/docker/login-action@v2
+      with:
+          registry: ${{ steps.hostport.outputs.value }}
+          username: ${{ inputs.doer }}
+          password: ${{ inputs.token }}
+
+    - uses: https://code.forgejo.org/forgejo/forgejo-container-image@v1
+      env:
+        VERIFY: 'false'
+      with:
+        url: https://${{ steps.hostport.outputs.value }}
+        destination-owner: ${{ inputs.to-owner }}
+        owner: ${{ inputs.from-owner }}
+        suffixes: '-rootless'
+        project: ${{ inputs.repo }}
+        tag: ${{ steps.tag-version.outputs.value }}
+        doer: ${{ inputs.doer }}
+        token: ${{ inputs.token }}
+        verbose: ${{ inputs.verbose }}
diff --git a/.forgejo/testdata/build-release/Dockerfile b/.forgejo/testdata/build-release/Dockerfile
new file mode 100644
index 0000000000..4b6933845c
--- /dev/null
+++ b/.forgejo/testdata/build-release/Dockerfile
@@ -0,0 +1,3 @@
+FROM public.ecr.aws/docker/library/alpine:3.18
+RUN mkdir -p /app/gitea
+RUN ( echo '#!/bin/sh' ; echo "echo forgejo v1.2.3" ) > /app/gitea/gitea ; chmod +x /app/gitea/gitea
diff --git a/.forgejo/testdata/build-release/Makefile b/.forgejo/testdata/build-release/Makefile
new file mode 100644
index 0000000000..406acd06d2
--- /dev/null
+++ b/.forgejo/testdata/build-release/Makefile
@@ -0,0 +1,5 @@
+VERSION ?= $(shell cat VERSION 2>/dev/null)
+sources-tarbal:
+	mkdir -p dist/release
+	echo $(VERSION) > VERSION
+	sources=forgejo-src-$(VERSION).tar.gz ; tar --transform 's|^./|forgejo-src-$(VERSION)/|' -czf dist/release/forgejo-src-$(VERSION).tar.gz . ; cd dist/release ; shasum -a 256 $$sources > $$sources.sha256
diff --git a/.forgejo/testdata/build-release/modules/public/bindata.go b/.forgejo/testdata/build-release/modules/public/bindata.go
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/.forgejo/testdata/build-release/public/assets/css/placeholder b/.forgejo/testdata/build-release/public/assets/css/placeholder
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/.forgejo/testdata/build-release/public/assets/fonts/placeholder b/.forgejo/testdata/build-release/public/assets/fonts/placeholder
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/.forgejo/testdata/build-release/public/assets/js/placeholder b/.forgejo/testdata/build-release/public/assets/js/placeholder
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/.forgejo/workflows/build-release-integration.yml b/.forgejo/workflows/build-release-integration.yml
new file mode 100644
index 0000000000..2920729d33
--- /dev/null
+++ b/.forgejo/workflows/build-release-integration.yml
@@ -0,0 +1,99 @@
+name: Integration tests for the release process
+
+on:
+  push:
+    paths:
+      - Makefile
+      - Dockerfile
+      - Dockerfile.rootless
+      - docker/**
+      - .forgejo/actions/build-release/action.yml
+      - .forgejo/workflows/build-release.yml
+      - .forgejo/workflows/build-release-integration.yml
+
+jobs:
+  release-simulation:
+    runs-on: self-hosted
+    if: secrets.ROLE != 'forgejo-integration' && secrets.ROLE != 'forgejo-experimental' && secrets.ROLE != 'forgejo-release'
+    steps:
+      - uses: actions/checkout@v3
+
+      - id: forgejo
+        uses: https://code.forgejo.org/actions/setup-forgejo@v1
+        with:
+          user: root
+          password: admin1234
+          image-version: 1.19
+          lxc-ip-prefix: 10.0.9
+
+      - name: publish the forgejo release
+        run: |
+          set -x
+
+          version=1.2.3
+          cat > /etc/docker/daemon.json <<EOF
+            {
+              "insecure-registries" : ["${{ steps.forgejo.outputs.host-port }}"]
+            }
+          EOF
+          systemctl restart docker
+
+          apt-get install -qq -y xz-utils
+
+          dir=$(mktemp -d)
+          trap "rm -fr $dir" EXIT
+
+          url=http://root:admin1234@${{ steps.forgejo.outputs.host-port }}
+          export FORGEJO_RUNNER_LOGS="${{ steps.forgejo.outputs.runner-logs }}"
+
+          #
+          # Create a new project with a fake forgejo and the release workflow only
+          #
+          cp -a .forgejo/testdata/build-release/* $dir
+          mkdir -p $dir/.forgejo/workflows
+          cp .forgejo/workflows/build-release.yml $dir/.forgejo/workflows
+          cp -a .forgejo/actions $dir/.forgejo/actions
+          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)
+
+          #
+          # Push a tag to trigger the release workflow and wait for it to complete
+          #
+          forgejo-curl.sh api_json --data-raw '{"tag_name": "v'$version'", "target": "'$sha'"}' $url/api/v1/repos/root/forgejo/tags
+          LOOPS=180 forgejo-test-helper.sh wait_success "$url" root/forgejo $sha
+
+          #
+          # uncomment to see the logs even when everything is reported to be working ok
+          #
+          #cat $FORGEJO_RUNNER_LOGS
+
+          #
+          # 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
diff --git a/.forgejo/workflows/build-release.yml b/.forgejo/workflows/build-release.yml
new file mode 100644
index 0000000000..e080504261
--- /dev/null
+++ b/.forgejo/workflows/build-release.yml
@@ -0,0 +1,190 @@
+name: Build release
+
+on:
+  push:
+    tags: 'v*'
+
+jobs:
+  release:
+    runs-on: self-hosted
+    # root is used for testing, allow it
+    if: secrets.ROLE == 'forgejo-integration' || github.repository_owner == 'root'
+    steps:
+      - uses: actions/checkout@v3
+
+      - name: Increase the verbosity when there are no secrets
+        id: verbose
+        run: |
+          if test -z "${{ secrets.TOKEN }}"; then
+            value=true
+          else
+            value=false
+          fi
+          echo "value=$value" >> "$GITHUB_OUTPUT"
+
+      - name: Sanitize the name of the repository
+        id: repository
+        run: |
+          set -x # comment out
+          repository="${{ github.repository }}"
+          echo "value=${repository##*/}" >> "$GITHUB_OUTPUT"
+
+      - name: When in a test environment, create a token
+        id: token
+        if: ${{ secrets.TOKEN == '' }}
+        run: |
+          apt-get -qq install -y jq
+          url="${{ env.GITHUB_SERVER_URL }}"
+          hostport=${url##http*://}
+          hostport=${hostport%%/}
+          doer=root
+          api=http://$doer:admin1234@$hostport/api/v1/users/$doer/tokens
+          curl -sS -X DELETE $api/release
+          token=$(curl -sS -X POST -H 'Content-Type: application/json' --data-raw '{"name": "release", "scopes": ["all"]}' $api | jq --raw-output .sha1)
+          echo "value=${token}" >> "$GITHUB_OUTPUT"
+
+      - uses: https://code.forgejo.org/actions/setup-node@v3
+        with:
+          node-version: 18
+
+      - uses: https://code.forgejo.org/actions/setup-go@v4
+        with:
+          go-version: ">=1.20"
+          check-latest: true
+
+      - name: Create the version from ref_name
+        id: tag-version
+        run: |
+          version="${{ github.ref_name }}"
+          version=${version##*v}
+          echo "value=$version" >> "$GITHUB_OUTPUT"
+
+      - name: Create the release notes
+        id: release-notes
+        run: |
+          cat >> "$GITHUB_OUTPUT" <<EOF
+          value<<ENDVAR
+          See https://codeberg.org/forgejo/forgejo/src/branch/forgejo/RELEASE-NOTES.md#${{ steps.tag-version.outputs.value }}
+          ENDVAR
+          EOF
+
+      - name: Build sources
+        run: |
+          set -x
+          apt-get -qq install -y make
+          version=${{ steps.tag-version.outputs.value }}
+          #
+          # Make sure all files are owned by the current user.
+          # When run as root `npx webpack` will assume the identity
+          # of the owner of the current working directory and may
+          # fail to create files if some sub-directories are not owned
+          # by the same user.
+          #
+          #   Binaries:
+          #   Node: 18.17.0 - /usr/local/node-v18.17.0-linux-x64/bin/node
+          #   npm: 9.6.7 - /usr/local/node-v18.17.0-linux-x64/bin/npm
+          # Packages:
+          #   add-asset-webpack-plugin: 2.0.1 => 2.0.1
+          #   css-loader: 6.8.1 => 6.8.1
+          #   esbuild-loader: 3.0.1 => 3.0.1
+          #   license-checker-webpack-plugin: 0.2.1 => 0.2.1
+          #   monaco-editor-webpack-plugin: 7.0.1 => 7.0.1
+          #   vue-loader: 17.2.2 => 17.2.2
+          #   webpack: 5.87.0 => 5.87.0
+          #   webpack-cli: 5.1.4 => 5.1.4
+          #
+          chown -R $(id -u) .
+          make VERSION=$version TAGS=bindata sources-tarbal
+          mv dist/release release
+
+          (
+            tmp=$(mktemp -d)
+            tar --directory $tmp -zxvf release/*$version*.tar.gz
+            cd $tmp/*
+            #
+            # Verify `make frontend` files are available
+            #
+            test -d public/assets/css
+            test -d public/assets/fonts
+            test -d public/assets/js
+            #
+            # Verify `make generate` files are available
+            #
+            test -f modules/public/bindata.go
+            #
+            # Sanity check to verify that the source tarbal knows the
+            # version and is able to rebuild itself from it.
+            #
+            # When in sources the version is determined with git.
+            # When in the tarbal the version is determined from a VERSION file.
+            #
+            make sources-tarbal
+            tarbal=$(echo dist/release/*$version*.tar.gz)
+            if ! test -f $tarbal ; then
+              echo $tarbal does not exist
+              find dist release
+              exit 1
+            fi
+          )
+
+      - name: build container & release (when TOKEN secret is not set)
+        if: ${{ secrets.TOKEN == '' }}
+        uses: ./.forgejo/actions/build-release
+        with:
+          forgejo: "${{ env.GITHUB_SERVER_URL }}"
+          owner: "${{ env.GITHUB_REPOSITORY_OWNER }}"
+          repository: "${{ steps.repository.outputs.value }}"
+          doer: root
+          tag-version: "${{ steps.tag-version.outputs.value }}"
+          token: ${{ steps.token.outputs.value }}
+          platforms: linux/amd64,linux/arm64,linux/arm/v6
+          release-notes: "${{ steps.release-notes.outputs.value }}"
+          binary-name: forgejo
+          binary-path: /app/gitea/gitea
+          verbose: ${{ steps.verbose.outputs.value }}
+
+      - name: build rootless container (when TOKEN secret is not set)
+        if: ${{ secrets.TOKEN == '' }}
+        uses: ./.forgejo/actions/build-release
+        with:
+          forgejo: "${{ env.GITHUB_SERVER_URL }}"
+          owner: "${{ env.GITHUB_REPOSITORY_OWNER }}"
+          repository: "${{ steps.repository.outputs.value }}"
+          doer: root
+          tag-version: "${{ steps.tag-version.outputs.value }}"
+          token: ${{ steps.token.outputs.value }}
+          platforms: linux/amd64,linux/arm64,linux/arm/v6
+          suffix: -rootless
+          dockerfile: Dockerfile.rootless
+          verbose: ${{ steps.verbose.outputs.value }}
+
+      - name: build container & release (when TOKEN secret is set)
+        if: ${{ secrets.TOKEN != '' }}
+        uses: ./.forgejo/actions/build-release
+        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 }}"
+          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: ${{ steps.verbose.outputs.value }}
+
+      - name: build rootless container (when TOKEN secret is set)
+        if: ${{ secrets.TOKEN != '' }}
+        uses: ./.forgejo/actions/build-release
+        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 }}"
+          token: "${{ secrets.TOKEN }}"
+          platforms: linux/amd64,linux/arm64,linux/arm/v6
+          suffix: -rootless
+          dockerfile: Dockerfile.rootless
+          verbose: ${{ steps.verbose.outputs.value }}
diff --git a/.forgejo/workflows/publish-release.yml b/.forgejo/workflows/publish-release.yml
new file mode 100644
index 0000000000..c76e78cc12
--- /dev/null
+++ b/.forgejo/workflows/publish-release.yml
@@ -0,0 +1,60 @@
+# SPDX-License-Identifier: MIT
+#
+# See also https://forgejo.org/docs/next/developer/RELEASE/#release-process
+#
+# https://codeberg.org/forgejo-experimental/forgejo
+#
+#  Copies a release from codeberg.org/forgejo-integration to codeberg.org/forgejo-experimental
+#
+#  ROLE: forgejo-experimental
+#  FORGEJO: https://codeberg.org
+#  FROM_OWNER: forgejo-integration
+#  TO_OWNER: forgejo-experimental
+#  DOER: forgejo-experimental-ci
+#  TOKEN: <generated from codeberg.org/forgejo-experimental-ci>
+#
+# https://forgejo.octopuce.forgejo.org/forgejo/forgejo
+#
+#  Copies & sign a release from codeberg.org/forgejo-integration to codeberg.org/forgejo
+#
+#  ROLE: forgejo-release
+#  FORGEJO: https://codeberg.org
+#  FROM_OWNER: forgejo-integration
+#  TO_OWNER: forgejo
+#  DOER: release-team
+#  TOKEN: <generated from codeberg.org/release-team>
+#  GPG_PRIVATE_KEY: <XYZ>
+#  GPG_PASSPHRASE: <ABC>
+#
+name: Pubish release
+
+on: 
+  push:
+    tags: 'v*'
+
+jobs:
+  publish:
+    runs-on: self-hosted
+    if: secrets.DOER != '' && secrets.FORGEJO != '' && secrets.TO_OWNER != '' && secrets.FROM_OWNER != '' && secrets.TOKEN != ''
+    steps:
+      - name: install the certificate authority
+        if: secrets.ROLE == 'forgejo-release'
+        run: |
+          apt-get install -qq -y wget
+          wget --no-check-certificate -O /usr/local/share/ca-certificates/enough.crt https://forgejo.octopuce.forgejo.org/forgejo/enough/raw/branch/main/certs/2023-05-13/ca.crt
+          update-ca-certificates --fresh
+
+      - uses: actions/checkout@v3
+
+      - name: copy & sign binaries and container images from one owner to another
+        uses: ./.forgejo/actions/publish-release
+        with:
+          forgejo: ${{ secrets.FORGEJO }}
+          from-owner: ${{ secrets.FROM_OWNER }}
+          to-owner: ${{ secrets.TO_OWNER }}
+          ref-name: ${{ github.ref_name }}
+          doer: ${{ secrets.DOER }}
+          token: ${{ secrets.TOKEN }}
+          gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
+          gpg-passphrase: ${{ secrets.GPG_PASSPHRASE }}
+          verbose: ${{ secrets.VERBOSE }}
diff --git a/Dockerfile b/Dockerfile
index 67c5398917..98d1d707b5 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,6 @@
-# Build stage
-FROM docker.io/library/golang:1.21-alpine3.19 AS build-env
+FROM --platform=$BUILDPLATFORM docker.io/tonistiigi/xx AS xx
+
+FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.21-alpine3.19 as build-env
 
 ARG GOPROXY
 ENV GOPROXY ${GOPROXY:-direct}
@@ -9,24 +10,33 @@ ARG TAGS="sqlite sqlite_unlock_notify"
 ENV TAGS "bindata timetzdata $TAGS"
 ARG CGO_EXTRA_CFLAGS
 
-# Build deps
-RUN apk --no-cache add \
-    build-base \
-    git \
-    nodejs \
-    npm \
-    && rm -rf /var/cache/apk/*
+#
+# Transparently cross compile for the target platform
+#
+COPY --from=xx / /
+ARG TARGETPLATFORM
+RUN apk --no-cache add clang lld
+RUN xx-apk --no-cache add gcc musl-dev
+ENV CGO_ENABLED=1
+RUN xx-go --wrap
+#
+# for go generate and binfmt to find
+# without it the generate phase will fail with
+# #19 25.04 modules/public/public_bindata.go:8: running "go": exit status 1
+# #19 25.39 aarch64-binfmt-P: Could not open '/lib/ld-musl-aarch64.so.1': No such file or directory
+# why exactly is it needed? where is binfmt involved?
+#
+RUN cp /*-alpine-linux-musl*/lib/ld-musl-*.so.1 /lib || true
+
+RUN apk --no-cache add build-base git nodejs npm
 
-# Setup repo
 COPY . ${GOPATH}/src/code.gitea.io/gitea
 WORKDIR ${GOPATH}/src/code.gitea.io/gitea
 
-# Checkout version if set
-RUN if [ -n "${GITEA_VERSION}" ]; then git checkout "${GITEA_VERSION}"; fi \
- && make clean-all build
-
-# Begin env-to-ini build
-RUN go build contrib/environment-to-ini/environment-to-ini.go
+RUN make clean-all
+RUN make frontend
+RUN go build contrib/environment-to-ini/environment-to-ini.go && xx-verify environment-to-ini
+RUN make go-check generate-backend static-executable && xx-verify gitea
 
 # Copy local files
 COPY docker/root /tmp/local
@@ -81,6 +91,7 @@ ENTRYPOINT ["/usr/bin/entrypoint"]
 CMD ["/bin/s6-svscan", "/etc/s6"]
 
 COPY --from=build-env /tmp/local /
+RUN cd /usr/local/bin ; ln -s gitea forgejo
 COPY --from=build-env /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea
 COPY --from=build-env /go/src/code.gitea.io/gitea/environment-to-ini /usr/local/bin/environment-to-ini
 COPY --from=build-env /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete /etc/profile.d/gitea_bash_autocomplete.sh
diff --git a/Dockerfile.rootless b/Dockerfile.rootless
index a41c27b3e2..0c09df61a9 100644
--- a/Dockerfile.rootless
+++ b/Dockerfile.rootless
@@ -1,5 +1,6 @@
-# Build stage
-FROM docker.io/library/golang:1.21-alpine3.19 AS build-env
+FROM --platform=$BUILDPLATFORM docker.io/tonistiigi/xx AS xx
+
+FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.21-alpine3.19 as build-env
 
 ARG GOPROXY
 ENV GOPROXY ${GOPROXY:-direct}
@@ -9,24 +10,33 @@ ARG TAGS="sqlite sqlite_unlock_notify"
 ENV TAGS "bindata timetzdata $TAGS"
 ARG CGO_EXTRA_CFLAGS
 
-#Build deps
-RUN apk --no-cache add \
-    build-base \
-    git \
-    nodejs \
-    npm \
-    && rm -rf /var/cache/apk/*
+#
+# Transparently cross compile for the target platform
+#
+COPY --from=xx / /
+ARG TARGETPLATFORM
+RUN apk --no-cache add clang lld
+RUN xx-apk --no-cache add gcc musl-dev
+ENV CGO_ENABLED=1
+RUN xx-go --wrap
+#
+# for go generate and binfmt to find
+# without it the generate phase will fail with
+# #19 25.04 modules/public/public_bindata.go:8: running "go": exit status 1
+# #19 25.39 aarch64-binfmt-P: Could not open '/lib/ld-musl-aarch64.so.1': No such file or directory
+# why exactly is it needed? where is binfmt involved?
+#
+RUN cp /*-alpine-linux-musl*/lib/ld-musl-*.so.1 /lib || true
+
+RUN apk --no-cache add build-base git nodejs npm
 
-# Setup repo
 COPY . ${GOPATH}/src/code.gitea.io/gitea
 WORKDIR ${GOPATH}/src/code.gitea.io/gitea
 
-# Checkout version if set
-RUN if [ -n "${GITEA_VERSION}" ]; then git checkout "${GITEA_VERSION}"; fi \
- && make clean-all build
-
-# Begin env-to-ini build
-RUN go build contrib/environment-to-ini/environment-to-ini.go
+RUN make clean-all
+RUN make frontend
+RUN go build contrib/environment-to-ini/environment-to-ini.go && xx-verify environment-to-ini
+RUN make go-check generate-backend static-executable && xx-verify gitea
 
 # Copy local files
 COPY docker/rootless /tmp/local
@@ -69,18 +79,19 @@ RUN mkdir -p /var/lib/gitea /etc/gitea
 RUN chown git:git /var/lib/gitea /etc/gitea
 
 COPY --from=build-env /tmp/local /
+RUN cd /usr/local/bin ; ln -s gitea forgejo
 COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea
 COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/environment-to-ini /usr/local/bin/environment-to-ini
 COPY --from=build-env /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete /etc/profile.d/gitea_bash_autocomplete.sh
 
-# git:git
+#git:git
 USER 1000:1000
 ENV GITEA_WORK_DIR /var/lib/gitea
 ENV GITEA_CUSTOM /var/lib/gitea/custom
 ENV GITEA_TEMP /tmp/gitea
 ENV TMPDIR /tmp/gitea
 
-# TODO add to docs the ability to define the ini to load (useful to test and revert a config)
+#TODO add to docs the ability to define the ini to load (useful to test and revert a config)
 ENV GITEA_APP_INI /etc/gitea/app.ini
 ENV HOME "/var/lib/gitea/git"
 VOLUME ["/var/lib/gitea", "/etc/gitea"]
@@ -88,3 +99,4 @@ WORKDIR /var/lib/gitea
 
 ENTRYPOINT ["/usr/bin/dumb-init", "--", "/usr/local/bin/docker-entrypoint.sh"]
 CMD []
+
diff --git a/Makefile b/Makefile
index ce67c0bcf0..48bbc5cbcc 100644
--- a/Makefile
+++ b/Makefile
@@ -83,31 +83,14 @@ endif
 STORED_VERSION_FILE := VERSION
 HUGO_VERSION ?= 0.111.3
 
-GITHUB_REF_TYPE ?= branch
-GITHUB_REF_NAME ?= $(shell git rev-parse --abbrev-ref HEAD)
 
-ifneq ($(GITHUB_REF_TYPE),branch)
-	VERSION ?= $(subst v,,$(GITHUB_REF_NAME))
-	GITEA_VERSION ?= $(VERSION)
+STORED_VERSION=$(shell cat $(STORED_VERSION_FILE) 2>/dev/null)
+ifneq ($(STORED_VERSION),)
+  GITEA_VERSION ?= $(STORED_VERSION)
 else
-	ifneq ($(GITHUB_REF_NAME),)
-		VERSION ?= $(subst release/v,,$(GITHUB_REF_NAME))
-	else
-		VERSION ?= main
-	endif
-
-	STORED_VERSION=$(shell cat $(STORED_VERSION_FILE) 2>/dev/null)
-	ifneq ($(STORED_VERSION),)
-		GITEA_VERSION ?= $(STORED_VERSION)
-	else
-		GITEA_VERSION ?= $(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')
-	endif
-endif
-
-# if version = "main" then update version to "nightly"
-ifeq ($(VERSION),main)
-	VERSION := main-nightly
+  GITEA_VERSION ?= $(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')
 endif
+VERSION = ${GITEA_VERSION}
 
 LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)"
 
@@ -788,9 +771,15 @@ security-check:
 $(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ)
 	CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' -o $@
 
+static-executable: $(GO_SOURCES) $(TAGS_PREREQ)
+	CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -o $(EXECUTABLE)
+
 .PHONY: release
 release: frontend generate release-windows release-linux release-darwin release-freebsd release-copy release-compress vendor release-sources release-docs release-check
 
+# just the sources, with all assets builtin and frontend resources generated
+sources-tarbal: frontend generate vendor release-sources release-check
+
 $(DIST_DIRS):
 	mkdir -p $(DIST_DIRS)