From 2af4c73d12a13ac5e0cd9bb02ff4195e3d88c47c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Lo=C3=AFc=20Dachary?= <loic@dachary.org>
Date: Sat, 5 Nov 2022 00:00:07 +0100
Subject: [PATCH] [CI] implementation: Woodpecker based CI

(cherry picked from commit c2a7aaeee82293793b2740251dc5fd27dfb32ddb)
(cherry picked from commit 6b6007fbced1cecb15aeb09e709b23855c43177e)
(cherry picked from commit 63608a221e551a0e1822901be62f37a557622edd)
(cherry picked from commit 5cfe60baa7a3d250458a8b562976f605357c806b)
---
 .gitignore                                    |   3 +
 .woodpecker/compliance.yml                    |  75 ++++++++++
 .woodpecker/testing-amd64.yml                 | 141 ++++++++++++++++++
 Makefile                                      |   5 +-
 releases/Dockerfile                           |   3 +
 releases/Dockerfile-rootless                  |   3 +
 releases/binaries-pull-push-test.sh           |  69 +++++++++
 releases/binaries-pull-push.sh                |  88 +++++++++++
 .../container-images-pull-verify-push-test.sh |  70 +++++++++
 releases/container-images-pull-verify-push.sh | 122 +++++++++++++++
 releases/woodpecker-build/binaries.yml        | 107 +++++++++++++
 .../woodpecker-build/container-images.yml     |  65 ++++++++
 releases/woodpecker-build/releases-helper.yml |  34 +++++
 releases/woodpecker-publish/binaries.yml      |  36 +++++
 .../woodpecker-publish/container-images.yml   |  27 ++++
 15 files changed, 845 insertions(+), 3 deletions(-)
 create mode 100644 .woodpecker/compliance.yml
 create mode 100644 .woodpecker/testing-amd64.yml
 create mode 100644 releases/Dockerfile
 create mode 100644 releases/Dockerfile-rootless
 create mode 100755 releases/binaries-pull-push-test.sh
 create mode 100755 releases/binaries-pull-push.sh
 create mode 100755 releases/container-images-pull-verify-push-test.sh
 create mode 100755 releases/container-images-pull-verify-push.sh
 create mode 100644 releases/woodpecker-build/binaries.yml
 create mode 100644 releases/woodpecker-build/container-images.yml
 create mode 100644 releases/woodpecker-build/releases-helper.yml
 create mode 100644 releases/woodpecker-publish/binaries.yml
 create mode 100644 releases/woodpecker-publish/container-images.yml

diff --git a/.gitignore b/.gitignore
index 1ce2a87611..6d9eceb30f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
+# Emacs
+*~
+
 # Compiled Object files, Static and Dynamic libs (Shared Objects)
 *.o
 *.a
diff --git a/.woodpecker/compliance.yml b/.woodpecker/compliance.yml
new file mode 100644
index 0000000000..a65882921a
--- /dev/null
+++ b/.woodpecker/compliance.yml
@@ -0,0 +1,75 @@
+platform: linux/amd64
+
+when:
+  event: [ push, pull_request, manual ]
+  branch:
+    exclude: [ soft-fork/*/*, soft-fork/*/*/* ]
+
+variables:
+ - &golang_image 'golang:1.20'
+ - &test_image 'codeberg.org/forgejo/test_env:main'
+ - &goproxy_override ''
+ - &goproxy_setup |-
+      if [ -n "$${GOPROXY_OVERRIDE:-}" ]; then
+        export GOPROXY="$${GOPROXY_OVERRIDE}";
+        echo "Using goproxy from goproxy_override \"$${GOPROXY}\"";
+      elif [ -n "$${GOPROXY_DEFAULT:-}" ]; then
+        export GOPROXY="$${GOPROXY_DEFAULT}";
+        echo "Using goproxy from goproxy_default (secret) not displaying";
+      else
+        export GOPROXY="https://proxy.golang.org,direct";
+        echo "No goproxy overrides or defaults given, using \"$${GOPROXY}\"";
+      fi
+
+workspace:
+  base: /go
+  path: src/codeberg/gitea
+
+pipeline:
+  deps-backend:
+    image: *golang_image
+    pull: true
+    environment:
+      GOPROXY_OVERRIDE: *goproxy_override
+    secrets:
+      - goproxy_default
+    commands:
+    - *goproxy_setup
+    - make deps-backend
+
+  security-check:
+    image: *golang_image
+    group: checks
+    pull: true
+    environment:
+      GOPROXY_OVERRIDE: *goproxy_override
+    secrets:
+      - goproxy_default
+    commands:
+    - *goproxy_setup
+    - make security-check
+
+  lint-backend:
+    image: *test_image
+    pull: true
+    group: checks
+    environment:
+      GOPROXY_OVERRIDE: *goproxy_override
+      TAGS: 'bindata sqlite sqlite_unlock_notify'
+      GOSUMDB: 'sum.golang.org'
+    secrets:
+      - goproxy_default
+    commands:
+    - *goproxy_setup
+    - make lint-backend
+
+  checks-backend:
+    image: *test_image
+    group: checks
+    environment:
+      GOPROXY_OVERRIDE: *goproxy_override
+    secrets:
+      - goproxy_default
+    commands:
+    - *goproxy_setup
+    - make --always-make checks-backend
diff --git a/.woodpecker/testing-amd64.yml b/.woodpecker/testing-amd64.yml
new file mode 100644
index 0000000000..340ee55736
--- /dev/null
+++ b/.woodpecker/testing-amd64.yml
@@ -0,0 +1,141 @@
+platform: linux/amd64
+
+when:
+  event: [ push, pull_request, manual ]
+  branch:
+    exclude: [ soft-fork/*/*, soft-fork/*/*/* ]
+
+depends_on:
+- compliance
+
+variables:
+ - &golang_image 'golang:1.20'
+ - &test_image 'codeberg.org/forgejo/test_env:main'
+ - &mysql_image 'mysql:8'
+ - &pgsql_image 'postgres:10'
+ - &goproxy_override ''
+ - &goproxy_setup |-
+      if [ -n "$${GOPROXY_OVERRIDE:-}" ]; then
+        export GOPROXY="$${GOPROXY_OVERRIDE}";
+        echo "Using goproxy from goproxy_override \"$${GOPROXY}\"";
+      elif [ -n "$${GOPROXY_DEFAULT:-}" ]; then
+        export GOPROXY="$${GOPROXY_DEFAULT}";
+        echo "Using goproxy from goproxy_default (secret) not displaying";
+      else
+        export GOPROXY="https://proxy.golang.org,direct";
+        echo "No goproxy overrides or defaults given, using \"$${GOPROXY}\"";
+      fi
+
+services:
+  mysql8:
+    image: *mysql_image
+    pull: true
+    environment:
+      MYSQL_ALLOW_EMPTY_PASSWORD: yes
+      MYSQL_DATABASE: testgitea
+
+  pgsql:
+    image: *pgsql_image
+    pull: true
+    environment:
+      POSTGRES_DB: test
+      POSTGRES_PASSWORD: postgres
+
+workspace:
+  base: /go
+  path: src/codeberg/gitea
+
+pipeline:
+  git-safe:
+    image: *golang_image
+    pull: true
+    commands:
+    - git config --add safe.directory '*'
+
+  deps-backend:
+    image: *golang_image
+    pull: true
+    environment:
+      GOPROXY_OVERRIDE: *goproxy_override
+    secrets:
+      - goproxy_default
+    commands:
+    - *goproxy_setup
+    - make deps-backend
+
+  tag-pre-condition:
+    image: *golang_image
+    pull: true
+    commands:
+    - git update-ref refs/heads/tag_test ${CI_COMMIT_SHA}
+
+  prepare-test-env:
+    image: *test_image
+    pull: true
+    commands:
+    - ./build/test-env-prepare.sh
+
+  build:
+    image: *test_image
+    environment:
+      GOSUMDB: sum.golang.org
+      TAGS: bindata sqlite sqlite_unlock_notify
+      GOPROXY_OVERRIDE: *goproxy_override
+    secrets:
+      - goproxy_default
+    commands:
+    - *goproxy_setup
+    - su gitea -c './build/test-env-check.sh'
+    - su gitea -c 'make backend'
+
+  unit-test:
+    image: *test_image
+    environment:
+      TAGS: 'bindata sqlite sqlite_unlock_notify'
+      RACE_ENABLED: 'true'
+      GOPROXY_OVERRIDE: *goproxy_override
+    secrets:
+    - github_read_token
+    - goproxy_default
+    commands:
+    - *goproxy_setup
+    - su gitea -c 'make unit-test-coverage test-check'
+
+  test-mysql8:
+    group: integration
+    image: *test_image
+    commands:
+      - *goproxy_setup
+      - su gitea -c 'timeout -s ABRT 50m make test-mysql8-migration test-mysql8'
+    environment:
+      TAGS: 'bindata'
+      RACE_ENABLED: 'true'
+      USE_REPO_TEST_DIR: '1'
+      GOPROXY_OVERRIDE: *goproxy_override
+    secrets:
+      - goproxy_default
+
+  test-pgsql:
+    group: integration
+    image: *test_image
+    commands:
+      - *goproxy_setup
+      - su gitea -c 'timeout -s ABRT 50m make test-pgsql-migration test-pgsql'
+    environment:
+      TAGS: 'bindata'
+      RACE_ENABLED: 'true'
+      USE_REPO_TEST_DIR: '1'
+      GOPROXY_OVERRIDE: *goproxy_override
+    secrets:
+      - goproxy_default
+
+  test-sqlite:
+    group: integration
+    image: *test_image
+    environment:
+    - USE_REPO_TEST_DIR=1
+    - GOPROXY=off
+    - TAGS=bindata gogit sqlite sqlite_unlock_notify
+    - TEST_TAGS=bindata gogit sqlite sqlite_unlock_notify
+    commands:
+    - su gitea -c 'timeout -s ABRT 120m make test-sqlite-migration test-sqlite'
diff --git a/Makefile b/Makefile
index bb74c2e8a1..c16926b0a0 100644
--- a/Makefile
+++ b/Makefile
@@ -287,8 +287,7 @@ misspell-check:
 .PHONY: vet
 vet:
 	@echo "Running go vet..."
-	@GOOS= GOARCH= $(GO) build code.gitea.io/gitea-vet
-	@$(GO) vet -vettool=gitea-vet $(GO_PACKAGES)
+	@$(GO) vet $(GO_PACKAGES)
 
 .PHONY: $(TAGS_EVIDENCE)
 $(TAGS_EVIDENCE):
@@ -752,7 +751,7 @@ $(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ)
 	CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' -o $@
 
 .PHONY: release
-release: frontend generate release-windows release-linux release-darwin release-freebsd release-copy release-compress vendor release-sources release-docs release-check
+release: frontend generate release-linux release-copy release-compress vendor release-sources release-check
 
 $(DIST_DIRS):
 	mkdir -p $(DIST_DIRS)
diff --git a/releases/Dockerfile b/releases/Dockerfile
new file mode 100644
index 0000000000..bef4e4f6de
--- /dev/null
+++ b/releases/Dockerfile
@@ -0,0 +1,3 @@
+FROM alpine:3.17
+
+RUN echo root > state
diff --git a/releases/Dockerfile-rootless b/releases/Dockerfile-rootless
new file mode 100644
index 0000000000..561b67e9a8
--- /dev/null
+++ b/releases/Dockerfile-rootless
@@ -0,0 +1,3 @@
+FROM alpine:3.17
+
+RUN echo rootless > state
diff --git a/releases/binaries-pull-push-test.sh b/releases/binaries-pull-push-test.sh
new file mode 100755
index 0000000000..d7299c8123
--- /dev/null
+++ b/releases/binaries-pull-push-test.sh
@@ -0,0 +1,69 @@
+#!/bin/sh
+
+set -ex
+
+test_teardown() {
+    setup_api
+    api DELETE repos/$PUSH_USER/forgejo/releases/tags/$TAG || true
+    api DELETE repos/$PUSH_USER/forgejo/tags/$TAG || true
+    rm -fr dist/release
+    setup_tea
+    $BIN_DIR/tea login delete $RELEASETEAMUSER || true
+}
+
+test_setup() {
+    mkdir -p $RELEASE_DIR
+    touch $RELEASE_DIR/file-one.txt
+    touch $RELEASE_DIR/file-two.txt
+}
+
+test_ensure_tag() {
+    api DELETE repos/$PUSH_USER/forgejo/tags/$TAG || true
+    #
+    # idempotent
+    #
+    ensure_tag
+    api GET repos/$PUSH_USER/forgejo/tags/$TAG > /tmp/tag1.json
+    ensure_tag
+    api GET repos/$PUSH_USER/forgejo/tags/$TAG > /tmp/tag2.json
+    diff -u /tmp/tag[12].json
+    #
+    # sanity check on the SHA of an existing tag
+    #
+    (
+	CI_COMMIT_SHA=12345
+	! ensure_tag
+    )
+    api DELETE repos/$PUSH_USER/forgejo/tags/$TAG
+}
+
+#
+# Running the test locally instead of within Woodpecker
+#
+# 1. Setup: obtain a token at https://codeberg.org/user/settings/applications
+# 2. Run: RELEASETEAMUSER=<username> RELEASETEAMTOKEn=<apptoken> binaries-pull-push-test.sh test_run
+# 3. Verify: (optional) manual verification at https://codeberg.org/<username>/forgejo/releases
+# 4. Cleanup: RELEASETEAMUSER=<username> RELEASETEAMTOKEn=<apptoken> binaries-pull-push-test.sh test_teardown
+#
+test_run() {
+    test_teardown
+    to_push=/tmp/binaries-releases-to-push
+    pulled=/tmp/binaries-releases-pulled
+    RELEASE_DIR=$to_push
+    test_setup
+    test_ensure_tag
+    echo "================================ TEST BEGIN"
+    push
+    RELEASE_DIR=$pulled
+    pull
+    diff -r $to_push $pulled
+    echo "================================ TEST END"
+}
+
+: ${CI_REPO_OWNER:=dachary}
+: ${PULL_USER=$CI_REPO_OWNER}
+: ${PUSH_USER=$CI_REPO_OWNER}
+: ${CI_COMMIT_TAG:=W17.8.20-1}
+: ${CI_COMMIT_SHA:=$(git rev-parse HEAD)}
+
+. $(dirname $0)/binaries-pull-push.sh
diff --git a/releases/binaries-pull-push.sh b/releases/binaries-pull-push.sh
new file mode 100755
index 0000000000..1ffb5d8567
--- /dev/null
+++ b/releases/binaries-pull-push.sh
@@ -0,0 +1,88 @@
+#!/bin/sh
+
+set -ex
+
+: ${PULL_USER:=forgejo-integration}
+if test "$CI_REPO" = "forgejo/release" ; then
+    : ${PUSH_USER:=forgejo}
+else
+    : ${PUSH_USER:=forgejo-experimental}
+fi
+: ${TAG:=${CI_COMMIT_TAG}}
+: ${DOMAIN:=codeberg.org}
+: ${RELEASE_DIR:=dist/release}
+: ${BIN_DIR:=/tmp}
+: ${TEA_VERSION:=0.9.0}
+
+
+setup_tea() {
+    if ! test -f $BIN_DIR/tea ; then
+	curl -sL https://dl.gitea.io/tea/$TEA_VERSION/tea-$TEA_VERSION-linux-amd64 > $BIN_DIR/tea
+	chmod +x $BIN_DIR/tea
+    fi
+}
+
+ensure_tag() {
+    if api GET repos/$PUSH_USER/forgejo/tags/$TAG > /tmp/tag.json ; then
+	local sha=$(jq --raw-output .commit.sha < /tmp/tag.json)
+	if test "$sha" != "$CI_COMMIT_SHA" ; then
+	    cat /tmp/tag.json
+	    echo "the tag SHA in the $PUSH_USER repository does not match the tag SHA that triggered the build: $CI_COMMIT_SHA"
+	    false
+	fi
+    else
+	api POST repos/$PUSH_USER/forgejo/tags --data-raw '{"tag_name": "'$CI_COMMIT_TAG'", "target": "'$CI_COMMIT_SHA'"}'
+    fi
+}
+
+upload() {
+    ASSETS=$(ls $RELEASE_DIR/* | sed -e 's/^/-a /')
+    echo "${CI_COMMIT_TAG}" | grep -qi '\-rc' && export RELEASETYPE="--prerelease" && echo "Uploading as Pre-Release"
+    echo "${CI_COMMIT_TAG}" | grep -qi '\-test' && export RELEASETYPE="--draft" && echo "Uploading as Draft"
+    test ${RELEASETYPE+false} || echo "Uploading as Stable"
+    ensure_tag
+    anchor=$(echo $CI_COMMIT_TAG | sed -e 's/^v//' -e 's/[^a-zA-Z0-9]/-/g')
+    $BIN_DIR/tea release create $ASSETS --repo $PUSH_USER/forgejo --note "See https://codeberg.org/forgejo/forgejo/src/branch/forgejo/RELEASE-NOTES.md#${anchor}" --tag $CI_COMMIT_TAG --title $CI_COMMIT_TAG ${RELEASETYPE}
+}
+
+push() {
+    setup_api
+    setup_tea
+    GITEA_SERVER_TOKEN=$RELEASETEAMTOKEN $BIN_DIR/tea login add --name $RELEASETEAMUSER --url $DOMAIN
+    upload
+}
+
+setup_api() {
+    if ! which jq || ! which curl ; then
+	apk --update --no-cache add jq curl
+    fi
+}
+
+api() {
+    method=$1
+    shift
+    path=$1
+    shift
+
+    curl --fail -X $method -sS -H "Content-Type: application/json" -H "Authorization: token $RELEASETEAMTOKEN" "$@" https://$DOMAIN/api/v1/$path
+}
+
+pull() {
+    setup_api
+    (
+	mkdir -p $RELEASE_DIR
+	cd $RELEASE_DIR
+	api GET repos/$PULL_USER/forgejo/releases/tags/$TAG > /tmp/assets.json
+	jq --raw-output '.assets[] | "\(.name) \(.browser_download_url)"' < /tmp/assets.json | while read name url ; do
+	    wget --quiet -O $name $url
+	done
+    )
+}
+
+
+missing() {
+    echo need pull or push argument got nothing
+    exit 1
+}
+
+${@:-missing}
diff --git a/releases/container-images-pull-verify-push-test.sh b/releases/container-images-pull-verify-push-test.sh
new file mode 100755
index 0000000000..96e60b33f9
--- /dev/null
+++ b/releases/container-images-pull-verify-push-test.sh
@@ -0,0 +1,70 @@
+#!/bin/sh
+
+set -ex
+
+image_delete() {
+    curl -sS -H @$TOKEN_HEADER -X DELETE https://$DOMAIN/v2/$1/forgejo/manifests/$2
+}
+
+#
+# Create the same set of images that buildx would
+#
+test_setup() {
+    dir=$(dirname $0)
+
+    for suffix in '' '-rootless' ; do
+	(
+	    cd $dir
+	    manifests=""
+	    for arch in $ARCHS ; do
+		image=$(arch_image_name $PULL_USER $arch $suffix)
+		docker build -f Dockerfile$suffix --platform linux/$arch -t $image .
+		docker push $image
+		images="$images $image"
+	    done
+	    manifest=$(image_name $PULL_USER $suffix)
+	    docker manifest rm $manifest || true
+	    docker manifest create $manifest $images
+	    image_put $PULL_USER $(image_tag $suffix) $manifest
+	)
+    done
+}
+
+test_teardown() {
+    authenticate
+    for suffix in '' '-rootless' ; do
+	image_delete $PULL_USER $(image_tag $suffix)
+	image_delete $PUSH_USER $(image_tag $suffix)
+	image_delete $PUSH_USER $(short_image_tag $suffix)
+	for arch in $ARCHS ; do
+	    image_delete $PULL_USER $(arch_image_tag $arch $suffix)
+	    image_delete $PUSH_USER $(arch_image_tag $arch $suffix)
+	done
+    done
+}
+
+#
+# Running the test locally instead of within Woodpecker
+#
+# 1. Setup: obtain a token at https://codeberg.org/user/settings/applications
+# 2. Run: RELEASETEAMUSER=<username> RELEASETEAMTOKEn=<apptoken> container-images-pull-verify-push-test.sh test_run
+# 3. Verify: (optional) manual verification at https://codeberg.org/<username>/-/packages/container/forgejo/versions
+# 4. Cleanup: RELEASETEAMUSER=<username> RELEASETEAMTOKEn=<apptoken> container-images-pull-verify-push-test.sh test_teardown
+#
+test_run() {
+    boot
+    test_teardown
+    test_setup
+    VERIFY_STRING=something
+    VERIFY_COMMAND="echo $VERIFY_STRING"
+    echo "================================ TEST BEGIN"
+    main
+    echo "================================ TEST END"
+}
+
+: ${CI_REPO_OWNER:=dachary}
+: ${PULL_USER:=$CI_REPO_OWNER}
+: ${PUSH_USER:=$CI_REPO_OWNER}
+: ${CI_COMMIT_TAG:=v17.1.42-2}
+
+. $(dirname $0)/container-images-pull-verify-push.sh
diff --git a/releases/container-images-pull-verify-push.sh b/releases/container-images-pull-verify-push.sh
new file mode 100755
index 0000000000..9b2ccc203c
--- /dev/null
+++ b/releases/container-images-pull-verify-push.sh
@@ -0,0 +1,122 @@
+#!/bin/sh
+
+set -ex
+
+: ${DOCKER_HOST:=unix:///var/run/docker.sock}
+: ${ARCHS:=amd64 arm64}
+: ${PULL_USER:=forgejo-integration}
+if test "$CI_REPO" = "forgejo/release" ; then
+    : ${PUSH_USER:=forgejo}
+else
+    : ${PUSH_USER:=forgejo-experimental}
+fi
+: ${INTEGRATION_IMAGE:=codeberg.org/$PULL_USER/forgejo}
+: ${TAG:=${CI_COMMIT_TAG##v}}
+: ${SHORT_TAG=${TAG%.*-*}}
+: ${DOMAIN:=codeberg.org}
+: ${TOKEN_HEADER:=/tmp/token$$}
+trap "rm -f ${TOKEN_HEADER}" EXIT
+
+: ${VERIFY:=true}
+VERIFY_COMMAND='gitea --version'
+VERIFY_STRING='built with'
+
+publish() {
+    for suffix in '' '-rootless' ; do
+	images=""
+	for arch in $ARCHS ; do
+	    #
+	    # Get the image from the integration user
+	    #
+	    image=$(image_name $PULL_USER $suffix)
+	    docker pull --platform linux/$arch $image
+	    #
+	    # Verify it is usable
+	    #
+	    if $VERIFY ; then
+		docker run --platform linux/$arch --rm $image $VERIFY_COMMAND | grep "$VERIFY_STRING"
+	    fi
+	    #
+	    # Push the image with a tag reflecting the architecture to the repo owner
+	    #
+	    arch_image=$(arch_image_name $PUSH_USER $arch $suffix)
+	    docker tag $image $arch_image
+	    docker push $arch_image
+	    images="$images $arch_image"
+	done
+
+	#
+	# Push a manifest with all the architectures to the repo owner
+	#
+	manifest=$(image_name $PUSH_USER $suffix)
+	docker manifest rm $manifest || true
+	docker manifest create $manifest $images
+	image_put $PUSH_USER $(image_tag $suffix) $manifest
+	image_put $PUSH_USER $(short_image_tag $suffix) $manifest
+	#
+	# Sanity check to ensure the manifest that are published can actualy
+	# be used.
+	#
+	for arch in $ARCHS ; do
+	    docker pull --platform linux/$arch $(image_name $PUSH_USER $suffix)
+	    docker pull --platform linux/$arch $(short_image_name $PUSH_USER $suffix)
+	done
+    done
+}
+
+boot() {
+    if docker version ; then
+	return
+    fi
+    apk --update --no-cache add coredns jq curl
+    ( echo ".:53 {" ; echo "  forward . /etc/resolv.conf"; echo "}" ) > /etc/coredns/Corefile
+    coredns -conf /etc/coredns/Corefile &
+    /usr/local/bin/dockerd --data-root /var/lib/docker --host=$DOCKER_HOST --dns 172.17.0.3 &
+    for i in $(seq 60) ; do
+	docker version && break
+	sleep 1
+    done
+    docker version || exit 1
+}
+
+authenticate() {
+    echo "$RELEASETEAMTOKEN" | docker login --password-stdin --username "$RELEASETEAMUSER" $DOMAIN
+    curl -u$RELEASETEAMUSER:$RELEASETEAMTOKEN -sS https://$DOMAIN/v2/token | jq --raw-output '"Authorization: token \(.token)"' > $TOKEN_HEADER
+}
+
+image_put() {
+    docker manifest inspect $3 > /tmp/manifest.json
+    curl -sS -H @$TOKEN_HEADER -X PUT --data-binary @/tmp/manifest.json https://$DOMAIN/v2/$1/forgejo/manifests/$2
+}
+
+main() {
+    boot
+    authenticate
+    publish
+}
+
+image_name() {
+    echo $DOMAIN/$1/forgejo:$(image_tag $2)
+}
+
+image_tag() {
+    echo $TAG$1
+}
+
+short_image_name() {
+    echo $DOMAIN/$1/forgejo:$(short_image_tag $2)
+}
+
+short_image_tag() {
+    echo $SHORT_TAG$1
+}
+
+arch_image_name() {
+    echo $DOMAIN/$1/forgejo:$(arch_image_tag $2 $3)
+}
+
+arch_image_tag() {
+    echo $TAG-$1$2
+}
+
+${@:-main}
diff --git a/releases/woodpecker-build/binaries.yml b/releases/woodpecker-build/binaries.yml
new file mode 100644
index 0000000000..f140725468
--- /dev/null
+++ b/releases/woodpecker-build/binaries.yml
@@ -0,0 +1,107 @@
+platform: linux/amd64
+
+when:
+  event: tag
+  tag: v*
+
+variables:
+ - &node_image 'node:18'
+ - &golang_image 'golang:1.20'
+ - &alpine_image 'alpine:3.17'
+ - &gpg_sign_image 'plugins/gpgsign:1'
+ - &xgo_image 'techknowlogick/xgo:go-1.19.x'
+ - &gpg_sign_image 'plugins/gpgsign:1'
+ - &goproxy_override ''
+ - &goproxy_setup |-
+      if [ -n "$${GOPROXY_OVERRIDE:-}" ]; then
+        export GOPROXY="$${GOPROXY_OVERRIDE}";
+        echo "Using goproxy from goproxy_override \"$${GOPROXY}\"";
+      elif [ -n "$${GOPROXY_DEFAULT:-}" ]; then
+        export GOPROXY="$${GOPROXY_DEFAULT}";
+        echo "Using goproxy from goproxy_default (secret) not displaying";
+      else
+        export GOPROXY="https://proxy.golang.org,direct";
+        echo "No goproxy overrides or defaults given, using \"$${GOPROXY}\"";
+      fi
+
+workspace:
+  base: /source
+  path: /
+
+pipeline:
+  fetch-tags:
+    image: *golang_image
+    pull: true
+    group: deps
+    commands:
+     - git config --add safe.directory '*'
+     - git fetch --tags --force
+
+  deps-frontend:
+    image: *node_image
+    pull: true
+    group: deps
+    commands:
+      - make deps-frontend
+
+  deps-backend:
+    image: *golang_image
+    pull: true
+    group: deps
+    environment:
+      GOPROXY_OVERRIDE: *goproxy_override
+    secrets:
+      - goproxy_default
+    commands:
+    - *goproxy_setup
+    - make deps-backend
+
+  static:
+    image: *xgo_image
+    pull: true
+    commands:
+      - *goproxy_setup
+      - curl -sL https://deb.nodesource.com/setup_16.x | bash - && apt-get -qqy install nodejs
+      - export PATH=$PATH:$GOPATH/bin
+      - make CI=true LINUX_ARCHS=linux/amd64,linux/arm64,linux/arm-6 release
+    environment:
+      TAGS: 'bindata sqlite sqlite_unlock_notify'
+      DEBIAN_FRONTEND: 'noninteractive'
+      GOPROXY_OVERRIDE: *goproxy_override
+    secrets:
+      - goproxy_default
+
+  #
+  # See https://codeberg.org/forgejo/forgejo/issues/230 for a discussion on this
+  # compilation stage. The goal is just to verify the build does not break, not that
+  # the binary produced actually works.
+  #
+  freebsd:
+    image: *xgo_image
+    group: build
+    commands:
+      - *goproxy_setup
+      - export PATH=$PATH:$GOPATH/bin
+      - make CI=false release-freebsd
+    environment:
+      TAGS: 'bindata sqlite sqlite_unlock_notify'
+      GOPROXY_OVERRIDE: *goproxy_override
+    secrets:
+      - goproxy_default
+
+  verifyruns:
+    image: *golang_image
+    commands:
+      - ./dist/release/forgejo-*-amd64 --version | grep 'built with'
+      - apt-get update
+      - apt-get install -y qemu-user-static
+      - /usr/bin/qemu-aarch64-static ./dist/release/forgejo-*-arm64 --version | grep 'built with'
+      - /usr/bin/qemu-arm-static ./dist/release/forgejo-*-arm-6 --version | grep 'built with'
+
+  push-integration:
+    image: *alpine_image
+    commands:
+      - PUSH_USER=$CI_REPO_OWNER releases/binaries-pull-push.sh push
+    secrets:
+      - releaseteamtoken
+      - releaseteamuser
diff --git a/releases/woodpecker-build/container-images.yml b/releases/woodpecker-build/container-images.yml
new file mode 100644
index 0000000000..ebcd7cdc1c
--- /dev/null
+++ b/releases/woodpecker-build/container-images.yml
@@ -0,0 +1,65 @@
+platform: linux/amd64
+
+when:
+  event: tag
+  tag: v*
+
+variables:
+ - &golang_image 'golang:1.20'
+ - &dind_image 'docker:20.10-dind'
+ - &buildx_image 'woodpeckerci/plugin-docker-buildx:2.0.0'
+ - &integration_image 'codeberg.org/forgejo-integration/forgejo'
+ - &dockerfile_root 'Dockerfile'
+# for testing purposes
+# - &dockerfile_root 'releases/Dockerfile'
+ - &dockerfile_rootless 'Dockerfile.rootless'
+# for testing purposes
+# - &dockerfile_rootless 'releases/Dockerfile-rootless'
+ - &verify 'true'
+# for testing purposes
+# - &verify 'false'
+ - &archs 'amd64 arm64'
+
+pipeline:
+  fetch-tags:
+    image: *golang_image
+    pull: true
+    commands:
+     - git config --add safe.directory '*'
+     - git fetch --tags --force
+
+  build-root:
+    image: *buildx_image
+    group: integration
+    pull: true
+    settings:
+      platforms: linux/amd64,linux/arm64
+      dockerfile: *dockerfile_root
+      registry:
+        from_secret: domain
+      tag: ${CI_COMMIT_TAG##v}
+      repo: *integration_image
+      build_args:
+        - GOPROXY=https://proxy.golang.org
+      password:
+        from_secret: releaseteamtoken
+      username:
+        from_secret: releaseteamuser
+
+  build-rootless:
+    image: *buildx_image
+    group: integration
+    pull: true
+    settings:
+      platforms: linux/amd64,linux/arm64
+      dockerfile: *dockerfile_rootless
+      registry:
+        from_secret: domain
+      tag: ${CI_COMMIT_TAG##v}-rootless
+      repo: *integration_image
+      build_args:
+        - GOPROXY=https://proxy.golang.org
+      password:
+        from_secret: releaseteamtoken
+      username:
+        from_secret: releaseteamuser
diff --git a/releases/woodpecker-build/releases-helper.yml b/releases/woodpecker-build/releases-helper.yml
new file mode 100644
index 0000000000..e700a0f23c
--- /dev/null
+++ b/releases/woodpecker-build/releases-helper.yml
@@ -0,0 +1,34 @@
+platform: linux/amd64
+
+when:
+  event: push
+
+variables:
+ - &dind_image 'docker:20.10-dind'
+ - &alpine_image 'alpine:3.17'
+
+pipeline:
+  container-images-pull-verify-push:
+    image: *dind_image
+    group: integration
+    commands:
+# arm64 would require qemu-user-static which is not available on alpline
+# the test coverage does not change much and running the tests test locally
+# is possible if there is a doubt
+      - ARCHS=amd64 ./releases/container-images-pull-verify-push-test.sh test_run
+      - ./releases/container-images-pull-verify-push-test.sh test_teardown
+    secrets:
+      - releaseteamuser
+      - releaseteamtoken
+      - domain
+
+  binaries-pull-push:
+    image: *alpine_image
+    group: integration
+    commands:
+      - ./releases/binaries-pull-push-test.sh test_run
+      - ./releases/binaries-pull-push-test.sh test_teardown
+    secrets:
+      - releaseteamuser
+      - releaseteamtoken
+      - domain
diff --git a/releases/woodpecker-publish/binaries.yml b/releases/woodpecker-publish/binaries.yml
new file mode 100644
index 0000000000..c269903b85
--- /dev/null
+++ b/releases/woodpecker-publish/binaries.yml
@@ -0,0 +1,36 @@
+platform: linux/amd64
+
+when:
+  event: tag
+
+variables:
+ - &dind_image 'docker:20.10-dind'
+ - &gpg_sign_image 'plugins/gpgsign:1'
+
+pipeline:
+
+  pull:
+    image: *dind_image
+    commands:
+      - ./releases/binaries-pull-push.sh pull
+
+  gpg-sign:
+    image: *gpg_sign_image
+    pull: true
+    settings:
+      detach_sign: true
+      excludes:
+        - "dist/release/*.sha256"
+      files:
+        - "dist/release/*"
+      key:
+        from_secret: releaseteamgpg
+
+  push:
+    image: *dind_image
+    commands:
+      - ./releases/binaries-pull-push.sh push
+    secrets:
+      - releaseteamtoken
+      - releaseteamuser
+      - domain
diff --git a/releases/woodpecker-publish/container-images.yml b/releases/woodpecker-publish/container-images.yml
new file mode 100644
index 0000000000..77b1ac2933
--- /dev/null
+++ b/releases/woodpecker-publish/container-images.yml
@@ -0,0 +1,27 @@
+platform: linux/amd64
+
+when:
+  event: tag
+
+variables:
+ - &dind_image 'docker:20.10-dind'
+ - &integration_image 'codeberg.org/forgejo-integration/forgejo'
+ - &verify 'true'
+# for testing purposes
+# - &verify 'false'
+ - &archs 'amd64 arm64'
+
+pipeline:
+
+  publish:
+    image: *dind_image
+    environment:
+      INTEGRATION_IMAGE: *integration_image
+      VERIFY: *verify
+      ARCHS: *archs
+    commands:
+      - ./releases/container-images-pull-verify-push.sh
+    secrets:
+      - releaseteamtoken
+      - releaseteamuser
+      - domain