From c2fb145c5f33ea1b4c3ad7dd4ce21588bedc5e95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 10 Apr 2026 21:33:20 +0200 Subject: [PATCH 01/55] litepub-0.1.jsonld cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/context-cleanup.skip | 1 + priv/static/schemas/litepub-0.1.jsonld | 59 +++++++++++++++----------- 2 files changed, 35 insertions(+), 25 deletions(-) create mode 100644 changelog.d/context-cleanup.skip diff --git a/changelog.d/context-cleanup.skip b/changelog.d/context-cleanup.skip new file mode 100644 index 000000000..ae609602d --- /dev/null +++ b/changelog.d/context-cleanup.skip @@ -0,0 +1 @@ +litepub-0.1.jsonld cleanup diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld index 3569165a4..4e82d4b01 100644 --- a/priv/static/schemas/litepub-0.1.jsonld +++ b/priv/static/schemas/litepub-0.1.jsonld @@ -4,46 +4,55 @@ "https://w3id.org/security/v1", "https://purl.archive.org/socialweb/webfinger", { - "Emoji": "toot:Emoji", + "as": "https://www.w3.org/ns/activitystreams#", + "ostatus": "http://ostatus.org#", + "schema": "http://schema.org#", + "vcard": "http://www.w3.org/2006/vcard/ns#", + + "fedibird": "http://fedibird.com/ns#", + "litepub": "http://litepub.social/ns#", + "sm": "http://smithereen.software/ns#", + "toot": "http://joinmastodon.org/ns#", + + "alsoKnownAs": { + "@id": "as:alsoKnownAs", + "@type": "@id" + }, "Hashtag": "as:Hashtag", - "PropertyValue": "schema:PropertyValue", + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "quoteUrl": "as:quoteUrl", + "sensitive": "as:sensitive", + "atomUri": "ostatus:atomUri", "conversation": { "@id": "ostatus:conversation", "@type": "@id" }, - "discoverable": "toot:discoverable", - "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", - "capabilities": "litepub:capabilities", - "ostatus": "http://ostatus.org#", - "schema": "http://schema.org#", - "toot": "http://joinmastodon.org/ns#", - "fedibird": "http://fedibird.com/ns#", + + "PropertyValue": "schema:PropertyValue", "value": "schema:value", - "sensitive": "as:sensitive", - "litepub": "http://litepub.social/ns#", - "invisible": "litepub:invisible", + + "quoteUri": "fedibird:quoteUri", + + "capabilities": "litepub:capabilities", + "ChatMessage": "litepub:ChatMessage", "directMessage": "litepub:directMessage", + "EmojiReact": "litepub:EmojiReact", + "formerRepresentations": "litepub:formerRepresentations", + "invisible": "litepub:invisible", "listMessage": { "@id": "litepub:listMessage", "@type": "@id" }, - "quoteUrl": "as:quoteUrl", - "quoteUri": "fedibird:quoteUri", "oauthRegistrationEndpoint": { "@id": "litepub:oauthRegistrationEndpoint", "@type": "@id" }, - "EmojiReact": "litepub:EmojiReact", - "ChatMessage": "litepub:ChatMessage", - "alsoKnownAs": { - "@id": "as:alsoKnownAs", - "@type": "@id" - }, - "vcard": "http://www.w3.org/2006/vcard/ns#", - "formerRepresentations": "litepub:formerRepresentations", - "sm": "http://smithereen.software/ns#", - "nonAnonymous": "sm:nonAnonymous" + + "nonAnonymous": "sm:nonAnonymous", + + "discoverable": "toot:discoverable", + "Emoji": "toot:Emoji" } ] -} +} \ No newline at end of file From fc5aea73ff094a4b44599d20c0498688f039d3f4 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 7 Apr 2026 14:25:48 +0200 Subject: [PATCH 02/55] Woodpecker CI: Add develop Docker image build pipeline --- .woodpecker/docker-develop.yaml | 45 +++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .woodpecker/docker-develop.yaml diff --git a/.woodpecker/docker-develop.yaml b/.woodpecker/docker-develop.yaml new file mode 100644 index 000000000..c50fd7eb1 --- /dev/null +++ b/.woodpecker/docker-develop.yaml @@ -0,0 +1,45 @@ +when: + - event: push + branch: ${CI_REPO_DEFAULT_BRANCH} + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + +matrix: + platform: + - linux/amd64 + - linux/arm64 + +# This is needed for the when clauses below. +# When the platform clause is fixed, this might not be needed anymore +labels: + platform: ${platform} + +steps: + docker-develop-amd64: + image: woodpeckerci/plugin-docker-buildx:6.0.4 + # when: + # - platform: linux/amd64 + # does not work even though it should according to docs + # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 + when: + - evaluate: platform == "linux/amd64" + settings: + repo: git.pleroma.social/pleroma/pleroma + tags: [latest-amd64, develop-amd64] + registry: git.pleroma.social + username: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + + docker-develop-arm64: + image: woodpeckerci/plugin-docker-buildx:6.0.4 + when: + - evaluate: platform == "linux/arm64" + settings: + repo: git.pleroma.social/pleroma/pleroma + tags: [latest-arm64, develop-arm64] + registry: git.pleroma.social + username: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password From 5351cd4ce98511113a7bb132f43b2b3c46c00041 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 7 Apr 2026 19:01:26 +0200 Subject: [PATCH 03/55] Woodpecker CI: Add OTP develop pipeline musl and glibc builds need to be split due to workspace polution. Workflow's steps share the same workspace, so two separate build steps can't be in the same workflow since they share the buld artifacts, deps. --- .woodpecker/otp-develop-musl.yaml | 85 ++++++++++++++++++++++++++++++ .woodpecker/otp-develop.yaml | 87 +++++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 .woodpecker/otp-develop-musl.yaml create mode 100644 .woodpecker/otp-develop.yaml diff --git a/.woodpecker/otp-develop-musl.yaml b/.woodpecker/otp-develop-musl.yaml new file mode 100644 index 000000000..74703e759 --- /dev/null +++ b/.woodpecker/otp-develop-musl.yaml @@ -0,0 +1,85 @@ +when: + - event: push + branch: ${CI_REPO_DEFAULT_BRANCH} + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + +matrix: + platform: + - linux/amd64 + - linux/arm64 + +# This is needed for the when clauses below. +# When the platform clause is fixed, this might not be needed anymore +labels: + platform: ${platform} + +steps: + otp-develop-amd64-musl: + image: docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 + # when: + # - platform: linux/amd64 + # does not work even though it should according to docs + # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 + when: + - evaluate: platform == "linux/amd64" + environment: + MIX_ENV: prod + VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS + commands: + - apk add git build-base cmake file-dev openssl vips-dev zip + - echo "import Config" > config/prod.secret.exs + - mix local.hex --force + - mix local.rebar --force + - mix deps.get --only prod + - mkdir release + - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} + - mix release --path release + - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip release + + upload-artifacts-amd64-musl: + image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + when: + - evaluate: platform == "linux/amd64" + settings: + user: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + owner: 'pleroma' + package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-amd64-musl + package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl + file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip + file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip + + otp-develop-arm64-musl: + image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 + when: + - evaluate: platform == "linux/arm64" + environment: + MIX_ENV: prod + VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS + commands: + - apk add git build-base cmake file-dev openssl vips-dev zip + - echo "import Config" > config/prod.secret.exs + - mix local.hex --force + - mix local.rebar --force + - mix deps.get --only prod + - mkdir release + - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} + - mix release --path release + - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip release + + upload-artifacts-arm64-musl: + image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + when: + - evaluate: platform == "linux/arm64" + settings: + user: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + owner: 'pleroma' + package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-arm64-musl + package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl + file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip + file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip diff --git a/.woodpecker/otp-develop.yaml b/.woodpecker/otp-develop.yaml new file mode 100644 index 000000000..20a81569d --- /dev/null +++ b/.woodpecker/otp-develop.yaml @@ -0,0 +1,87 @@ +when: + - event: push + branch: ${CI_REPO_DEFAULT_BRANCH} + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + +matrix: + platform: + - linux/amd64 + - linux/arm64 + +# This is needed for the when clauses below. +# When the platform clause is fixed, this might not be needed anymore +labels: + platform: ${platform} + +steps: + otp-develop-amd64: + image: docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 + # when: + # - platform: linux/amd64 + # does not work even though it should according to docs + # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 + when: + - evaluate: platform == "linux/amd64" + environment: + MIX_ENV: prod + VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS + DEBIAN_FRONTEND: noninteractive + commands: + - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip + - echo "import Config" > config/prod.secret.exs + - mix local.hex --force + - mix local.rebar --force + - mix deps.get --only prod + - mkdir release + - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} + - mix release --path release + - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip release + + upload-artifacts-amd64: + image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + when: + - evaluate: platform == "linux/amd64" + settings: + user: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + owner: 'pleroma' + package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-amd64 + package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64 + file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip + file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip + + otp-develop-arm64: + image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 + when: + - evaluate: platform == "linux/arm64" + environment: + MIX_ENV: prod + VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS + DEBIAN_FRONTEND: noninteractive + commands: + - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip + - echo "import Config" > config/prod.secret.exs + - mix local.hex --force + - mix local.rebar --force + - mix deps.get --only prod + - mkdir release + - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} + - mix release --path release + - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip release + + upload-artifacts-arm64: + image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + when: + - evaluate: platform == "linux/arm64" + settings: + user: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + owner: 'pleroma' + package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-arm64 + package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64 + file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip + file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip From d2f7c9252f5d4e92144ca434f9643728a836ed86 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 9 Apr 2026 21:47:51 +0200 Subject: [PATCH 04/55] Woodpecker CI Docker develop: Switch to kaniko --- .woodpecker/docker-develop.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.woodpecker/docker-develop.yaml b/.woodpecker/docker-develop.yaml index c50fd7eb1..92299ca7c 100644 --- a/.woodpecker/docker-develop.yaml +++ b/.woodpecker/docker-develop.yaml @@ -15,7 +15,7 @@ labels: steps: docker-develop-amd64: - image: woodpeckerci/plugin-docker-buildx:6.0.4 + image: woodpeckerci/plugin-kaniko:2.3.1 # when: # - platform: linux/amd64 # does not work even though it should according to docs @@ -23,7 +23,7 @@ steps: when: - evaluate: platform == "linux/amd64" settings: - repo: git.pleroma.social/pleroma/pleroma + repo: pleroma/pleroma tags: [latest-amd64, develop-amd64] registry: git.pleroma.social username: @@ -32,11 +32,11 @@ steps: from_secret: pleroma-ci-password docker-develop-arm64: - image: woodpeckerci/plugin-docker-buildx:6.0.4 + image: woodpeckerci/plugin-kaniko:2.3.1 when: - evaluate: platform == "linux/arm64" settings: - repo: git.pleroma.social/pleroma/pleroma + repo: pleroma/pleroma tags: [latest-arm64, develop-arm64] registry: git.pleroma.social username: From e2adc796c40d23881640533ed1c3250ae451d089 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 9 Apr 2026 23:34:49 +0200 Subject: [PATCH 05/55] Woodpecker CI: Multiplatform Docker image manifests --- .woodpecker/docker-develop-combine.yaml | 34 +++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .woodpecker/docker-develop-combine.yaml diff --git a/.woodpecker/docker-develop-combine.yaml b/.woodpecker/docker-develop-combine.yaml new file mode 100644 index 000000000..f79e6b59b --- /dev/null +++ b/.woodpecker/docker-develop-combine.yaml @@ -0,0 +1,34 @@ +when: + - event: push + branch: ${CI_REPO_DEFAULT_BRANCH} + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + +depends_on: + - docker-develop + +skip_clone: true + +steps: + docker-develop-combine: + image: docker.io/docker:cli + environment: + BUILD_ARCHES: "amd64 arm64" + REGISTRY: "git.fluffytail.org" + IMAGE_PATH: "git.fluffytail.org/pleroma-test/pleroma" + REGISTRY_USER: + from_secret: pleroma-ci-user + REGISTRY_PASSWORD: + from_secret: pleroma-ci-password + commands: + - set +x + - mkdir -p ~/.docker + - echo "{\"auths\":{\"$REGISTRY\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASSWORD\"}}}" > ~/.docker/config.json + - set -x + - IMAGES_DEVELOP=; for arch in $BUILD_ARCHES; do IMAGES_DEVELOP="$IMAGES_DEVELOP $IMAGE_PATH:develop-$arch"; done + - IMAGES_LATEST=; for arch in $BUILD_ARCHES; do IMAGES_LATEST="$IMAGES_LATEST $IMAGE_PATH:latest-$arch"; done + - echo $IMAGES_DEVELOP + - echo $IMAGES_LATEST + - docker manifest create $IMAGE_PATH:develop $IMAGES_DEVELOP + - docker manifest push $IMAGE_PATH:develop + - docker manifest create $IMAGE_PATH:latest $IMAGES_LATEST + - docker manifest push $IMAGE_PATH:latest From 67e7f788c9b6688cda039f9ee4473522367ced72 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sun, 12 Apr 2026 00:00:10 +0200 Subject: [PATCH 06/55] Woodpecker CI Docker Develop combine: Switch to plugin Replaces manual tagging handling with a plugin, mostly to avoid dealing with echoed out secrets in the job log, which should be censored automatically, but who knows when that breaks... --- .woodpecker/docker-develop-combine.yaml | 28 +++++++------------------ 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/.woodpecker/docker-develop-combine.yaml b/.woodpecker/docker-develop-combine.yaml index f79e6b59b..67da6ca12 100644 --- a/.woodpecker/docker-develop-combine.yaml +++ b/.woodpecker/docker-develop-combine.yaml @@ -10,25 +10,13 @@ skip_clone: true steps: docker-develop-combine: - image: docker.io/docker:cli - environment: - BUILD_ARCHES: "amd64 arm64" - REGISTRY: "git.fluffytail.org" - IMAGE_PATH: "git.fluffytail.org/pleroma-test/pleroma" - REGISTRY_USER: + image: git.fluffytail.org/phnt/wpc-docker-tagger:latest + settings: + registry: "git.fluffytail.org" + image: "pleroma-test/pleroma" + architectures: [amd64, arm64] + tags: [latest, develop] + username: from_secret: pleroma-ci-user - REGISTRY_PASSWORD: + password: from_secret: pleroma-ci-password - commands: - - set +x - - mkdir -p ~/.docker - - echo "{\"auths\":{\"$REGISTRY\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASSWORD\"}}}" > ~/.docker/config.json - - set -x - - IMAGES_DEVELOP=; for arch in $BUILD_ARCHES; do IMAGES_DEVELOP="$IMAGES_DEVELOP $IMAGE_PATH:develop-$arch"; done - - IMAGES_LATEST=; for arch in $BUILD_ARCHES; do IMAGES_LATEST="$IMAGES_LATEST $IMAGE_PATH:latest-$arch"; done - - echo $IMAGES_DEVELOP - - echo $IMAGES_LATEST - - docker manifest create $IMAGE_PATH:develop $IMAGES_DEVELOP - - docker manifest push $IMAGE_PATH:develop - - docker manifest create $IMAGE_PATH:latest $IMAGES_LATEST - - docker manifest push $IMAGE_PATH:latest From 13d6246ed9e77e5cd5f812c6b22859d8a310ee0e Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sun, 12 Apr 2026 22:19:56 +0200 Subject: [PATCH 07/55] Woodpecker CI: Cleanup develop releases CI code duplication --- .woodpecker/docker-develop.yaml | 23 ++++++++-------- .woodpecker/otp-develop-musl.yaml | 44 ++++++++++++++----------------- .woodpecker/otp-develop.yaml | 44 ++++++++++++++----------------- 3 files changed, 51 insertions(+), 60 deletions(-) diff --git a/.woodpecker/docker-develop.yaml b/.woodpecker/docker-develop.yaml index 92299ca7c..1a61b1626 100644 --- a/.woodpecker/docker-develop.yaml +++ b/.woodpecker/docker-develop.yaml @@ -13,6 +13,15 @@ matrix: labels: platform: ${platform} +variables: + docker_variables: &docker_variables + repo: pleroma/pleroma + registry: git.pleroma.social + username: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + steps: docker-develop-amd64: image: woodpeckerci/plugin-kaniko:2.3.1 @@ -23,23 +32,13 @@ steps: when: - evaluate: platform == "linux/amd64" settings: - repo: pleroma/pleroma + <<: *docker_variables tags: [latest-amd64, develop-amd64] - registry: git.pleroma.social - username: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password docker-develop-arm64: image: woodpeckerci/plugin-kaniko:2.3.1 when: - evaluate: platform == "linux/arm64" settings: - repo: pleroma/pleroma + <<: *docker_variables tags: [latest-arm64, develop-arm64] - registry: git.pleroma.social - username: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password diff --git a/.woodpecker/otp-develop-musl.yaml b/.woodpecker/otp-develop-musl.yaml index 74703e759..7ed567949 100644 --- a/.woodpecker/otp-develop-musl.yaml +++ b/.woodpecker/otp-develop-musl.yaml @@ -13,6 +13,22 @@ matrix: labels: platform: ${platform} +variables: + pleroma_build_cmds: &pleroma_build_cmds + - echo "import Config" > config/prod.secret.exs + - mix local.hex --force + - mix local.rebar --force + - mix deps.get --only prod + - mkdir release + - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} + - mix release --path release + artifacts_uploader_settings: &artifacts_uploader_settings + user: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + owner: 'pleroma' + steps: otp-develop-amd64-musl: image: docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 @@ -27,13 +43,7 @@ steps: VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS commands: - apk add git build-base cmake file-dev openssl vips-dev zip - - echo "import Config" > config/prod.secret.exs - - mix local.hex --force - - mix local.rebar --force - - mix deps.get --only prod - - mkdir release - - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} - - mix release --path release + - <<: *pleroma_build_cmds - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip release upload-artifacts-amd64-musl: @@ -41,11 +51,7 @@ steps: when: - evaluate: platform == "linux/amd64" settings: - user: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password - owner: 'pleroma' + <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-amd64-musl package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip @@ -60,13 +66,7 @@ steps: VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS commands: - apk add git build-base cmake file-dev openssl vips-dev zip - - echo "import Config" > config/prod.secret.exs - - mix local.hex --force - - mix local.rebar --force - - mix deps.get --only prod - - mkdir release - - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} - - mix release --path release + - <<: *pleroma_build_cmds - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip release upload-artifacts-arm64-musl: @@ -74,11 +74,7 @@ steps: when: - evaluate: platform == "linux/arm64" settings: - user: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password - owner: 'pleroma' + <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-arm64-musl package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip diff --git a/.woodpecker/otp-develop.yaml b/.woodpecker/otp-develop.yaml index 20a81569d..6cd76d450 100644 --- a/.woodpecker/otp-develop.yaml +++ b/.woodpecker/otp-develop.yaml @@ -13,6 +13,22 @@ matrix: labels: platform: ${platform} +variables: + pleroma_build_cmds: &pleroma_build_cmds + - echo "import Config" > config/prod.secret.exs + - mix local.hex --force + - mix local.rebar --force + - mix deps.get --only prod + - mkdir release + - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} + - mix release --path release + artifacts_uploader_settings: &artifacts_uploader_settings + user: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + owner: 'pleroma' + steps: otp-develop-amd64: image: docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 @@ -28,13 +44,7 @@ steps: DEBIAN_FRONTEND: noninteractive commands: - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - - echo "import Config" > config/prod.secret.exs - - mix local.hex --force - - mix local.rebar --force - - mix deps.get --only prod - - mkdir release - - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} - - mix release --path release + - <<: *pleroma_build_cmds - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip release upload-artifacts-amd64: @@ -42,11 +52,7 @@ steps: when: - evaluate: platform == "linux/amd64" settings: - user: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password - owner: 'pleroma' + <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-amd64 package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64 file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip @@ -62,13 +68,7 @@ steps: DEBIAN_FRONTEND: noninteractive commands: - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - - echo "import Config" > config/prod.secret.exs - - mix local.hex --force - - mix local.rebar --force - - mix deps.get --only prod - - mkdir release - - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} - - mix release --path release + - <<: *pleroma_build_cmds - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip release upload-artifacts-arm64: @@ -76,11 +76,7 @@ steps: when: - evaluate: platform == "linux/arm64" settings: - user: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password - owner: 'pleroma' + <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-arm64 package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64 file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip From f00c13602d734f4215e2ac6c54a6cbbff12e5d40 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sun, 12 Apr 2026 22:44:40 +0200 Subject: [PATCH 08/55] Woodpecker CI Develop: Also tag images using commit sha With the commit sha being present, `tags` now has to be a list instead of an array, otherwise Woodpecker raises a yaml compiler warning: yaml: line 17: did not find expected ',' or ']' --- .woodpecker/docker-develop-combine.yaml | 5 ++++- .woodpecker/docker-develop.yaml | 10 ++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.woodpecker/docker-develop-combine.yaml b/.woodpecker/docker-develop-combine.yaml index 67da6ca12..ebe5a502c 100644 --- a/.woodpecker/docker-develop-combine.yaml +++ b/.woodpecker/docker-develop-combine.yaml @@ -15,7 +15,10 @@ steps: registry: "git.fluffytail.org" image: "pleroma-test/pleroma" architectures: [amd64, arm64] - tags: [latest, develop] + tags: + - latest + - develop + - ${CI_COMMIT_SHA} username: from_secret: pleroma-ci-user password: diff --git a/.woodpecker/docker-develop.yaml b/.woodpecker/docker-develop.yaml index 1a61b1626..4f6bbdf12 100644 --- a/.woodpecker/docker-develop.yaml +++ b/.woodpecker/docker-develop.yaml @@ -33,7 +33,10 @@ steps: - evaluate: platform == "linux/amd64" settings: <<: *docker_variables - tags: [latest-amd64, develop-amd64] + tags: + - latest-amd64 + - develop-amd64 + - ${CI_COMMIT_SHA}-amd64 docker-develop-arm64: image: woodpeckerci/plugin-kaniko:2.3.1 @@ -41,4 +44,7 @@ steps: - evaluate: platform == "linux/arm64" settings: <<: *docker_variables - tags: [latest-arm64, develop-arm64] + tags: + - latest-arm64 + - develop-arm64 + - ${CI_COMMIT_SHA}-arm64 From e002650e23f4e1afe7d79585f8e6c6177a34f97d Mon Sep 17 00:00:00 2001 From: Phantasm Date: Mon, 13 Apr 2026 15:15:45 +0200 Subject: [PATCH 09/55] Woodpecker CI: Add Docker stable releases --- .woodpecker/docker-stable-combine.yaml | 27 +++++++++++++ .woodpecker/docker-stable.yaml | 52 ++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 .woodpecker/docker-stable-combine.yaml create mode 100644 .woodpecker/docker-stable.yaml diff --git a/.woodpecker/docker-stable-combine.yaml b/.woodpecker/docker-stable-combine.yaml new file mode 100644 index 000000000..e011750f1 --- /dev/null +++ b/.woodpecker/docker-stable-combine.yaml @@ -0,0 +1,27 @@ +when: + - event: push + branch: stable + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + - event: tag + branch: stable + +depends_on: + - docker-stable + +skip_clone: true + +steps: + docker-stable-combine: + image: git.fluffytail.org/phnt/wpc-docker-tagger:latest + settings: + registry: "git.fluffytail.org" + image: "pleroma-test/pleroma" + architectures: [amd64, arm64] + tags: + - latest + - stable + - ${CI_COMMIT_SHA} + username: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password diff --git a/.woodpecker/docker-stable.yaml b/.woodpecker/docker-stable.yaml new file mode 100644 index 000000000..6c5e31091 --- /dev/null +++ b/.woodpecker/docker-stable.yaml @@ -0,0 +1,52 @@ +when: + - event: push + branch: stable + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + - event: tag + branch: stable + +matrix: + platform: + - linux/amd64 + - linux/arm64 + +# This is needed for the when clauses below. +# When the platform clause is fixed, this might not be needed anymore +labels: + platform: ${platform} + +variables: + docker_variables: &docker_variables + repo: pleroma-test/pleroma + registry: git.fluffytail.org + username: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + +steps: + docker-stable-amd64: + image: woodpeckerci/plugin-kaniko:2.3.1 + # when: + # - platform: linux/amd64 + # does not work even though it should according to docs + # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 + when: + - evaluate: platform == "linux/amd64" + settings: + <<: *docker_variables + tags: + - latest-amd64 + - stable-amd64 + - ${CI_COMMIT_SHA}-amd64 + + docker-stable-arm64: + image: woodpeckerci/plugin-kaniko:2.3.1 + when: + - evaluate: platform == "linux/arm64" + settings: + <<: *docker_variables + tags: + - latest-arm64 + - stable-arm64 + - ${CI_COMMIT_SHA}-arm64 From 97a2e8c7647bbf40e554fb1ee40ef8160525dfd9 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Mon, 13 Apr 2026 17:17:28 +0200 Subject: [PATCH 10/55] Woodpecker CI: Tag stable docker release with version tag --- .woodpecker/docker-stable-combine.yaml | 16 +++++++++++-- .woodpecker/docker-stable.yaml | 33 +++++++++++++++++++++----- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/.woodpecker/docker-stable-combine.yaml b/.woodpecker/docker-stable-combine.yaml index e011750f1..e07e7a83c 100644 --- a/.woodpecker/docker-stable-combine.yaml +++ b/.woodpecker/docker-stable-combine.yaml @@ -13,11 +13,13 @@ skip_clone: true steps: docker-stable-combine: image: git.fluffytail.org/phnt/wpc-docker-tagger:latest - settings: + when: + - event: push + settings: &docker_settings registry: "git.fluffytail.org" image: "pleroma-test/pleroma" architectures: [amd64, arm64] - tags: + tags: &docker_tags - latest - stable - ${CI_COMMIT_SHA} @@ -25,3 +27,13 @@ steps: from_secret: pleroma-ci-user password: from_secret: pleroma-ci-password + + docker-stable-tag-combine: + image: git.fluffytail.org/phnt/wpc-docker-tagger:latest + when: + - event: tag + settings: + <<: *docker_settings + tags: + - <<: *docker_tags + - ${CI_COMMIT_TAG} diff --git a/.woodpecker/docker-stable.yaml b/.woodpecker/docker-stable.yaml index 6c5e31091..dcf98e31f 100644 --- a/.woodpecker/docker-stable.yaml +++ b/.woodpecker/docker-stable.yaml @@ -23,30 +23,51 @@ variables: from_secret: pleroma-ci-user password: from_secret: pleroma-ci-password + kaniko_image: &kaniko_image woodpeckerci/plugin-kaniko:2.3.1 steps: docker-stable-amd64: - image: woodpeckerci/plugin-kaniko:2.3.1 + image: *kaniko_image # when: # - platform: linux/amd64 # does not work even though it should according to docs # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 when: - - evaluate: platform == "linux/amd64" + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push"' settings: <<: *docker_variables - tags: + tags: &amd64_tags - latest-amd64 - stable-amd64 - ${CI_COMMIT_SHA}-amd64 - docker-stable-arm64: - image: woodpeckerci/plugin-kaniko:2.3.1 + docker-stable-tag-amd64: + image: *kaniko_image when: - - evaluate: platform == "linux/arm64" + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' settings: <<: *docker_variables tags: + - <<: *amd64_tags + - ${CI_COMMIT_TAG}-amd64 + + docker-stable-arm64: + image: *kaniko_image + when: + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push"' + settings: + <<: *docker_variables + tags: &arm64_tags - latest-arm64 - stable-arm64 - ${CI_COMMIT_SHA}-arm64 + + docker-stable-tag-arm64: + image: *kaniko_image + when: + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *docker_variables + tags: + - <<: *arm64_tags + - ${CI_COMMIT_TAG}-arm64 From 42eb9706a5cb2a76d9093852735459371d647fb1 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 17 Apr 2026 19:39:53 +0200 Subject: [PATCH 11/55] Woodpecker CI: Build stable OTP releases --- .woodpecker/otp-stable-musl.yaml | 83 +++++++++++++++++++++++++++++++ .woodpecker/otp-stable.yaml | 85 ++++++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 .woodpecker/otp-stable-musl.yaml create mode 100644 .woodpecker/otp-stable.yaml diff --git a/.woodpecker/otp-stable-musl.yaml b/.woodpecker/otp-stable-musl.yaml new file mode 100644 index 000000000..f97bde4ef --- /dev/null +++ b/.woodpecker/otp-stable-musl.yaml @@ -0,0 +1,83 @@ +when: + - event: push + branch: stable + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + - event: tag + branch: stable + +matrix: + platform: + - linux/amd64 + - linux/arm64 + +# This is needed for the when clauses below. +# When the platform clause is fixed, this might not be needed anymore +labels: + platform: ${platform} + +variables: + pleroma_build_cmds: &pleroma_build_cmds + - echo "import Config" > config/prod.secret.exs + - mix local.hex --force + - mix local.rebar --force + - mix deps.get --only prod + - mkdir release + - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} + - mix release --path release + artifacts_uploader_settings: &artifacts_uploader_settings + user: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + owner: 'pleroma-test' + +steps: + otp-stable-amd64-musl: + image: docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 + # when: + # - platform: linux/amd64 + # does not work even though it should according to docs + # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 + when: + - evaluate: platform == "linux/amd64" + environment: + MIX_ENV: prod + VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS + commands: + - apk add git build-base cmake file-dev openssl vips-dev zip + - <<: *pleroma_build_cmds + - zip -9rq pleroma-stable-${CI_COMMIT_SHA}-amd64-musl.zip release + + upload-artifacts-amd64-musl: + image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + when: + - evaluate: platform == "linux/amd64" + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-amd64-musl + package_version: stable-${CI_COMMIT_SHA}-amd64-musl + file_source: ./pleroma-stable-${CI_COMMIT_SHA}-amd64-musl.zip + file_name: ./pleroma-stable-${CI_COMMIT_SHA}-amd64-musl.zip + + otp-stable-arm64-musl: + image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 + when: + - evaluate: platform == "linux/arm64" + environment: + MIX_ENV: prod + VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS + commands: + - apk add git build-base cmake file-dev openssl vips-dev zip + - <<: *pleroma_build_cmds + - zip -9rq pleroma-stable-${CI_COMMIT_SHA}-arm64-musl.zip release + + upload-artifacts-arm64-musl: + image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + when: + - evaluate: platform == "linux/arm64" + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-arm64-musl + package_version: stable-${CI_COMMIT_SHA}-arm64-musl + file_source: ./pleroma-stable-${CI_COMMIT_SHA}-arm64-musl.zip + file_name: ./pleroma-stable-${CI_COMMIT_SHA}-arm64-musl.zip diff --git a/.woodpecker/otp-stable.yaml b/.woodpecker/otp-stable.yaml new file mode 100644 index 000000000..6219ff403 --- /dev/null +++ b/.woodpecker/otp-stable.yaml @@ -0,0 +1,85 @@ +when: + - event: push + branch: stable + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + - event: tag + branch: stable + +matrix: + platform: + - linux/amd64 + - linux/arm64 + +# This is needed for the when clauses below. +# When the platform clause is fixed, this might not be needed anymore +labels: + platform: ${platform} + +variables: + pleroma_build_cmds: &pleroma_build_cmds + - echo "import Config" > config/prod.secret.exs + - mix local.hex --force + - mix local.rebar --force + - mix deps.get --only prod + - mkdir release + - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} + - mix release --path release + artifacts_uploader_settings: &artifacts_uploader_settings + user: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + owner: 'pleroma-test' + +steps: + otp-stable-amd64: + image: docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 + # when: + # - platform: linux/amd64 + # does not work even though it should according to docs + # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 + when: + - evaluate: platform == "linux/amd64" + environment: + MIX_ENV: prod + VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS + DEBIAN_FRONTEND: noninteractive + commands: + - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip + - <<: *pleroma_build_cmds + - zip -9rq pleroma-stable-${CI_COMMIT_SHA}-amd64.zip release + + upload-artifacts-amd64: + image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + when: + - evaluate: platform == "linux/amd64" + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-amd64 + package_version: stable-${CI_COMMIT_SHA}-amd64 + file_source: ./pleroma-stable-${CI_COMMIT_SHA}-amd64.zip + file_name: ./pleroma-stable-${CI_COMMIT_SHA}-amd64.zip + + otp-stable-arm64: + image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 + when: + - evaluate: platform == "linux/arm64" + environment: + MIX_ENV: prod + VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS + DEBIAN_FRONTEND: noninteractive + commands: + - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip + - <<: *pleroma_build_cmds + - zip -9rq pleroma-stable-${CI_COMMIT_SHA}-arm64.zip release + + upload-artifacts-arm64: + image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + when: + - evaluate: platform == "linux/arm64" + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-arm64 + package_version: stable-${CI_COMMIT_SHA}-arm64 + file_source: ./pleroma-stable-${CI_COMMIT_SHA}-arm64.zip + file_name: ./pleroma-stable-${CI_COMMIT_SHA}-arm64.zip From eea01b54b7852628142dae3af9a280ee92620248 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 18 Apr 2026 22:28:40 +0200 Subject: [PATCH 12/55] Woodpecker CI: Allow running stable release jobs manually Also allows Docker images to be tagged with a version in manual jobs when CI_COMMIT_TAG is manually specified --- .woodpecker/docker-stable-combine.yaml | 4 ++++ .woodpecker/docker-stable.yaml | 6 ++++++ .woodpecker/otp-stable-musl.yaml | 2 ++ .woodpecker/otp-stable.yaml | 2 ++ 4 files changed, 14 insertions(+) diff --git a/.woodpecker/docker-stable-combine.yaml b/.woodpecker/docker-stable-combine.yaml index e07e7a83c..9f144d3f6 100644 --- a/.woodpecker/docker-stable-combine.yaml +++ b/.woodpecker/docker-stable-combine.yaml @@ -4,6 +4,8 @@ when: path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: tag branch: stable + - event: manual + branch: stable depends_on: - docker-stable @@ -15,6 +17,7 @@ steps: image: git.fluffytail.org/phnt/wpc-docker-tagger:latest when: - event: push + - evaluate: 'CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG == ""' settings: &docker_settings registry: "git.fluffytail.org" image: "pleroma-test/pleroma" @@ -32,6 +35,7 @@ steps: image: git.fluffytail.org/phnt/wpc-docker-tagger:latest when: - event: tag + - evaluate: 'CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG != ""' settings: <<: *docker_settings tags: diff --git a/.woodpecker/docker-stable.yaml b/.woodpecker/docker-stable.yaml index dcf98e31f..bd32b94d1 100644 --- a/.woodpecker/docker-stable.yaml +++ b/.woodpecker/docker-stable.yaml @@ -4,6 +4,8 @@ when: path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: tag branch: stable + - event: manual + branch: stable matrix: platform: @@ -34,6 +36,7 @@ steps: # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 when: - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG == ""' settings: <<: *docker_variables tags: &amd64_tags @@ -45,6 +48,7 @@ steps: image: *kaniko_image when: - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG != ""' settings: <<: *docker_variables tags: @@ -55,6 +59,7 @@ steps: image: *kaniko_image when: - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG == ""' settings: <<: *docker_variables tags: &arm64_tags @@ -66,6 +71,7 @@ steps: image: *kaniko_image when: - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG != ""' settings: <<: *docker_variables tags: diff --git a/.woodpecker/otp-stable-musl.yaml b/.woodpecker/otp-stable-musl.yaml index f97bde4ef..4663a4401 100644 --- a/.woodpecker/otp-stable-musl.yaml +++ b/.woodpecker/otp-stable-musl.yaml @@ -4,6 +4,8 @@ when: path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: tag branch: stable + - event: manual + branch: stable matrix: platform: diff --git a/.woodpecker/otp-stable.yaml b/.woodpecker/otp-stable.yaml index 6219ff403..afb23eada 100644 --- a/.woodpecker/otp-stable.yaml +++ b/.woodpecker/otp-stable.yaml @@ -4,6 +4,8 @@ when: path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: tag branch: stable + - event: manual + branch: stable matrix: platform: From dd29b9c11bdef53a81071f530b7416970baa8e70 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 18 Apr 2026 23:25:53 +0200 Subject: [PATCH 13/55] Woodpecker CI OTP: use CI_COMMIT_BRANCH variable instead of stable --- .woodpecker/otp-stable-musl.yaml | 20 ++++++++++---------- .woodpecker/otp-stable.yaml | 20 ++++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/.woodpecker/otp-stable-musl.yaml b/.woodpecker/otp-stable-musl.yaml index 4663a4401..96be0bf8e 100644 --- a/.woodpecker/otp-stable-musl.yaml +++ b/.woodpecker/otp-stable-musl.yaml @@ -48,7 +48,7 @@ steps: commands: - apk add git build-base cmake file-dev openssl vips-dev zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-stable-${CI_COMMIT_SHA}-amd64-musl.zip release + - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip release upload-artifacts-amd64-musl: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -56,10 +56,10 @@ steps: - evaluate: platform == "linux/amd64" settings: <<: *artifacts_uploader_settings - package_name: pleroma-otp-stable-amd64-musl - package_version: stable-${CI_COMMIT_SHA}-amd64-musl - file_source: ./pleroma-stable-${CI_COMMIT_SHA}-amd64-musl.zip - file_name: ./pleroma-stable-${CI_COMMIT_SHA}-amd64-musl.zip + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64-musl + package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl + file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip + file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip otp-stable-arm64-musl: image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 @@ -71,7 +71,7 @@ steps: commands: - apk add git build-base cmake file-dev openssl vips-dev zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-stable-${CI_COMMIT_SHA}-arm64-musl.zip release + - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip release upload-artifacts-arm64-musl: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -79,7 +79,7 @@ steps: - evaluate: platform == "linux/arm64" settings: <<: *artifacts_uploader_settings - package_name: pleroma-otp-stable-arm64-musl - package_version: stable-${CI_COMMIT_SHA}-arm64-musl - file_source: ./pleroma-stable-${CI_COMMIT_SHA}-arm64-musl.zip - file_name: ./pleroma-stable-${CI_COMMIT_SHA}-arm64-musl.zip + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64-musl + package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl + file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip + file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip diff --git a/.woodpecker/otp-stable.yaml b/.woodpecker/otp-stable.yaml index afb23eada..ae2670a79 100644 --- a/.woodpecker/otp-stable.yaml +++ b/.woodpecker/otp-stable.yaml @@ -49,7 +49,7 @@ steps: commands: - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-stable-${CI_COMMIT_SHA}-amd64.zip release + - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip release upload-artifacts-amd64: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -57,10 +57,10 @@ steps: - evaluate: platform == "linux/amd64" settings: <<: *artifacts_uploader_settings - package_name: pleroma-otp-stable-amd64 - package_version: stable-${CI_COMMIT_SHA}-amd64 - file_source: ./pleroma-stable-${CI_COMMIT_SHA}-amd64.zip - file_name: ./pleroma-stable-${CI_COMMIT_SHA}-amd64.zip + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64 + package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64 + file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip + file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip otp-stable-arm64: image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 @@ -73,7 +73,7 @@ steps: commands: - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-stable-${CI_COMMIT_SHA}-arm64.zip release + - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip release upload-artifacts-arm64: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -81,7 +81,7 @@ steps: - evaluate: platform == "linux/arm64" settings: <<: *artifacts_uploader_settings - package_name: pleroma-otp-stable-arm64 - package_version: stable-${CI_COMMIT_SHA}-arm64 - file_source: ./pleroma-stable-${CI_COMMIT_SHA}-arm64.zip - file_name: ./pleroma-stable-${CI_COMMIT_SHA}-arm64.zip + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64 + package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64 + file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip + file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip From 89a78d765cc231934608149f394389a9b90e840c Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 21 Apr 2026 16:09:15 +0200 Subject: [PATCH 14/55] Woodpecker CI: Unify Docker image workflows --- .woodpecker/docker-combine.yaml | 65 +++++++++++++++++++ .woodpecker/docker-develop-combine.yaml | 25 ------- .woodpecker/docker-develop.yaml | 50 -------------- .woodpecker/docker-stable-combine.yaml | 43 ------------ .../{docker-stable.yaml => docker.yaml} | 50 +++++++++----- 5 files changed, 100 insertions(+), 133 deletions(-) create mode 100644 .woodpecker/docker-combine.yaml delete mode 100644 .woodpecker/docker-develop-combine.yaml delete mode 100644 .woodpecker/docker-develop.yaml delete mode 100644 .woodpecker/docker-stable-combine.yaml rename .woodpecker/{docker-stable.yaml => docker.yaml} (58%) diff --git a/.woodpecker/docker-combine.yaml b/.woodpecker/docker-combine.yaml new file mode 100644 index 000000000..46401b0cb --- /dev/null +++ b/.woodpecker/docker-combine.yaml @@ -0,0 +1,65 @@ +when: + - event: push + branch: ${CI_REPO_DEFAULT_BRANCH} + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + - event: push + branch: stable + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + - event: tag + branch: stable + - event: manual + branch: stable + +depends_on: + - docker + +skip_clone: true + +steps: + docker-develop-combine: + image: git.fluffytail.org/phnt/wpc-docker-tagger:latest + when: + - event: push + branch: ${CI_REPO_DEFAULT_BRANCH} + settings: + registry: "git.pleroma.social" + image: "pleroma/pleroma" + architectures: [amd64, arm64] + tags: + - latest + - develop + - ${CI_COMMIT_SHA} + username: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + + docker-stable-combine: + image: git.fluffytail.org/phnt/wpc-docker-tagger:latest + when: + - event: push + branch: stable + - evaluate: 'CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG == ""' + settings: &docker_settings + registry: "git.pleroma.social" + image: "pleroma/pleroma" + architectures: [amd64, arm64] + tags: &docker_tags + - latest + - stable + - ${CI_COMMIT_SHA} + username: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + + docker-stable-tag-combine: + image: git.fluffytail.org/phnt/wpc-docker-tagger:latest + when: + - event: tag + - evaluate: 'CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG != ""' + settings: + <<: *docker_settings + tags: + - <<: *docker_tags + - ${CI_COMMIT_TAG} diff --git a/.woodpecker/docker-develop-combine.yaml b/.woodpecker/docker-develop-combine.yaml deleted file mode 100644 index ebe5a502c..000000000 --- a/.woodpecker/docker-develop-combine.yaml +++ /dev/null @@ -1,25 +0,0 @@ -when: - - event: push - branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - -depends_on: - - docker-develop - -skip_clone: true - -steps: - docker-develop-combine: - image: git.fluffytail.org/phnt/wpc-docker-tagger:latest - settings: - registry: "git.fluffytail.org" - image: "pleroma-test/pleroma" - architectures: [amd64, arm64] - tags: - - latest - - develop - - ${CI_COMMIT_SHA} - username: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password diff --git a/.woodpecker/docker-develop.yaml b/.woodpecker/docker-develop.yaml deleted file mode 100644 index 4f6bbdf12..000000000 --- a/.woodpecker/docker-develop.yaml +++ /dev/null @@ -1,50 +0,0 @@ -when: - - event: push - branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - -matrix: - platform: - - linux/amd64 - - linux/arm64 - -# This is needed for the when clauses below. -# When the platform clause is fixed, this might not be needed anymore -labels: - platform: ${platform} - -variables: - docker_variables: &docker_variables - repo: pleroma/pleroma - registry: git.pleroma.social - username: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password - -steps: - docker-develop-amd64: - image: woodpeckerci/plugin-kaniko:2.3.1 - # when: - # - platform: linux/amd64 - # does not work even though it should according to docs - # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 - when: - - evaluate: platform == "linux/amd64" - settings: - <<: *docker_variables - tags: - - latest-amd64 - - develop-amd64 - - ${CI_COMMIT_SHA}-amd64 - - docker-develop-arm64: - image: woodpeckerci/plugin-kaniko:2.3.1 - when: - - evaluate: platform == "linux/arm64" - settings: - <<: *docker_variables - tags: - - latest-arm64 - - develop-arm64 - - ${CI_COMMIT_SHA}-arm64 diff --git a/.woodpecker/docker-stable-combine.yaml b/.woodpecker/docker-stable-combine.yaml deleted file mode 100644 index 9f144d3f6..000000000 --- a/.woodpecker/docker-stable-combine.yaml +++ /dev/null @@ -1,43 +0,0 @@ -when: - - event: push - branch: stable - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - - event: tag - branch: stable - - event: manual - branch: stable - -depends_on: - - docker-stable - -skip_clone: true - -steps: - docker-stable-combine: - image: git.fluffytail.org/phnt/wpc-docker-tagger:latest - when: - - event: push - - evaluate: 'CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG == ""' - settings: &docker_settings - registry: "git.fluffytail.org" - image: "pleroma-test/pleroma" - architectures: [amd64, arm64] - tags: &docker_tags - - latest - - stable - - ${CI_COMMIT_SHA} - username: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password - - docker-stable-tag-combine: - image: git.fluffytail.org/phnt/wpc-docker-tagger:latest - when: - - event: tag - - evaluate: 'CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG != ""' - settings: - <<: *docker_settings - tags: - - <<: *docker_tags - - ${CI_COMMIT_TAG} diff --git a/.woodpecker/docker-stable.yaml b/.woodpecker/docker.yaml similarity index 58% rename from .woodpecker/docker-stable.yaml rename to .woodpecker/docker.yaml index bd32b94d1..c1299b4fe 100644 --- a/.woodpecker/docker-stable.yaml +++ b/.woodpecker/docker.yaml @@ -1,4 +1,7 @@ when: + - event: push + branch: ${CI_REPO_DEFAULT_BRANCH} + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: push branch: stable path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] @@ -13,14 +16,13 @@ matrix: - linux/arm64 # This is needed for the when clauses below. -# When the platform clause is fixed, this might not be needed anymore labels: platform: ${platform} variables: docker_variables: &docker_variables - repo: pleroma-test/pleroma - registry: git.fluffytail.org + repo: pleroma/pleroma + registry: git.pleroma.social username: from_secret: pleroma-ci-user password: @@ -28,15 +30,33 @@ variables: kaniko_image: &kaniko_image woodpeckerci/plugin-kaniko:2.3.1 steps: + docker-develop-amd64: + image: woodpeckerci/plugin-kaniko:2.3.1 + when: + - evaluate: 'platform == "linux/amd64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + settings: + <<: *docker_variables + tags: + - latest-amd64 + - develop-amd64 + - ${CI_COMMIT_SHA}-amd64 + + docker-develop-arm64: + image: woodpeckerci/plugin-kaniko:2.3.1 + when: + - evaluate: 'platform == "linux/arm64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + settings: + <<: *docker_variables + tags: + - latest-arm64 + - develop-arm64 + - ${CI_COMMIT_SHA}-arm64 + docker-stable-amd64: image: *kaniko_image - # when: - # - platform: linux/amd64 - # does not work even though it should according to docs - # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 when: - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push"' - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG == ""' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG == ""' settings: <<: *docker_variables tags: &amd64_tags @@ -47,8 +67,8 @@ steps: docker-stable-tag-amd64: image: *kaniko_image when: - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG != ""' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG != ""' settings: <<: *docker_variables tags: @@ -58,8 +78,8 @@ steps: docker-stable-arm64: image: *kaniko_image when: - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push"' - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG == ""' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG == ""' settings: <<: *docker_variables tags: &arm64_tags @@ -70,8 +90,8 @@ steps: docker-stable-tag-arm64: image: *kaniko_image when: - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG != ""' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG != ""' settings: <<: *docker_variables tags: From d8b8cbbb8d74938159bdf50c190bf54eba276557 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 21 Apr 2026 18:33:39 +0200 Subject: [PATCH 15/55] Woodpecker CI: Shorten commit sha to eight chars This will hopefully help with avoiding: https://github.com/woodpecker-ci/woodpecker/issues/5450 --- .woodpecker/docker-combine.yaml | 4 ++-- .woodpecker/docker.yaml | 8 ++++---- .woodpecker/otp-develop-musl.yaml | 16 ++++++++-------- .woodpecker/otp-develop.yaml | 16 ++++++++-------- .woodpecker/otp-stable-musl.yaml | 16 ++++++++-------- .woodpecker/otp-stable.yaml | 16 ++++++++-------- 6 files changed, 38 insertions(+), 38 deletions(-) diff --git a/.woodpecker/docker-combine.yaml b/.woodpecker/docker-combine.yaml index 46401b0cb..c69bcf1cf 100644 --- a/.woodpecker/docker-combine.yaml +++ b/.woodpecker/docker-combine.yaml @@ -28,7 +28,7 @@ steps: tags: - latest - develop - - ${CI_COMMIT_SHA} + - ${CI_COMMIT_SHA:0:8} username: from_secret: pleroma-ci-user password: @@ -47,7 +47,7 @@ steps: tags: &docker_tags - latest - stable - - ${CI_COMMIT_SHA} + - ${CI_COMMIT_SHA:0:8} username: from_secret: pleroma-ci-user password: diff --git a/.woodpecker/docker.yaml b/.woodpecker/docker.yaml index c1299b4fe..66efe6903 100644 --- a/.woodpecker/docker.yaml +++ b/.woodpecker/docker.yaml @@ -39,7 +39,7 @@ steps: tags: - latest-amd64 - develop-amd64 - - ${CI_COMMIT_SHA}-amd64 + - ${CI_COMMIT_SHA:0:8}-amd64 docker-develop-arm64: image: woodpeckerci/plugin-kaniko:2.3.1 @@ -50,7 +50,7 @@ steps: tags: - latest-arm64 - develop-arm64 - - ${CI_COMMIT_SHA}-arm64 + - ${CI_COMMIT_SHA:0:8}-arm64 docker-stable-amd64: image: *kaniko_image @@ -62,7 +62,7 @@ steps: tags: &amd64_tags - latest-amd64 - stable-amd64 - - ${CI_COMMIT_SHA}-amd64 + - ${CI_COMMIT_SHA:0:8}-amd64 docker-stable-tag-amd64: image: *kaniko_image @@ -85,7 +85,7 @@ steps: tags: &arm64_tags - latest-arm64 - stable-arm64 - - ${CI_COMMIT_SHA}-arm64 + - ${CI_COMMIT_SHA:0:8}-arm64 docker-stable-tag-arm64: image: *kaniko_image diff --git a/.woodpecker/otp-develop-musl.yaml b/.woodpecker/otp-develop-musl.yaml index 7ed567949..aff52ee30 100644 --- a/.woodpecker/otp-develop-musl.yaml +++ b/.woodpecker/otp-develop-musl.yaml @@ -44,7 +44,7 @@ steps: commands: - apk add git build-base cmake file-dev openssl vips-dev zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip release + - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip release upload-artifacts-amd64-musl: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -53,9 +53,9 @@ steps: settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-amd64-musl - package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl - file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip - file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip + package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl + file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip otp-develop-arm64-musl: image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 @@ -67,7 +67,7 @@ steps: commands: - apk add git build-base cmake file-dev openssl vips-dev zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip release + - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip release upload-artifacts-arm64-musl: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -76,6 +76,6 @@ steps: settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-arm64-musl - package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl - file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip - file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip + package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl + file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip diff --git a/.woodpecker/otp-develop.yaml b/.woodpecker/otp-develop.yaml index 6cd76d450..623d1d846 100644 --- a/.woodpecker/otp-develop.yaml +++ b/.woodpecker/otp-develop.yaml @@ -45,7 +45,7 @@ steps: commands: - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip release + - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip release upload-artifacts-amd64: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -54,9 +54,9 @@ steps: settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-amd64 - package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64 - file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip - file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip + package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64 + file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip + file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip otp-develop-arm64: image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 @@ -69,7 +69,7 @@ steps: commands: - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip release + - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip release upload-artifacts-arm64: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -78,6 +78,6 @@ steps: settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-arm64 - package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64 - file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip - file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip + package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64 + file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip + file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip diff --git a/.woodpecker/otp-stable-musl.yaml b/.woodpecker/otp-stable-musl.yaml index 96be0bf8e..3a3d8bb53 100644 --- a/.woodpecker/otp-stable-musl.yaml +++ b/.woodpecker/otp-stable-musl.yaml @@ -48,7 +48,7 @@ steps: commands: - apk add git build-base cmake file-dev openssl vips-dev zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip release + - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip release upload-artifacts-amd64-musl: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -57,9 +57,9 @@ steps: settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64-musl - package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl - file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip - file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip + package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl + file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip otp-stable-arm64-musl: image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 @@ -71,7 +71,7 @@ steps: commands: - apk add git build-base cmake file-dev openssl vips-dev zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip release + - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip release upload-artifacts-arm64-musl: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -80,6 +80,6 @@ steps: settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64-musl - package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl - file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip - file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip + package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl + file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip diff --git a/.woodpecker/otp-stable.yaml b/.woodpecker/otp-stable.yaml index ae2670a79..80bfc9f7e 100644 --- a/.woodpecker/otp-stable.yaml +++ b/.woodpecker/otp-stable.yaml @@ -49,7 +49,7 @@ steps: commands: - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip release + - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip release upload-artifacts-amd64: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -58,9 +58,9 @@ steps: settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64 - package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64 - file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip - file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip + package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64 + file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip + file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip otp-stable-arm64: image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 @@ -73,7 +73,7 @@ steps: commands: - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip release + - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip release upload-artifacts-arm64: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -82,6 +82,6 @@ steps: settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64 - package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64 - file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip - file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip + package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64 + file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip + file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip From 5229e8ae6531d4b78828ad43ce17e2e075080c04 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 21 Apr 2026 21:40:56 +0200 Subject: [PATCH 16/55] Woodpecker CI: Unify OTP builds into a single worfklow --- .woodpecker/otp-develop-musl.yaml | 81 ------------------ .woodpecker/otp-develop.yaml | 83 ------------------- .../{otp-stable-musl.yaml => otp-musl.yaml} | 72 +++++++++------- .woodpecker/{otp-stable.yaml => otp.yaml} | 75 ++++++++++------- 4 files changed, 87 insertions(+), 224 deletions(-) delete mode 100644 .woodpecker/otp-develop-musl.yaml delete mode 100644 .woodpecker/otp-develop.yaml rename .woodpecker/{otp-stable-musl.yaml => otp-musl.yaml} (56%) rename .woodpecker/{otp-stable.yaml => otp.yaml} (54%) diff --git a/.woodpecker/otp-develop-musl.yaml b/.woodpecker/otp-develop-musl.yaml deleted file mode 100644 index aff52ee30..000000000 --- a/.woodpecker/otp-develop-musl.yaml +++ /dev/null @@ -1,81 +0,0 @@ -when: - - event: push - branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - -matrix: - platform: - - linux/amd64 - - linux/arm64 - -# This is needed for the when clauses below. -# When the platform clause is fixed, this might not be needed anymore -labels: - platform: ${platform} - -variables: - pleroma_build_cmds: &pleroma_build_cmds - - echo "import Config" > config/prod.secret.exs - - mix local.hex --force - - mix local.rebar --force - - mix deps.get --only prod - - mkdir release - - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} - - mix release --path release - artifacts_uploader_settings: &artifacts_uploader_settings - user: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password - owner: 'pleroma' - -steps: - otp-develop-amd64-musl: - image: docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 - # when: - # - platform: linux/amd64 - # does not work even though it should according to docs - # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 - when: - - evaluate: platform == "linux/amd64" - environment: - MIX_ENV: prod - VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS - commands: - - apk add git build-base cmake file-dev openssl vips-dev zip - - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip release - - upload-artifacts-amd64-musl: - image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 - when: - - evaluate: platform == "linux/amd64" - settings: - <<: *artifacts_uploader_settings - package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-amd64-musl - package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl - file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip - file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip - - otp-develop-arm64-musl: - image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 - when: - - evaluate: platform == "linux/arm64" - environment: - MIX_ENV: prod - VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS - commands: - - apk add git build-base cmake file-dev openssl vips-dev zip - - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip release - - upload-artifacts-arm64-musl: - image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 - when: - - evaluate: platform == "linux/arm64" - settings: - <<: *artifacts_uploader_settings - package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-arm64-musl - package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl - file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip - file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip diff --git a/.woodpecker/otp-develop.yaml b/.woodpecker/otp-develop.yaml deleted file mode 100644 index 623d1d846..000000000 --- a/.woodpecker/otp-develop.yaml +++ /dev/null @@ -1,83 +0,0 @@ -when: - - event: push - branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - -matrix: - platform: - - linux/amd64 - - linux/arm64 - -# This is needed for the when clauses below. -# When the platform clause is fixed, this might not be needed anymore -labels: - platform: ${platform} - -variables: - pleroma_build_cmds: &pleroma_build_cmds - - echo "import Config" > config/prod.secret.exs - - mix local.hex --force - - mix local.rebar --force - - mix deps.get --only prod - - mkdir release - - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} - - mix release --path release - artifacts_uploader_settings: &artifacts_uploader_settings - user: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password - owner: 'pleroma' - -steps: - otp-develop-amd64: - image: docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 - # when: - # - platform: linux/amd64 - # does not work even though it should according to docs - # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 - when: - - evaluate: platform == "linux/amd64" - environment: - MIX_ENV: prod - VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS - DEBIAN_FRONTEND: noninteractive - commands: - - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip release - - upload-artifacts-amd64: - image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 - when: - - evaluate: platform == "linux/amd64" - settings: - <<: *artifacts_uploader_settings - package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-amd64 - package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64 - file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip - file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip - - otp-develop-arm64: - image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 - when: - - evaluate: platform == "linux/arm64" - environment: - MIX_ENV: prod - VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS - DEBIAN_FRONTEND: noninteractive - commands: - - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip release - - upload-artifacts-arm64: - image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 - when: - - evaluate: platform == "linux/arm64" - settings: - <<: *artifacts_uploader_settings - package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-arm64 - package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64 - file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip - file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip diff --git a/.woodpecker/otp-stable-musl.yaml b/.woodpecker/otp-musl.yaml similarity index 56% rename from .woodpecker/otp-stable-musl.yaml rename to .woodpecker/otp-musl.yaml index 3a3d8bb53..87ab0b93a 100644 --- a/.woodpecker/otp-stable-musl.yaml +++ b/.woodpecker/otp-musl.yaml @@ -1,4 +1,7 @@ when: + - event: push + branch: ${CI_REPO_DEFAULT_BRANCH} + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: push branch: stable path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] @@ -18,7 +21,8 @@ labels: platform: ${platform} variables: - pleroma_build_cmds: &pleroma_build_cmds + build_cmds: &build_cmds + - apk add git build-base cmake file-dev openssl vips-dev zip - echo "import Config" > config/prod.secret.exs - mix local.hex --force - mix local.rebar --force @@ -26,32 +30,54 @@ variables: - mkdir release - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} - mix release --path release + build_image_amd64: &build_image_amd64 docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 + build_image_arm64: &build_image_arm64 docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 + artifacts_uploader_image: &artifacts_uploader_image docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 artifacts_uploader_settings: &artifacts_uploader_settings user: from_secret: pleroma-ci-user password: from_secret: pleroma-ci-password - owner: 'pleroma-test' + owner: 'pleroma' + env: &env + MIX_ENV: prod + VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS steps: - otp-stable-amd64-musl: - image: docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 - # when: - # - platform: linux/amd64 - # does not work even though it should according to docs - # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 + otp-develop-amd64-musl: + image: *build_image_amd64 when: - - evaluate: platform == "linux/amd64" - environment: - MIX_ENV: prod - VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS - commands: - - apk add git build-base cmake file-dev openssl vips-dev zip - - <<: *pleroma_build_cmds + - evaluate: 'platform == "linux/amd64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + environment: *env + commands: &amd64_build + - <<: *build_cmds - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip release + otp-stable-amd64-musl: + image: *build_image_amd64 + when: + - evaluate: 'platform == "linux/amd64" && CI_COMMIT_BRANCH == "stable"' + environment: *env + commands: *amd64_build + + otp-develop-arm64-musl: + image: *build_image_arm64 + when: + - evaluate: 'platform == "linux/arm64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + environment: *env + commands: &arm64_build + - <<: *build_cmds + - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip release + + otp-stable-arm64-musl: + image: *build_image_arm64 + when: + - evaluate: 'platform == "linux/arm64" && CI_COMMIT_BRANCH == "stable"' + environment: *env + commands: *arm64_build + upload-artifacts-amd64-musl: - image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + image: *artifacts_uploader_image when: - evaluate: platform == "linux/amd64" settings: @@ -61,20 +87,8 @@ steps: file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip - otp-stable-arm64-musl: - image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 - when: - - evaluate: platform == "linux/arm64" - environment: - MIX_ENV: prod - VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS - commands: - - apk add git build-base cmake file-dev openssl vips-dev zip - - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip release - upload-artifacts-arm64-musl: - image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + image: *artifacts_uploader_image when: - evaluate: platform == "linux/arm64" settings: diff --git a/.woodpecker/otp-stable.yaml b/.woodpecker/otp.yaml similarity index 54% rename from .woodpecker/otp-stable.yaml rename to .woodpecker/otp.yaml index 80bfc9f7e..533143769 100644 --- a/.woodpecker/otp-stable.yaml +++ b/.woodpecker/otp.yaml @@ -1,4 +1,7 @@ when: + - event: push + branch: ${CI_REPO_DEFAULT_BRANCH} + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: push branch: stable path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] @@ -18,7 +21,8 @@ labels: platform: ${platform} variables: - pleroma_build_cmds: &pleroma_build_cmds + build_cmds: &build_cmds + - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - echo "import Config" > config/prod.secret.exs - mix local.hex --force - mix local.rebar --force @@ -26,33 +30,55 @@ variables: - mkdir release - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} - mix release --path release + build_image_amd64: &build_image_amd64 docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 + build_image_arm64: &build_image_arm64 docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 + artifacts_uploader_image: &artifacts_uploader_image docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 artifacts_uploader_settings: &artifacts_uploader_settings user: from_secret: pleroma-ci-user password: from_secret: pleroma-ci-password - owner: 'pleroma-test' + owner: 'pleroma' + env: &env + MIX_ENV: prod + VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS + DEBIAN_FRONTEND: noninteractive steps: - otp-stable-amd64: - image: docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 - # when: - # - platform: linux/amd64 - # does not work even though it should according to docs - # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 + otp-develop-amd64: + image: *build_image_amd64 when: - - evaluate: platform == "linux/amd64" - environment: - MIX_ENV: prod - VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS - DEBIAN_FRONTEND: noninteractive - commands: - - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - - <<: *pleroma_build_cmds + - evaluate: 'platform == "linux/amd64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + environment: *env + commands: &amd64_build + - <<: *build_cmds - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip release + otp-stable-amd64: + image: *build_image_amd64 + when: + - evaluate: 'platform == "linux/amd64" && CI_COMMIT_BRANCH == "stable"' + environment: *env + commands: *amd64_build + + otp-develop-arm64: + image: *build_image_arm64 + when: + - evaluate: 'platform == "linux/arm64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + environment: *env + commands: &arm64_build + - <<: *build_cmds + - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip release + + otp-stable-arm64: + image: *build_image_arm64 + when: + - evaluate: 'platform == "linux/arm64" && CI_COMMIT_BRANCH == "stable"' + environment: *env + commands: *arm64_build + upload-artifacts-amd64: - image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + image: *artifacts_uploader_image when: - evaluate: platform == "linux/amd64" settings: @@ -62,21 +88,8 @@ steps: file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip - otp-stable-arm64: - image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 - when: - - evaluate: platform == "linux/arm64" - environment: - MIX_ENV: prod - VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS - DEBIAN_FRONTEND: noninteractive - commands: - - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip release - upload-artifacts-arm64: - image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + image: *artifacts_uploader_image when: - evaluate: platform == "linux/arm64" settings: From 2e968890de91bcc7df982c7ad5bac7ee8621505f Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 22 Apr 2026 11:35:34 +0200 Subject: [PATCH 17/55] Woodpecker CI: Remove branch requirement for tag Tag events don't have CI_COMMIT_BRANCH set, and neither can they be restricted to specific branches. The branch condition is ignored on tags. --- .woodpecker/docker-combine.yaml | 1 - .woodpecker/docker.yaml | 5 ++- .woodpecker/otp-musl.yaml | 59 ++++++++++++++++++++++++++++++--- .woodpecker/otp.yaml | 59 ++++++++++++++++++++++++++++++--- 4 files changed, 110 insertions(+), 14 deletions(-) diff --git a/.woodpecker/docker-combine.yaml b/.woodpecker/docker-combine.yaml index c69bcf1cf..e615ca7c2 100644 --- a/.woodpecker/docker-combine.yaml +++ b/.woodpecker/docker-combine.yaml @@ -6,7 +6,6 @@ when: branch: stable path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: tag - branch: stable - event: manual branch: stable diff --git a/.woodpecker/docker.yaml b/.woodpecker/docker.yaml index 66efe6903..0c4b18fbc 100644 --- a/.woodpecker/docker.yaml +++ b/.woodpecker/docker.yaml @@ -6,7 +6,6 @@ when: branch: stable path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: tag - branch: stable - event: manual branch: stable @@ -67,7 +66,7 @@ steps: docker-stable-tag-amd64: image: *kaniko_image when: - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG != ""' settings: <<: *docker_variables @@ -90,7 +89,7 @@ steps: docker-stable-tag-arm64: image: *kaniko_image when: - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG != ""' settings: <<: *docker_variables diff --git a/.woodpecker/otp-musl.yaml b/.woodpecker/otp-musl.yaml index 87ab0b93a..d611f9f13 100644 --- a/.woodpecker/otp-musl.yaml +++ b/.woodpecker/otp-musl.yaml @@ -6,7 +6,6 @@ when: branch: stable path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: tag - branch: stable - event: manual branch: stable @@ -56,10 +55,21 @@ steps: otp-stable-amd64-musl: image: *build_image_amd64 when: - - evaluate: 'platform == "linux/amd64" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"' environment: *env commands: *amd64_build + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + otp-stable-tag-amd64-musl: + image: *build_image_amd64 + when: + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' + environment: *env + commands: + - <<: *build_cmds + - zip -9rq pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip release + otp-develop-arm64-musl: image: *build_image_arm64 when: @@ -72,14 +82,27 @@ steps: otp-stable-arm64-musl: image: *build_image_arm64 when: - - evaluate: 'platform == "linux/arm64" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"' environment: *env commands: *arm64_build + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + otp-stable-tag-arm64-musl: + image: *build_image_arm64 + when: + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' + environment: *env + commands: + - <<: *build_cmds + - zip -9rq pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip release + upload-artifacts-amd64-musl: image: *artifacts_uploader_image when: - - evaluate: platform == "linux/amd64" + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"' settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64-musl @@ -87,13 +110,39 @@ steps: file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-artifacts-tag-amd64-musl: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-amd64-musl + package_version: stable-${CI_COMMIT_SHA:0:8}-amd64-musl + file_source: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + upload-artifacts-arm64-musl: image: *artifacts_uploader_image when: - - evaluate: platform == "linux/arm64" + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"' settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64-musl package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-artifacts-tag-arm64-musl: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-arm64-musl + package_version: stable-${CI_COMMIT_SHA:0:8}-arm64-musl + file_source: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip diff --git a/.woodpecker/otp.yaml b/.woodpecker/otp.yaml index 533143769..0d0b08bec 100644 --- a/.woodpecker/otp.yaml +++ b/.woodpecker/otp.yaml @@ -6,7 +6,6 @@ when: branch: stable path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: tag - branch: stable - event: manual branch: stable @@ -57,10 +56,21 @@ steps: otp-stable-amd64: image: *build_image_amd64 when: - - evaluate: 'platform == "linux/amd64" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"' environment: *env commands: *amd64_build + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + otp-stable-tag-amd64: + image: *build_image_amd64 + when: + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' + environment: *env + commands: + - <<: *build_cmds + - zip -9rq pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64.zip release + otp-develop-arm64: image: *build_image_arm64 when: @@ -73,14 +83,27 @@ steps: otp-stable-arm64: image: *build_image_arm64 when: - - evaluate: 'platform == "linux/arm64" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"' environment: *env commands: *arm64_build + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + otp-stable-tag-arm64: + image: *build_image_arm64 + when: + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' + environment: *env + commands: + - <<: *build_cmds + - zip -9rq pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64.zip release + upload-artifacts-amd64: image: *artifacts_uploader_image when: - - evaluate: platform == "linux/amd64" + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"' settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64 @@ -88,13 +111,39 @@ steps: file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-artifacts-tag-amd64: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-amd64 + package_version: stable-${CI_COMMIT_SHA:0:8}-amd64 + file_source: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64.zip + file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64.zip + upload-artifacts-arm64: image: *artifacts_uploader_image when: - - evaluate: platform == "linux/arm64" + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"' settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64 package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64 file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-artifacts-tag-arm64: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-arm64 + package_version: stable-${CI_COMMIT_SHA:0:8}-arm64 + file_source: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64.zip + file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64.zip From 16b7a95c481233e2b4e02e90128fedce817852f8 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 22 Apr 2026 15:31:46 +0200 Subject: [PATCH 18/55] Woodpecker CI: Run Docker image workflows also on Dockerfile changes --- .woodpecker/docker-combine.yaml | 4 ++-- .woodpecker/docker.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.woodpecker/docker-combine.yaml b/.woodpecker/docker-combine.yaml index e615ca7c2..147cb1d55 100644 --- a/.woodpecker/docker-combine.yaml +++ b/.woodpecker/docker-combine.yaml @@ -1,10 +1,10 @@ when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - event: push branch: stable - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - event: tag - event: manual branch: stable diff --git a/.woodpecker/docker.yaml b/.woodpecker/docker.yaml index 0c4b18fbc..f178fb840 100644 --- a/.woodpecker/docker.yaml +++ b/.woodpecker/docker.yaml @@ -1,10 +1,10 @@ when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - event: push branch: stable - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - event: tag - event: manual branch: stable From 209b9c0a1e9bd7c822213f1a4cad745605ae31b2 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 22 Apr 2026 21:04:31 +0200 Subject: [PATCH 19/55] Woodpecker CI: Shorten zip archive names further Hopefully this will also help with the workflows randomly failing to create the zip archive due to the Woodpecker bug. --- .woodpecker/otp-musl.yaml | 16 ++++++++-------- .woodpecker/otp.yaml | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.woodpecker/otp-musl.yaml b/.woodpecker/otp-musl.yaml index d611f9f13..44d44a662 100644 --- a/.woodpecker/otp-musl.yaml +++ b/.woodpecker/otp-musl.yaml @@ -50,7 +50,7 @@ steps: environment: *env commands: &amd64_build - <<: *build_cmds - - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip release + - zip -9rq ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip release otp-stable-amd64-musl: image: *build_image_amd64 @@ -68,7 +68,7 @@ steps: environment: *env commands: - <<: *build_cmds - - zip -9rq pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip release + - zip -9rq stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip release otp-develop-arm64-musl: image: *build_image_arm64 @@ -77,7 +77,7 @@ steps: environment: *env commands: &arm64_build - <<: *build_cmds - - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip release + - zip -9rq ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip release otp-stable-arm64-musl: image: *build_image_arm64 @@ -95,7 +95,7 @@ steps: environment: *env commands: - <<: *build_cmds - - zip -9rq pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip release + - zip -9rq stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip release upload-artifacts-amd64-musl: image: *artifacts_uploader_image @@ -107,7 +107,7 @@ steps: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64-musl package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl - file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable @@ -119,7 +119,7 @@ steps: <<: *artifacts_uploader_settings package_name: pleroma-otp-stable-amd64-musl package_version: stable-${CI_COMMIT_SHA:0:8}-amd64-musl - file_source: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + file_source: ./stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip upload-artifacts-arm64-musl: @@ -132,7 +132,7 @@ steps: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64-musl package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl - file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable @@ -144,5 +144,5 @@ steps: <<: *artifacts_uploader_settings package_name: pleroma-otp-stable-arm64-musl package_version: stable-${CI_COMMIT_SHA:0:8}-arm64-musl - file_source: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip diff --git a/.woodpecker/otp.yaml b/.woodpecker/otp.yaml index 0d0b08bec..008c77f95 100644 --- a/.woodpecker/otp.yaml +++ b/.woodpecker/otp.yaml @@ -51,7 +51,7 @@ steps: environment: *env commands: &amd64_build - <<: *build_cmds - - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip release + - zip -9rq ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip release otp-stable-amd64: image: *build_image_amd64 @@ -69,7 +69,7 @@ steps: environment: *env commands: - <<: *build_cmds - - zip -9rq pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64.zip release + - zip -9rq stable-${CI_COMMIT_SHA:0:8}-amd64.zip release otp-develop-arm64: image: *build_image_arm64 @@ -78,7 +78,7 @@ steps: environment: *env commands: &arm64_build - <<: *build_cmds - - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip release + - zip -9rq ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip release otp-stable-arm64: image: *build_image_arm64 @@ -96,7 +96,7 @@ steps: environment: *env commands: - <<: *build_cmds - - zip -9rq pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64.zip release + - zip -9rq stable-${CI_COMMIT_SHA:0:8}-arm64.zip release upload-artifacts-amd64: image: *artifacts_uploader_image @@ -108,7 +108,7 @@ steps: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64 package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64 - file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable @@ -120,7 +120,7 @@ steps: <<: *artifacts_uploader_settings package_name: pleroma-otp-stable-amd64 package_version: stable-${CI_COMMIT_SHA:0:8}-amd64 - file_source: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64.zip + file_source: ./stable-${CI_COMMIT_SHA:0:8}-amd64.zip file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64.zip upload-artifacts-arm64: @@ -133,7 +133,7 @@ steps: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64 package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64 - file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable @@ -145,5 +145,5 @@ steps: <<: *artifacts_uploader_settings package_name: pleroma-otp-stable-arm64 package_version: stable-${CI_COMMIT_SHA:0:8}-arm64 - file_source: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64.zip + file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm64.zip file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64.zip From 7f97e21910ea9435febda3c36a67a467af8e2993 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 24 Apr 2026 15:04:12 +0200 Subject: [PATCH 20/55] pleroma_ctl: Properly handle user arguments with whitespace When user supplied arguments to pleroma_ctl include whitespace that has been properly quoted, all arguments were sent to ReleaseTasks in one string, which then String.split/1 the input on any whitespace. This broke Mix tasks that accept certain user input like instance gen and user management. To fix this, pleroma_ctl now sends the arguments in list form. Additionally pleroma_ctl arguments now need to be pre-processed. Fixes pleroma/pleroma#7874 --- lib/pleroma/release_tasks.ex | 19 ++++++++++++++++++- rel/files/bin/pleroma_ctl | 10 +++++++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/release_tasks.ex b/lib/pleroma/release_tasks.ex index af2d35c8f..49400940f 100644 --- a/lib/pleroma/release_tasks.ex +++ b/lib/pleroma/release_tasks.ex @@ -5,7 +5,10 @@ defmodule Pleroma.ReleaseTasks do @repo Pleroma.Repo - def run(args) do + # TODO: Kept for some backwards compatibility with buggy pleroma_ctl, + # if a mismatch between pleroma_ctl and Pleroma accidentaly happens. + # Remove in the future. + def run(args) when is_binary(args) do [task | args] = String.split(args) case task do @@ -16,6 +19,20 @@ defmodule Pleroma.ReleaseTasks do end end + # HACK: Script arguments need to be received as a list, otherwise (quoted) arguments with + # whitespace will be broken. Previously the broken string form above was used, + # escaping in the shell does not help. + def run(args) when is_list(args) do + [task | args] = args + + case task do + "migrate" -> migrate(args) + "create" -> create() + "rollback" -> rollback(args) + task -> mix_task(task, args) + end + end + def find_module(task) do module_name = task diff --git a/rel/files/bin/pleroma_ctl b/rel/files/bin/pleroma_ctl index 6f0dba3a8..56dc13a06 100755 --- a/rel/files/bin/pleroma_ctl +++ b/rel/files/bin/pleroma_ctl @@ -137,7 +137,11 @@ else SCRIPT=$(realpath "$0") SCRIPTPATH=$(dirname "$SCRIPT") - FULL_ARGS="$*" + # HACK: Script arguments need to be sent as an array to Mix tasks, otherwise they will break (quoted) arguments with whitespace. + # Previously it was sent as string, which would get split on whitespace on the task side. + # Escaping does not help including non-POSIX printf %q + PREPARED_ARGS="" + for arg in "$@"; do PREPARED_ARGS="$PREPARED_ARGS \"$arg\","; done ACTION="$1" if [ $# -gt 0 ]; then @@ -154,8 +158,8 @@ else if [ "$ACTION" = "update" ]; then update "$@" elif [ "$ACTION" = "migrate" ] || [ "$ACTION" = "rollback" ] || [ "$ACTION" = "create" ] || [ "$ACTION $SUBACTION" = "instance gen" ] || [ "$PLEROMA_CTL_RPC_DISABLED" = true ]; then - "$SCRIPTPATH"/pleroma eval 'Pleroma.ReleaseTasks.run("'"$FULL_ARGS"'")' + "$SCRIPTPATH"/pleroma eval 'Pleroma.ReleaseTasks.run(['"${PREPARED_ARGS%%,}"'])' else - "$SCRIPTPATH"/pleroma rpc 'Pleroma.ReleaseTasks.run("'"$FULL_ARGS"'")' + "$SCRIPTPATH"/pleroma rpc 'Pleroma.ReleaseTasks.run(['"${PREPARED_ARGS%%,}"'])' fi fi From 95a33855d1b0363057112d3470dfb5d980c58eb5 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 24 Apr 2026 22:02:01 +0200 Subject: [PATCH 21/55] pleroma_ctl: Update update logic to Gitea API --- rel/files/bin/pleroma_ctl | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/rel/files/bin/pleroma_ctl b/rel/files/bin/pleroma_ctl index 56dc13a06..2e114ff50 100755 --- a/rel/files/bin/pleroma_ctl +++ b/rel/files/bin/pleroma_ctl @@ -78,12 +78,17 @@ update() { RELEASE_ROOT=$(dirname "$SCRIPTPATH") uri="https://git.pleroma.social" - project_id="2" + project_name="pleroma" + api_base="${uri}/api/v1/packages/${project_name}/generic" + package_base="${uri}/api/packages/${project_name}/generic" project_branch="${BRANCH:-$(detect_branch)}" flavour="${FLAVOUR:-$(detect_flavour)}" + # API responds in JSON, optimistically try to make it one object per line + ver=$(curl -s "${api_base}"/pleroma-otp-"${project_branch}"-"${flavour}"/-/latest | tr ',' '\n' | grep '"version":' | cut -d':' -f2 | tr -d '"') + file=$(curl -s "${api_base}"/pleroma-otp-"${project_branch}"-"${flavour}"/"${ver}"/files | tr ',' '\n' | grep '"name":' | cut -d':' -f2 | tr -d '"') + full_uri=${FULL_URI:-"${package_base}"/pleroma-otp-"${project_branch}"-"${flavour}"/"${ver}"/"${file}"} tmp="${TMP_DIR:-/tmp}" artifact="$tmp/pleroma.zip" - full_uri="${FULL_URI:-${uri}/api/v4/projects/${project_id}/jobs/artifacts/${project_branch}/download?job=${flavour}}" echo "Downloading the artifact from ${full_uri} to ${artifact}" curl "$full_uri" -o "${artifact}" echo "Unpacking ${artifact} to ${tmp}" From cafd75b072ef408cc30ddaecdd003827274cdcf8 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 24 Apr 2026 23:15:00 +0200 Subject: [PATCH 22/55] Woodpecker CI docker-combine: Hoist docker_settings anchor --- .woodpecker/docker-combine.yaml | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/.woodpecker/docker-combine.yaml b/.woodpecker/docker-combine.yaml index 147cb1d55..6fa9583ba 100644 --- a/.woodpecker/docker-combine.yaml +++ b/.woodpecker/docker-combine.yaml @@ -20,7 +20,7 @@ steps: when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} - settings: + settings: &docker_settings registry: "git.pleroma.social" image: "pleroma/pleroma" architectures: [amd64, arm64] @@ -39,18 +39,12 @@ steps: - event: push branch: stable - evaluate: 'CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG == ""' - settings: &docker_settings - registry: "git.pleroma.social" - image: "pleroma/pleroma" - architectures: [amd64, arm64] - tags: &docker_tags + settings: + <<: *docker_settings + tags: &stable_docker_tags - latest - stable - ${CI_COMMIT_SHA:0:8} - username: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password docker-stable-tag-combine: image: git.fluffytail.org/phnt/wpc-docker-tagger:latest @@ -60,5 +54,5 @@ steps: settings: <<: *docker_settings tags: - - <<: *docker_tags + - <<: *stable_docker_tags - ${CI_COMMIT_TAG} From 25e543d44d7846125b773ed81b59dcc27137afd5 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 24 Apr 2026 23:38:29 +0200 Subject: [PATCH 23/55] changelog --- changelog.d/woodpecker-release-pipeline.skip | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 changelog.d/woodpecker-release-pipeline.skip diff --git a/changelog.d/woodpecker-release-pipeline.skip b/changelog.d/woodpecker-release-pipeline.skip new file mode 100644 index 000000000..e69de29bb From a996d25b8459b5a8f369430106950a2ee9bdf881 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 25 Apr 2026 11:08:28 +0200 Subject: [PATCH 24/55] Woodpecker CI Docker: label workflow as high memory --- .woodpecker/docker.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.woodpecker/docker.yaml b/.woodpecker/docker.yaml index f178fb840..703f3bbfc 100644 --- a/.woodpecker/docker.yaml +++ b/.woodpecker/docker.yaml @@ -17,6 +17,7 @@ matrix: # This is needed for the when clauses below. labels: platform: ${platform} + memory: 'high' variables: docker_variables: &docker_variables From e4632eced36c368ec62f38bb4e05bc4cc6602559 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 25 Apr 2026 13:33:58 +0200 Subject: [PATCH 25/55] Woodpecker CI: Only run stable release pipelines on tag events Removes possible races when uploading images/bundles and purposeful pipeline failures when both a push and tag happened (OTP bundles do not allow overwriting). --- .woodpecker/docker-combine.yaml | 5 ----- .woodpecker/docker.yaml | 5 ----- .woodpecker/otp-musl.yaml | 11 ++--------- .woodpecker/otp.yaml | 11 ++--------- 4 files changed, 4 insertions(+), 28 deletions(-) diff --git a/.woodpecker/docker-combine.yaml b/.woodpecker/docker-combine.yaml index 6fa9583ba..be8583763 100644 --- a/.woodpecker/docker-combine.yaml +++ b/.woodpecker/docker-combine.yaml @@ -2,9 +2,6 @@ when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - - event: push - branch: stable - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - event: tag - event: manual branch: stable @@ -36,8 +33,6 @@ steps: docker-stable-combine: image: git.fluffytail.org/phnt/wpc-docker-tagger:latest when: - - event: push - branch: stable - evaluate: 'CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG == ""' settings: <<: *docker_settings diff --git a/.woodpecker/docker.yaml b/.woodpecker/docker.yaml index 703f3bbfc..317cb5fbb 100644 --- a/.woodpecker/docker.yaml +++ b/.woodpecker/docker.yaml @@ -2,9 +2,6 @@ when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - - event: push - branch: stable - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - event: tag - event: manual branch: stable @@ -55,7 +52,6 @@ steps: docker-stable-amd64: image: *kaniko_image when: - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG == ""' settings: <<: *docker_variables @@ -78,7 +74,6 @@ steps: docker-stable-arm64: image: *kaniko_image when: - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG == ""' settings: <<: *docker_variables diff --git a/.woodpecker/otp-musl.yaml b/.woodpecker/otp-musl.yaml index 44d44a662..c58eee66b 100644 --- a/.woodpecker/otp-musl.yaml +++ b/.woodpecker/otp-musl.yaml @@ -2,9 +2,6 @@ when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - - event: push - branch: stable - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: tag - event: manual branch: stable @@ -55,8 +52,7 @@ steps: otp-stable-amd64-musl: image: *build_image_amd64 when: - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' environment: *env commands: *amd64_build @@ -82,8 +78,7 @@ steps: otp-stable-arm64-musl: image: *build_image_arm64 when: - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' environment: *env commands: *arm64_build @@ -101,7 +96,6 @@ steps: image: *artifacts_uploader_image when: - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"' settings: <<: *artifacts_uploader_settings @@ -126,7 +120,6 @@ steps: image: *artifacts_uploader_image when: - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"' settings: <<: *artifacts_uploader_settings diff --git a/.woodpecker/otp.yaml b/.woodpecker/otp.yaml index 008c77f95..c01c2e557 100644 --- a/.woodpecker/otp.yaml +++ b/.woodpecker/otp.yaml @@ -2,9 +2,6 @@ when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - - event: push - branch: stable - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: tag - event: manual branch: stable @@ -56,8 +53,7 @@ steps: otp-stable-amd64: image: *build_image_amd64 when: - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' environment: *env commands: *amd64_build @@ -83,8 +79,7 @@ steps: otp-stable-arm64: image: *build_image_arm64 when: - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' environment: *env commands: *arm64_build @@ -102,7 +97,6 @@ steps: image: *artifacts_uploader_image when: - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"' settings: <<: *artifacts_uploader_settings @@ -127,7 +121,6 @@ steps: image: *artifacts_uploader_image when: - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"' settings: <<: *artifacts_uploader_settings From cb2271978ec55c0714d6396f73394957b3704abf Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 30 Apr 2026 00:17:59 +0200 Subject: [PATCH 26/55] UpdateValidator: fix tests --- .../object_validators/update_handling_test.exs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs b/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs index 347c5b578..94c502ad6 100644 --- a/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs @@ -29,7 +29,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do assert {:ok, _update, []} = ObjectValidator.validate(valid_update, []) end - test "returns an error if the object can't be updated by the actor", %{ + test "returns an error if the object can't be updated by the actor (different domain)", %{ valid_update: valid_update } do other_user = insert(:user, local: false) @@ -41,24 +41,26 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do assert {:error, _cng} = ObjectValidator.validate(update, []) end - test "validates as long as the object is same-origin with the actor", %{ + test "returns an error if the object can't be updated by the actor (same domain)", %{ + user: user, valid_update: valid_update } do - other_user = insert(:user) + user_ap_id = user.ap_id + user_domain = URI.parse(user_ap_id).host + other_user = insert(:user, local: false, domain: user_domain) update = valid_update |> Map.put("actor", other_user.ap_id) - assert {:ok, _update, []} = ObjectValidator.validate(update, []) + assert {:error, _cng} = ObjectValidator.validate(update, []) end - test "validates if the object is not of an Actor type" do - note = insert(:note) + test "validates if the object is not of an Actor type", %{user: user} do + note = insert(:note, user: user) updated_note = note.data |> Map.put("content", "edited content") - other_user = insert(:user) - {:ok, update, _} = Builder.update(other_user, updated_note) + {:ok, update, _} = Builder.update(user, updated_note) assert {:ok, _update, _} = ObjectValidator.validate(update, []) end From af6d12c0a5be10291c9e2bc73cb29bc24c29115a Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 30 Apr 2026 00:18:54 +0200 Subject: [PATCH 27/55] UpdateValidator: Check Actor owns Object or updates itself --- .../object_validators/update_validator.ex | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex index aab90235f..5586b74cf 100644 --- a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex @@ -75,15 +75,36 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do end end - # For remote Updates, verify the host is the same. + # For remote Updates, verify the Actor is the same def validate_updating_rights_remote(cng) do with actor = get_field(cng, :actor), object = get_field(cng, :object), {:ok, object_id} <- ObjectValidators.ObjectID.cast(object), - actor_uri <- URI.parse(actor), - object_uri <- URI.parse(object_id), - true <- actor_uri.host == object_uri.host do - cng + entity <- + Object.normalize(object_id, fetch: false) || User.get_cached_by_ap_id(object_id) do + case entity do + # Actor must own Object to update it + %Object{} -> + if actor == entity.data["actor"] do + cng + else + cng + |> add_error(:object, "Can't be updated by this actor") + end + + # Actor must only be allowed to update itself + %User{} -> + if actor == entity.ap_id do + cng + else + cng + |> add_error(:object, "Can't be updated by this actor") + end + + true -> + cng + |> add_error(:object, "Update is neither for Object or Actor") + end else _e -> cng From da28a4c44109f1af944d23b87e16ea661b76d868 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 30 Apr 2026 00:58:43 +0200 Subject: [PATCH 28/55] ReceiverWorker: Add cancels on actor does not match signature test --- test/pleroma/workers/receiver_worker_test.exs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/test/pleroma/workers/receiver_worker_test.exs b/test/pleroma/workers/receiver_worker_test.exs index 12abc1a27..1c886a74e 100644 --- a/test/pleroma/workers/receiver_worker_test.exs +++ b/test/pleroma/workers/receiver_worker_test.exs @@ -302,4 +302,48 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end end end + + test "cancels when signature actor does not match payload actor" do + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + + note = insert(:note, user: bob, object_local: false) + + update = %{ + "type" => "Update", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/malicious-update", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data + } + + req_headers = [ + ["host", "example.com"], + ["date", "Thu, 25 Jul 2024 13:33:31 GMT"], + ["digest", "SHA-256=fake-digest"], + ["content-type", "application/activity+json"], + [ + "signature", + "keyId=\"https://example.com/users/alice#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" + ] + ] + + oban_job = %Oban.Job{ + args: %{ + "op" => "incoming_ap_doc", + "method" => "POST", + "params" => update, + "req_headers" => req_headers, + "request_path" => "/inbox", + "query_string" => "" + } + } + + with_mock Pleroma.Signature, [:passthrough], + refetch_public_key: fn _conn -> {:ok, :fake_public_key} end, + validate_signature: fn _conn -> true end do + assert {:cancel, :invalid_signature} = ReceiverWorker.perform(oban_job) + end + end end From 42683e79dfe89651d8f44feef6659a5ceaa78183 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 30 Apr 2026 01:34:14 +0200 Subject: [PATCH 29/55] ReceiverWorker: Check that signature matches actor --- lib/pleroma/workers/receiver_worker.ex | 14 ++++++++++++++ test/pleroma/workers/receiver_worker_test.exs | 4 ++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/workers/receiver_worker.ex b/lib/pleroma/workers/receiver_worker.ex index e2c950967..507e099c2 100644 --- a/lib/pleroma/workers/receiver_worker.ex +++ b/lib/pleroma/workers/receiver_worker.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Workers.ReceiverWorker do alias Pleroma.Signature alias Pleroma.User alias Pleroma.Web.Federator + alias Pleroma.Web.Plugs.MappedSignatureToIdentityPlug use Oban.Worker, queue: :federator_incoming, max_attempts: 5, unique: [period: :infinity] @@ -27,6 +28,7 @@ defmodule Pleroma.Workers.ReceiverWorker do req_headers = Enum.into(req_headers, [], &List.to_tuple(&1)) conn_data = %Plug.Conn{ + assigns: %{valid_signature: true}, method: method, params: params, req_headers: req_headers, @@ -37,6 +39,7 @@ defmodule Pleroma.Workers.ReceiverWorker do with {:ok, %User{}} <- User.get_or_fetch_by_ap_id(conn_data.params["actor"]), {:ok, _public_key} <- Signature.refetch_public_key(conn_data), {:signature, true} <- {:signature, Signature.validate_signature(conn_data)}, + {:same_actor, true} <- {:same_actor, validate_same_actor(conn_data)}, {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do unless Instances.reachable?(params["actor"]) do domain = URI.parse(params["actor"]).host @@ -67,6 +70,16 @@ defmodule Pleroma.Workers.ReceiverWorker do def timeout(_job), do: :timer.seconds(5) + defp validate_same_actor(conn_data) do + case MappedSignatureToIdentityPlug.call(conn_data, []) do + %Plug.Conn{assigns: %{valid_signature: true}} -> + true + + _ -> + false + end + end + defp process_errors({:error, {:error, _} = error}), do: process_errors(error) defp process_errors(errors) do @@ -85,6 +98,7 @@ defmodule Pleroma.Workers.ReceiverWorker do {:error, {:reject, _} = reason} -> {:cancel, reason} # HTTP Sigs {:signature, false} -> {:cancel, :invalid_signature} + {:same_actor, false} -> {:cancel, :actor_signature_mismatch} # Origin / URL validation failed somewhere possibly due to spoofing {:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed} # Unclear if this can be reached diff --git a/test/pleroma/workers/receiver_worker_test.exs b/test/pleroma/workers/receiver_worker_test.exs index 1c886a74e..bc027ad4c 100644 --- a/test/pleroma/workers/receiver_worker_test.exs +++ b/test/pleroma/workers/receiver_worker_test.exs @@ -304,7 +304,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels when signature actor does not match payload actor" do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") note = insert(:note, user: bob, object_local: false) @@ -343,7 +343,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do with_mock Pleroma.Signature, [:passthrough], refetch_public_key: fn _conn -> {:ok, :fake_public_key} end, validate_signature: fn _conn -> true end do - assert {:cancel, :invalid_signature} = ReceiverWorker.perform(oban_job) + assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) end end end From 80e72b79f57bad270c530d94527f760c14d8c152 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 30 Apr 2026 14:31:06 +0400 Subject: [PATCH 30/55] Add spoofing regression tests --- .gitignore | 3 + .../activity_pub_controller_test.exs | 68 +++++ .../update_handling_test.exs | 26 ++ ...mapped_signature_to_identity_plug_test.exs | 10 +- test/pleroma/workers/receiver_worker_test.exs | 239 ++++++++++++++++++ 5 files changed, 342 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 355cea069..d8e5ed553 100644 --- a/.gitignore +++ b/.gitignore @@ -56,6 +56,9 @@ pleroma.iml # asdf .tool-versions +# mise +mise.toml + # Editor temp files *~ *# diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index d5947186f..42cb35669 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -726,6 +726,74 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert Activity.get_by_ap_id(data["id"]) end + test "does not create a forged post after failed signature retry", %{conn: conn} do + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + object_id = "https://example.com/objects/inbox-forged-note" + + data = %{ + "type" => "Create", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/inbox-forged-create", + "context" => "https://example.com/contexts/inbox-forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => object_id, + "actor" => bob.ap_id, + "attributedTo" => bob.ap_id, + "context" => "https://example.com/contexts/inbox-forged-create", + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + conn = + conn + |> assign(:valid_signature, false) + |> put_req_header("content-type", "application/activity+json") + |> put_req_header("signature", "keyId=\"https://example.com/users/alice#main-key\"") + |> post("/inbox", data) + + assert "ok" == json_response(conn, 200) + + assert [{:cancel, :actor_signature_mismatch}] = + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + + refute Activity.get_by_ap_id(data["id"]) + refute Object.get_by_ap_id(object_id) + end + + test "does not create a forged like after failed signature retry", %{conn: conn} do + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + note = insert(:note) + + data = %{ + "type" => "Like", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/inbox-forged-like", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data["id"] + } + + conn = + conn + |> assign(:valid_signature, false) + |> put_req_header("content-type", "application/activity+json") + |> put_req_header("signature", "keyId=\"https://example.com/users/alice#main-key\"") + |> post("/inbox", data) + + assert "ok" == json_response(conn, 200) + + assert [{:cancel, :actor_signature_mismatch}] = + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + + refute Activity.get_by_ap_id(data["id"]) + end + test "accept follow activity", %{conn: conn} do clear_config([:instance, :federating], true) relay = Relay.get_actor() diff --git a/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs b/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs index 94c502ad6..f04f9cc61 100644 --- a/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs @@ -64,6 +64,32 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do assert {:ok, _update, _} = ObjectValidator.validate(update, []) end + + test "returns an error if the remote update target is unknown" do + remote_user = insert(:user, local: false, ap_id: "https://example.com/users/alice") + + update = %{ + "type" => "Update", + "actor" => remote_user.ap_id, + "id" => "https://example.com/activities/update-unknown-object", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => "https://example.com/objects/unknown", + "actor" => remote_user.ap_id, + "content" => "edited content", + "published" => "2024-07-25T13:33:31Z", + "updated" => "2024-07-25T13:34:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + assert {:error, %Ecto.Changeset{} = cng} = ObjectValidator.validate(update, local: false) + refute cng.valid? + assert Keyword.has_key?(cng.errors, :object) + end end describe "update note" do diff --git a/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs b/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs index 33eff1bc5..81c6b0c5d 100644 --- a/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs +++ b/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs @@ -47,13 +47,15 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlugTest do assert %{valid_signature: false} == conn.assigns end - @tag skip: "known breakage; the testsuite presently depends on it" test "it considers a mapped identity to be invalid when the identity cannot be found" do + actor = "http://niu.moe/users/rye" + conn = - build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"}) - |> set_signature("http://niu.moe/users/rye") + build_conn(:post, "/doesntmattter", %{"actor" => actor}) + |> set_signature(actor) |> MappedSignatureToIdentityPlug.call(%{}) - assert %{valid_signature: false} == conn.assigns + assert conn.assigns.valid_signature == false + refute Map.has_key?(conn.assigns, :user) end end diff --git a/test/pleroma/workers/receiver_worker_test.exs b/test/pleroma/workers/receiver_worker_test.exs index bc027ad4c..9dccd739b 100644 --- a/test/pleroma/workers/receiver_worker_test.exs +++ b/test/pleroma/workers/receiver_worker_test.exs @@ -14,6 +14,43 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do alias Pleroma.Web.Federator alias Pleroma.Workers.ReceiverWorker + defp mismatched_signature_headers do + [ + {"host", "example.com"}, + {"date", "Thu, 25 Jul 2024 13:33:31 GMT"}, + {"digest", "SHA-256=fake-digest"}, + {"content-type", "application/activity+json"}, + { + "signature", + "keyId=\"https://example.com/users/alice#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" + } + ] + end + + defp assert_mismatched_signature_cancelled(params) do + with_mocks [ + {Pleroma.Signature, [:passthrough], + [ + refetch_public_key: fn _conn -> {:ok, :fake_public_key} end, + validate_signature: fn _conn -> true end + ]}, + {Pleroma.Web.Federator, [:passthrough], + [perform: fn :incoming_ap_doc, _params -> {:ok, :processed} end]} + ] do + assert {:ok, oban_job} = + Federator.incoming_ap_doc(%{ + method: "POST", + req_headers: mismatched_signature_headers(), + request_path: "/inbox", + params: params, + query_string: "" + }) + + assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) + refute called(Pleroma.Web.Federator.perform(:incoming_ap_doc, :_)) + end + end + test "it does not retry MRF reject" do params = insert(:note).data @@ -346,4 +383,206 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) end end + + test "Federator preserves request metadata needed for ReceiverWorker signature checks" do + params = insert(:note_activity).data + + req_headers = [ + {"host", "example.com"}, + {"signature", "keyId=\"https://example.com/users/alice#main-key\""} + ] + + assert {:ok, oban_job} = + Federator.incoming_ap_doc(%{ + method: "POST", + req_headers: req_headers, + request_path: "/inbox", + params: params, + query_string: "foo=bar" + }) + + assert %{ + "method" => "POST", + "req_headers" => ^req_headers, + "request_path" => "/inbox", + "params" => ^params, + "query_string" => "foo=bar" + } = oban_job.args + end + + test "cancels signature actor mismatch through Federator-created jobs" do + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + + note = insert(:note, user: bob, object_local: false) + + update = %{ + "type" => "Update", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/federator-malicious-update", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data + } + + assert_mismatched_signature_cancelled(update) + end + + test "cancels signature actor mismatch before processing a forged Create" do + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + + create = %{ + "type" => "Create", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => "https://example.com/objects/forged-note", + "actor" => bob.ap_id, + "attributedTo" => bob.ap_id, + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + assert_mismatched_signature_cancelled(create) + end + + test "cancels signature actor mismatch before actually creating a forged post" do + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + + object_id = "https://example.com/objects/actually-forged-note" + + create = %{ + "type" => "Create", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/actually-forged-create", + "context" => "https://example.com/contexts/actually-forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => object_id, + "actor" => bob.ap_id, + "attributedTo" => bob.ap_id, + "context" => "https://example.com/contexts/actually-forged-create", + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + assert {:ok, oban_job} = + Federator.incoming_ap_doc(%{ + method: "POST", + req_headers: mismatched_signature_headers(), + request_path: "/inbox", + params: create, + query_string: "" + }) + + assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) + refute Pleroma.Object.get_by_ap_id(object_id) + end + + test "cancels signature actor mismatch before processing a forged Like" do + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + note = insert(:note) + + like = %{ + "type" => "Like", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/forged-like", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data["id"] + } + + assert_mismatched_signature_cancelled(like) + end + + test "cancels signature actor mismatch before actually creating a forged Like" do + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + note = insert(:note) + + like = %{ + "type" => "Like", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/actually-forged-like", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data["id"] + } + + assert {:ok, oban_job} = + Federator.incoming_ap_doc(%{ + method: "POST", + req_headers: mismatched_signature_headers(), + request_path: "/inbox", + params: like, + query_string: "" + }) + + assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) + refute Pleroma.Activity.get_by_ap_id(like["id"]) + end + + test "cancels signature actor mismatch before processing a forged Announce" do + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + note = insert(:note) + + announce = %{ + "type" => "Announce", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/forged-announce", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data["id"] + } + + assert_mismatched_signature_cancelled(announce) + end + + test "cancels signature actor mismatch before processing a forged Follow" do + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + followed = insert(:user) + + follow = %{ + "type" => "Follow", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/forged-follow", + "to" => [followed.ap_id], + "cc" => [], + "object" => followed.ap_id + } + + assert_mismatched_signature_cancelled(follow) + end + + test "cancels signature actor mismatch before processing a forged Undo" do + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + + undo = %{ + "type" => "Undo", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/forged-undo", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => "https://example.com/activities/existing-bob-activity" + } + + assert_mismatched_signature_cancelled(undo) + end end From 9c540995b4ca1bb33e38692bdf7504f9f4cdffc5 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 30 Apr 2026 15:36:55 +0400 Subject: [PATCH 31/55] Use Mox in spoofing regression tests --- .../activity_pub_controller_test.exs | 86 ++++++++++++++++++ test/pleroma/workers/receiver_worker_test.exs | 91 ++++++++++--------- 2 files changed, 136 insertions(+), 41 deletions(-) diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index 42cb35669..8cadf6686 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -794,6 +794,92 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do refute Activity.get_by_ap_id(data["id"]) end + test "does not create a forged post signed by a different actor", %{conn: conn} do + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + object_id = "https://example.com/objects/inbox-signed-forged-note" + + data = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Create", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/inbox-signed-forged-create", + "context" => "https://example.com/contexts/inbox-signed-forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => object_id, + "actor" => bob.ap_id, + "attributedTo" => bob.ap_id, + "context" => "https://example.com/contexts/inbox-signed-forged-create", + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end) + + conn = + conn + |> put_req_header("content-type", "application/activity+json") + |> put_req_header("date", "Thu, 25 Jul 2024 13:33:31 GMT") + |> put_req_header("digest", "SHA-256=fake-digest") + |> put_req_header( + "signature", + "keyId=\"#{alice.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" + ) + |> post("/inbox", data) + + assert conn.assigns.valid_signature == false + assert "ok" == json_response(conn, 200) + + assert [{:cancel, :actor_signature_mismatch}] = + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + + refute Activity.get_by_ap_id(data["id"]) + refute Object.get_by_ap_id(object_id) + end + + test "does not create a forged like signed by a different actor", %{conn: conn} do + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + note = insert(:note) + + data = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Like", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/inbox-signed-forged-like", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data["id"] + } + + Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end) + + conn = + conn + |> put_req_header("content-type", "application/activity+json") + |> put_req_header("date", "Thu, 25 Jul 2024 13:33:31 GMT") + |> put_req_header("digest", "SHA-256=fake-digest") + |> put_req_header( + "signature", + "keyId=\"#{alice.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" + ) + |> post("/inbox", data) + + assert conn.assigns.valid_signature == false + assert "ok" == json_response(conn, 200) + + assert [{:cancel, :actor_signature_mismatch}] = + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + + refute Activity.get_by_ap_id(data["id"]) + end + test "accept follow activity", %{conn: conn} do clear_config([:instance, :federating], true) relay = Relay.get_actor() diff --git a/test/pleroma/workers/receiver_worker_test.exs b/test/pleroma/workers/receiver_worker_test.exs index 9dccd739b..5e18fe771 100644 --- a/test/pleroma/workers/receiver_worker_test.exs +++ b/test/pleroma/workers/receiver_worker_test.exs @@ -11,6 +11,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do alias Pleroma.User alias Pleroma.Web.CommonAPI + alias Pleroma.Web.ActivityPub.UserView alias Pleroma.Web.Federator alias Pleroma.Workers.ReceiverWorker @@ -27,28 +28,34 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do ] end - defp assert_mismatched_signature_cancelled(params) do - with_mocks [ - {Pleroma.Signature, [:passthrough], - [ - refetch_public_key: fn _conn -> {:ok, :fake_public_key} end, - validate_signature: fn _conn -> true end - ]}, - {Pleroma.Web.Federator, [:passthrough], - [perform: fn :incoming_ap_doc, _params -> {:ok, :processed} end]} - ] do - assert {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: mismatched_signature_headers(), - request_path: "/inbox", - params: params, - query_string: "" - }) + defp expect_signature_from(%User{} = signer) do + signer_json = UserView.render("user.json", %{user: signer}) |> Map.delete("featured") - assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) - refute called(Pleroma.Web.Federator.perform(:incoming_ap_doc, :_)) - end + Tesla.Mock.mock(fn + %{url: url} when url == signer.ap_id -> + %Tesla.Env{ + status: 200, + body: Jason.encode!(signer_json), + headers: HttpRequestMock.activitypub_object_headers() + } + end) + + Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end) + end + + defp assert_mismatched_signature_cancelled(params, signer) do + expect_signature_from(signer) + + assert {:ok, oban_job} = + Federator.incoming_ap_doc(%{ + method: "POST", + req_headers: mismatched_signature_headers(), + request_path: "/inbox", + params: params, + query_string: "" + }) + + assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) end test "it does not retry MRF reject" do @@ -341,7 +348,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels when signature actor does not match payload actor" do - _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") note = insert(:note, user: bob, object_local: false) @@ -377,11 +384,9 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do } } - with_mock Pleroma.Signature, [:passthrough], - refetch_public_key: fn _conn -> {:ok, :fake_public_key} end, - validate_signature: fn _conn -> true end do - assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) - end + expect_signature_from(alice) + + assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) end test "Federator preserves request metadata needed for ReceiverWorker signature checks" do @@ -411,7 +416,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels signature actor mismatch through Federator-created jobs" do - _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") note = insert(:note, user: bob, object_local: false) @@ -425,11 +430,11 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do "object" => note.data } - assert_mismatched_signature_cancelled(update) + assert_mismatched_signature_cancelled(update, alice) end test "cancels signature actor mismatch before processing a forged Create" do - _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") create = %{ @@ -450,11 +455,11 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do } } - assert_mismatched_signature_cancelled(create) + assert_mismatched_signature_cancelled(create, alice) end test "cancels signature actor mismatch before actually creating a forged post" do - _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") object_id = "https://example.com/objects/actually-forged-note" @@ -479,6 +484,8 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do } } + expect_signature_from(alice) + assert {:ok, oban_job} = Federator.incoming_ap_doc(%{ method: "POST", @@ -493,7 +500,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels signature actor mismatch before processing a forged Like" do - _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") note = insert(:note) @@ -506,11 +513,11 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do "object" => note.data["id"] } - assert_mismatched_signature_cancelled(like) + assert_mismatched_signature_cancelled(like, alice) end test "cancels signature actor mismatch before actually creating a forged Like" do - _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") note = insert(:note) @@ -523,6 +530,8 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do "object" => note.data["id"] } + expect_signature_from(alice) + assert {:ok, oban_job} = Federator.incoming_ap_doc(%{ method: "POST", @@ -537,7 +546,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels signature actor mismatch before processing a forged Announce" do - _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") note = insert(:note) @@ -550,11 +559,11 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do "object" => note.data["id"] } - assert_mismatched_signature_cancelled(announce) + assert_mismatched_signature_cancelled(announce, alice) end test "cancels signature actor mismatch before processing a forged Follow" do - _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") followed = insert(:user) @@ -567,11 +576,11 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do "object" => followed.ap_id } - assert_mismatched_signature_cancelled(follow) + assert_mismatched_signature_cancelled(follow, alice) end test "cancels signature actor mismatch before processing a forged Undo" do - _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") undo = %{ @@ -583,6 +592,6 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do "object" => "https://example.com/activities/existing-bob-activity" } - assert_mismatched_signature_cancelled(undo) + assert_mismatched_signature_cancelled(undo, alice) end end From bd45704dba4eb82417daa449fc0af960867fcf9b Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 30 Apr 2026 17:21:40 +0400 Subject: [PATCH 32/55] Clarify cross-domain spoofing regressions --- .../activity_pub_controller_test.exs | 36 ++++---- test/pleroma/workers/receiver_worker_test.exs | 90 ++++++++++--------- 2 files changed, 68 insertions(+), 58 deletions(-) diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index 8cadf6686..2be5ca6df 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -727,14 +727,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do end test "does not create a forged post after failed signature retry", %{conn: conn} do - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") - object_id = "https://example.com/objects/inbox-forged-note" + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + object_id = "https://two.com/objects/inbox-forged-note" data = %{ "type" => "Create", "actor" => bob.ap_id, - "id" => "https://example.com/activities/inbox-forged-create", - "context" => "https://example.com/contexts/inbox-forged-create", + "id" => "https://two.com/activities/inbox-forged-create", + "context" => "https://two.com/contexts/inbox-forged-create", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], "object" => %{ @@ -742,7 +742,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do "id" => object_id, "actor" => bob.ap_id, "attributedTo" => bob.ap_id, - "context" => "https://example.com/contexts/inbox-forged-create", + "context" => "https://two.com/contexts/inbox-forged-create", "content" => "forged post", "published" => "2024-07-25T13:33:31Z", "to" => ["https://www.w3.org/ns/activitystreams#Public"], @@ -754,7 +754,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn |> assign(:valid_signature, false) |> put_req_header("content-type", "application/activity+json") - |> put_req_header("signature", "keyId=\"https://example.com/users/alice#main-key\"") + |> put_req_header("signature", "keyId=\"https://one.com/users/alice#main-key\"") |> post("/inbox", data) assert "ok" == json_response(conn, 200) @@ -767,13 +767,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do end test "does not create a forged like after failed signature retry", %{conn: conn} do - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") note = insert(:note) data = %{ "type" => "Like", "actor" => bob.ap_id, - "id" => "https://example.com/activities/inbox-forged-like", + "id" => "https://two.com/activities/inbox-forged-like", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], "object" => note.data["id"] @@ -783,7 +783,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn |> assign(:valid_signature, false) |> put_req_header("content-type", "application/activity+json") - |> put_req_header("signature", "keyId=\"https://example.com/users/alice#main-key\"") + |> put_req_header("signature", "keyId=\"https://one.com/users/alice#main-key\"") |> post("/inbox", data) assert "ok" == json_response(conn, 200) @@ -795,16 +795,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do end test "does not create a forged post signed by a different actor", %{conn: conn} do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") - object_id = "https://example.com/objects/inbox-signed-forged-note" + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + object_id = "https://two.com/objects/inbox-signed-forged-note" data = %{ "@context" => "https://www.w3.org/ns/activitystreams", "type" => "Create", "actor" => bob.ap_id, - "id" => "https://example.com/activities/inbox-signed-forged-create", - "context" => "https://example.com/contexts/inbox-signed-forged-create", + "id" => "https://two.com/activities/inbox-signed-forged-create", + "context" => "https://two.com/contexts/inbox-signed-forged-create", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], "object" => %{ @@ -812,7 +812,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do "id" => object_id, "actor" => bob.ap_id, "attributedTo" => bob.ap_id, - "context" => "https://example.com/contexts/inbox-signed-forged-create", + "context" => "https://two.com/contexts/inbox-signed-forged-create", "content" => "forged post", "published" => "2024-07-25T13:33:31Z", "to" => ["https://www.w3.org/ns/activitystreams#Public"], @@ -844,15 +844,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do end test "does not create a forged like signed by a different actor", %{conn: conn} do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") note = insert(:note) data = %{ "@context" => "https://www.w3.org/ns/activitystreams", "type" => "Like", "actor" => bob.ap_id, - "id" => "https://example.com/activities/inbox-signed-forged-like", + "id" => "https://two.com/activities/inbox-signed-forged-like", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], "object" => note.data["id"] diff --git a/test/pleroma/workers/receiver_worker_test.exs b/test/pleroma/workers/receiver_worker_test.exs index 5e18fe771..ec630eb7b 100644 --- a/test/pleroma/workers/receiver_worker_test.exs +++ b/test/pleroma/workers/receiver_worker_test.exs @@ -17,13 +17,13 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do defp mismatched_signature_headers do [ - {"host", "example.com"}, + {"host", "local.test"}, {"date", "Thu, 25 Jul 2024 13:33:31 GMT"}, {"digest", "SHA-256=fake-digest"}, {"content-type", "application/activity+json"}, { "signature", - "keyId=\"https://example.com/users/alice#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" + "keyId=\"https://one.com/users/alice#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" } ] end @@ -348,28 +348,33 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels when signature actor does not match payload actor" do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - note = insert(:note, user: bob, object_local: false) + note = + insert(:note, + user: bob, + object_local: false, + data: %{"id" => "https://two.com/objects/malicious-update-note"} + ) update = %{ "type" => "Update", "actor" => bob.ap_id, - "id" => "https://example.com/activities/malicious-update", + "id" => "https://two.com/activities/malicious-update", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], "object" => note.data } req_headers = [ - ["host", "example.com"], + ["host", "local.test"], ["date", "Thu, 25 Jul 2024 13:33:31 GMT"], ["digest", "SHA-256=fake-digest"], ["content-type", "application/activity+json"], [ "signature", - "keyId=\"https://example.com/users/alice#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" + "keyId=\"https://one.com/users/alice#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" ] ] @@ -393,8 +398,8 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do params = insert(:note_activity).data req_headers = [ - {"host", "example.com"}, - {"signature", "keyId=\"https://example.com/users/alice#main-key\""} + {"host", "local.test"}, + {"signature", "keyId=\"https://one.com/users/alice#main-key\""} ] assert {:ok, oban_job} = @@ -416,15 +421,20 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels signature actor mismatch through Federator-created jobs" do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - note = insert(:note, user: bob, object_local: false) + note = + insert(:note, + user: bob, + object_local: false, + data: %{"id" => "https://two.com/objects/federator-malicious-note"} + ) update = %{ "type" => "Update", "actor" => bob.ap_id, - "id" => "https://example.com/activities/federator-malicious-update", + "id" => "https://two.com/activities/federator-malicious-update", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], "object" => note.data @@ -434,18 +444,18 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels signature actor mismatch before processing a forged Create" do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") create = %{ "type" => "Create", "actor" => bob.ap_id, - "id" => "https://example.com/activities/forged-create", + "id" => "https://two.com/activities/forged-create", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], "object" => %{ "type" => "Note", - "id" => "https://example.com/objects/forged-note", + "id" => "https://two.com/objects/forged-note", "actor" => bob.ap_id, "attributedTo" => bob.ap_id, "content" => "forged post", @@ -459,16 +469,16 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels signature actor mismatch before actually creating a forged post" do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - object_id = "https://example.com/objects/actually-forged-note" + object_id = "https://two.com/objects/actually-forged-note" create = %{ "type" => "Create", "actor" => bob.ap_id, - "id" => "https://example.com/activities/actually-forged-create", - "context" => "https://example.com/contexts/actually-forged-create", + "id" => "https://two.com/activities/actually-forged-create", + "context" => "https://two.com/contexts/actually-forged-create", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], "object" => %{ @@ -476,7 +486,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do "id" => object_id, "actor" => bob.ap_id, "attributedTo" => bob.ap_id, - "context" => "https://example.com/contexts/actually-forged-create", + "context" => "https://two.com/contexts/actually-forged-create", "content" => "forged post", "published" => "2024-07-25T13:33:31Z", "to" => ["https://www.w3.org/ns/activitystreams#Public"], @@ -500,14 +510,14 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels signature actor mismatch before processing a forged Like" do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") note = insert(:note) like = %{ "type" => "Like", "actor" => bob.ap_id, - "id" => "https://example.com/activities/forged-like", + "id" => "https://two.com/activities/forged-like", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], "object" => note.data["id"] @@ -517,14 +527,14 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels signature actor mismatch before actually creating a forged Like" do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") note = insert(:note) like = %{ "type" => "Like", "actor" => bob.ap_id, - "id" => "https://example.com/activities/actually-forged-like", + "id" => "https://two.com/activities/actually-forged-like", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], "object" => note.data["id"] @@ -546,14 +556,14 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels signature actor mismatch before processing a forged Announce" do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") note = insert(:note) announce = %{ "type" => "Announce", "actor" => bob.ap_id, - "id" => "https://example.com/activities/forged-announce", + "id" => "https://two.com/activities/forged-announce", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], "object" => note.data["id"] @@ -563,14 +573,14 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels signature actor mismatch before processing a forged Follow" do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") followed = insert(:user) follow = %{ "type" => "Follow", "actor" => bob.ap_id, - "id" => "https://example.com/activities/forged-follow", + "id" => "https://two.com/activities/forged-follow", "to" => [followed.ap_id], "cc" => [], "object" => followed.ap_id @@ -580,16 +590,16 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels signature actor mismatch before processing a forged Undo" do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") undo = %{ "type" => "Undo", "actor" => bob.ap_id, - "id" => "https://example.com/activities/forged-undo", + "id" => "https://two.com/activities/forged-undo", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], - "object" => "https://example.com/activities/existing-bob-activity" + "object" => "https://two.com/activities/existing-bob-activity" } assert_mismatched_signature_cancelled(undo, alice) From 7756f491d5a4d3528fc96e87f05554b2c2380ad4 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Fri, 1 May 2026 08:43:42 +0400 Subject: [PATCH 33/55] Split failed-signature inbox retries Route failed-signature ActivityPub inbox retries through a dedicated worker so legacy and malformed retry jobs fail closed before processing. --- .../activity_pub/activity_pub_controller.ex | 2 +- lib/pleroma/web/federator.ex | 16 +- lib/pleroma/workers/receiver_worker.ex | 76 +-- lib/pleroma/workers/signature_retry_worker.ex | 144 +++++ .../activity_pub_controller_test.exs | 37 +- test/pleroma/workers/receiver_worker_test.exs | 503 ++++-------------- .../workers/signature_retry_worker_test.exs | 469 ++++++++++++++++ 7 files changed, 786 insertions(+), 461 deletions(-) create mode 100644 lib/pleroma/workers/signature_retry_worker.ex create mode 100644 test/pleroma/workers/signature_retry_worker_test.exs diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 4f1613a07..2bfff6968 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -348,7 +348,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do end def inbox(%{assigns: %{valid_signature: false}} = conn, params) do - Federator.incoming_ap_doc(%{ + Federator.incoming_failed_signature_ap_doc(%{ method: conn.method, req_headers: conn.req_headers, request_path: conn.request_path, diff --git a/lib/pleroma/web/federator.ex b/lib/pleroma/web/federator.ex index 676fc5137..90cd2e54a 100644 --- a/lib/pleroma/web/federator.ex +++ b/lib/pleroma/web/federator.ex @@ -11,6 +11,7 @@ defmodule Pleroma.Web.Federator do alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Workers.PublisherWorker alias Pleroma.Workers.ReceiverWorker + alias Pleroma.Workers.SignatureRetryWorker require Logger @@ -35,12 +36,21 @@ defmodule Pleroma.Web.Federator do end # Client API - def incoming_ap_doc(%{params: params, req_headers: req_headers}) do - ReceiverWorker.new( + def incoming_failed_signature_ap_doc(%{ + method: method, + params: params, + req_headers: req_headers, + request_path: request_path, + query_string: query_string + }) do + SignatureRetryWorker.new( %{ - "op" => "incoming_ap_doc", + "op" => "incoming_failed_signature_ap_doc", + "method" => method, "req_headers" => req_headers, "params" => params, + "request_path" => request_path, + "query_string" => query_string, "timeout" => :timer.seconds(20) }, priority: 2 diff --git a/lib/pleroma/workers/receiver_worker.ex b/lib/pleroma/workers/receiver_worker.ex index 507e099c2..3afbe138d 100644 --- a/lib/pleroma/workers/receiver_worker.ex +++ b/lib/pleroma/workers/receiver_worker.ex @@ -4,55 +4,36 @@ defmodule Pleroma.Workers.ReceiverWorker do alias Pleroma.Instances - alias Pleroma.Signature - alias Pleroma.User alias Pleroma.Web.Federator - alias Pleroma.Web.Plugs.MappedSignatureToIdentityPlug + alias Pleroma.Workers.SignatureRetryWorker use Oban.Worker, queue: :federator_incoming, max_attempts: 5, unique: [period: :infinity] @impl true - - def perform(%Job{ - args: %{ - "op" => "incoming_ap_doc", - "method" => method, - "params" => params, - "req_headers" => req_headers, - "request_path" => request_path, - "query_string" => query_string - } - }) do - # Oban's serialization converts our tuple headers to lists. - # Revert it for the signature validation. - req_headers = Enum.into(req_headers, [], &List.to_tuple(&1)) - - conn_data = %Plug.Conn{ - assigns: %{valid_signature: true}, - method: method, - params: params, - req_headers: req_headers, - request_path: request_path, - query_string: query_string - } - - with {:ok, %User{}} <- User.get_or_fetch_by_ap_id(conn_data.params["actor"]), - {:ok, _public_key} <- Signature.refetch_public_key(conn_data), - {:signature, true} <- {:signature, Signature.validate_signature(conn_data)}, - {:same_actor, true} <- {:same_actor, validate_same_actor(conn_data)}, - {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do - unless Instances.reachable?(params["actor"]) do - domain = URI.parse(params["actor"]).host - Oban.insert(Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain})) - end - - {:ok, res} + def perform(%Job{args: %{"op" => "incoming_ap_doc", "params" => params} = args} = job) do + if signature_retry_job?(args) do + perform_signature_retry(job) else - e -> process_errors(e) + perform_incoming(params) end end - def perform(%Job{args: %{"op" => "incoming_ap_doc", "params" => params}}) do + def perform(%Job{args: %{"op" => "incoming_ap_doc"} = args} = job) do + if signature_retry_job?(args) do + perform_signature_retry(job) + else + process_errors(:missing_incoming_ap_doc_params) + end + end + + defp perform_signature_retry(%Job{args: args} = job) do + SignatureRetryWorker.perform(%Job{ + job + | args: Map.put(args, "op", "incoming_failed_signature_ap_doc") + }) + end + + defp perform_incoming(params) do with {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do unless Instances.reachable?(params["actor"]) do domain = URI.parse(params["actor"]).host @@ -65,21 +46,15 @@ defmodule Pleroma.Workers.ReceiverWorker do end end + defp signature_retry_job?(args) do + Enum.any?(~w(method req_headers request_path query_string), &Map.has_key?(args, &1)) + end + @impl true def timeout(%_{args: %{"timeout" => timeout}}), do: timeout def timeout(_job), do: :timer.seconds(5) - defp validate_same_actor(conn_data) do - case MappedSignatureToIdentityPlug.call(conn_data, []) do - %Plug.Conn{assigns: %{valid_signature: true}} -> - true - - _ -> - false - end - end - defp process_errors({:error, {:error, _} = error}), do: process_errors(error) defp process_errors(errors) do @@ -103,6 +78,7 @@ defmodule Pleroma.Workers.ReceiverWorker do {:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed} # Unclear if this can be reached {:error, {:side_effects, {:error, :no_object_actor}} = reason} -> {:cancel, reason} + :missing_incoming_ap_doc_params -> {:cancel, :missing_incoming_ap_doc_params} # Catchall {:error, _} = e -> e e -> {:error, e} diff --git a/lib/pleroma/workers/signature_retry_worker.ex b/lib/pleroma/workers/signature_retry_worker.ex new file mode 100644 index 000000000..56673a514 --- /dev/null +++ b/lib/pleroma/workers/signature_retry_worker.ex @@ -0,0 +1,144 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.SignatureRetryWorker do + alias Pleroma.Instances + alias Pleroma.Signature + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Utils + alias Pleroma.Web.Federator + alias Pleroma.Web.Plugs.MappedSignatureToIdentityPlug + + use Oban.Worker, queue: :federator_incoming, max_attempts: 5, unique: [period: :infinity] + + @impl true + def perform(%Job{ + args: %{ + "op" => "incoming_failed_signature_ap_doc", + "method" => method, + "params" => params, + "req_headers" => req_headers, + "request_path" => request_path, + "query_string" => query_string + } + }) + when is_binary(method) and is_map(params) and is_list(req_headers) and + is_binary(request_path) and is_binary(query_string) do + with {:ok, req_headers} <- normalize_req_headers(req_headers), + conn_data = %Plug.Conn{ + assigns: %{valid_signature: true}, + method: method, + params: params, + req_headers: req_headers, + request_path: request_path, + query_string: query_string + }, + actor_id = Utils.get_ap_id(params["actor"]), + {:signature_actor, {:ok, signature_actor_id}} <- + {:signature_actor, signature_actor_id(conn_data)}, + {:same_actor, true} <- {:same_actor, signature_actor_id == actor_id}, + {:ok, %User{}} <- User.get_or_fetch_by_ap_id(actor_id), + {:ok, _public_key} <- Signature.refetch_public_key(conn_data), + {:signature, true} <- {:signature, validate_signature(conn_data)}, + {:same_actor, true} <- {:same_actor, validate_same_actor(conn_data)}, + {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do + unless Instances.reachable?(params["actor"]) do + domain = URI.parse(params["actor"]).host + Oban.insert(Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain})) + end + + {:ok, res} + else + e -> process_errors(e) + end + end + + def perform(%Job{args: %{"op" => "incoming_failed_signature_ap_doc"}}) do + process_errors(:missing_signature_retry_metadata) + end + + def perform(%Job{}), do: process_errors(:missing_signature_retry_metadata) + + @impl true + def timeout(%_{args: %{"timeout" => timeout}}), do: timeout + + def timeout(_job), do: :timer.seconds(5) + + defp normalize_req_headers(req_headers) do + req_headers + |> Enum.reduce_while({:ok, []}, fn + {key, value}, {:ok, acc} when is_binary(key) and is_binary(value) -> + {:cont, {:ok, [{key, value} | acc]}} + + [key, value], {:ok, acc} when is_binary(key) and is_binary(value) -> + {:cont, {:ok, [{key, value} | acc]}} + + _, _ -> + {:halt, {:error, :invalid_signature_retry_metadata}} + end) + |> case do + {:ok, headers} -> {:ok, Enum.reverse(headers)} + error -> error + end + end + + defp validate_same_actor(conn_data) do + case MappedSignatureToIdentityPlug.call(conn_data, []) do + %Plug.Conn{assigns: %{valid_signature: true}} -> + true + + _ -> + false + end + end + + defp validate_signature(conn_data) do + Signature.validate_signature(conn_data) + rescue + _ -> false + catch + _, _ -> false + end + + defp signature_actor_id(conn_data) do + Signature.get_actor_id(conn_data) + rescue + _ -> {:error, :invalid_signature} + catch + _, _ -> {:error, :invalid_signature} + end + + defp process_errors({:error, {:error, _} = error}), do: process_errors(error) + + defp process_errors(errors) do + case errors do + # User fetch failures + {:error, :not_found} = reason -> {:cancel, reason} + {:error, :forbidden} = reason -> {:cancel, reason} + # Inactive user + {:error, {:user_active, false} = reason} -> {:cancel, reason} + # Validator will error and return a changeset error + # e.g., duplicate activities or if the object was deleted + {:error, {:validate, {:error, _changeset} = reason}} -> {:cancel, reason} + # Duplicate detection during Normalization + {:error, :already_present} -> {:cancel, :already_present} + # MRFs will return a reject + {:error, {:reject, _} = reason} -> {:cancel, reason} + # HTTP Sigs + {:signature_actor, {:error, _}} -> {:cancel, :invalid_signature} + {:signature, false} -> {:cancel, :invalid_signature} + {:same_actor, false} -> {:cancel, :actor_signature_mismatch} + # Origin / URL validation failed somewhere possibly due to spoofing + {:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed} + # Unclear if this can be reached + {:error, {:side_effects, {:error, :no_object_actor}} = reason} -> {:cancel, reason} + # Fail closed if the retry cannot reconstruct the original request. + :missing_signature_retry_metadata -> {:cancel, :missing_signature_retry_metadata} + {:error, :invalid_signature_retry_metadata} -> {:cancel, :invalid_signature_retry_metadata} + # Catchall + {:error, _} = e -> e + e -> {:error, e} + end + end +end diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index 2be5ca6df..62c1dd830 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -19,6 +19,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do alias Pleroma.Web.CommonAPI alias Pleroma.Web.Endpoint alias Pleroma.Workers.ReceiverWorker + alias Pleroma.Workers.SignatureRetryWorker import Pleroma.Factory @@ -36,6 +37,24 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do setup do: clear_config([:instance, :federating], true) + defp expect_signature_retry_from(%User{} = signer) do + signer_json = UserView.render("user.json", %{user: signer}) |> Map.delete("featured") + + Tesla.Mock.mock(fn + %{url: url} when url == signer.ap_id -> + %Tesla.Env{ + status: 200, + body: Jason.encode!(signer_json), + headers: HttpRequestMock.activitypub_object_headers() + } + + env -> + apply(HttpRequestMock, :request, [env]) + end) + + Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end) + end + describe "/relay" do setup do: clear_config([:instance, :allow_relay]) @@ -727,6 +746,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do end test "does not create a forged post after failed signature retry", %{conn: conn} do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") object_id = "https://two.com/objects/inbox-forged-note" @@ -750,6 +770,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do } } + expect_signature_retry_from(alice) + conn = conn |> assign(:valid_signature, false) @@ -760,13 +782,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert "ok" == json_response(conn, 200) assert [{:cancel, :actor_signature_mismatch}] = - ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker)) refute Activity.get_by_ap_id(data["id"]) refute Object.get_by_ap_id(object_id) end test "does not create a forged like after failed signature retry", %{conn: conn} do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") note = insert(:note) @@ -779,6 +802,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do "object" => note.data["id"] } + expect_signature_retry_from(alice) + conn = conn |> assign(:valid_signature, false) @@ -789,7 +814,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert "ok" == json_response(conn, 200) assert [{:cancel, :actor_signature_mismatch}] = - ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker)) refute Activity.get_by_ap_id(data["id"]) end @@ -820,7 +845,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do } } - Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end) + expect_signature_retry_from(alice) conn = conn @@ -837,7 +862,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert "ok" == json_response(conn, 200) assert [{:cancel, :actor_signature_mismatch}] = - ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker)) refute Activity.get_by_ap_id(data["id"]) refute Object.get_by_ap_id(object_id) @@ -858,7 +883,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do "object" => note.data["id"] } - Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end) + expect_signature_retry_from(alice) conn = conn @@ -875,7 +900,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert "ok" == json_response(conn, 200) assert [{:cancel, :actor_signature_mismatch}] = - ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker)) refute Activity.get_by_ap_id(data["id"]) end diff --git a/test/pleroma/workers/receiver_worker_test.exs b/test/pleroma/workers/receiver_worker_test.exs index ec630eb7b..67a3f902e 100644 --- a/test/pleroma/workers/receiver_worker_test.exs +++ b/test/pleroma/workers/receiver_worker_test.exs @@ -11,11 +11,9 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do alias Pleroma.User alias Pleroma.Web.CommonAPI - alias Pleroma.Web.ActivityPub.UserView - alias Pleroma.Web.Federator alias Pleroma.Workers.ReceiverWorker - defp mismatched_signature_headers do + defp signature_headers_for(%User{} = signer) do [ {"host", "local.test"}, {"date", "Thu, 25 Jul 2024 13:33:31 GMT"}, @@ -23,39 +21,15 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do {"content-type", "application/activity+json"}, { "signature", - "keyId=\"https://one.com/users/alice#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" + "keyId=\"#{signer.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" } ] end - defp expect_signature_from(%User{} = signer) do - signer_json = UserView.render("user.json", %{user: signer}) |> Map.delete("featured") - - Tesla.Mock.mock(fn - %{url: url} when url == signer.ap_id -> - %Tesla.Env{ - status: 200, - body: Jason.encode!(signer_json), - headers: HttpRequestMock.activitypub_object_headers() - } - end) - - Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end) - end - - defp assert_mismatched_signature_cancelled(params, signer) do - expect_signature_from(signer) - - assert {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: mismatched_signature_headers(), - request_path: "/inbox", - params: params, - query_string: "" - }) - - assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) + defp perform_incoming(params) do + ReceiverWorker.perform(%Oban.Job{ + args: %{"op" => "incoming_ap_doc", "params" => params} + }) end test "it does not retry MRF reject" do @@ -125,16 +99,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do insert(:note_activity).data |> Map.put("actor", "https://springfield.social/users/bart") - {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: [], - request_path: "/inbox", - params: params, - query_string: "" - }) - - assert {:cancel, {:error, :forbidden}} = ReceiverWorker.perform(oban_job) + assert {:cancel, {:error, :forbidden}} = perform_incoming(params) end test "when request returns a 404" do @@ -142,16 +107,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do insert(:note_activity).data |> Map.put("actor", "https://springfield.social/users/troymcclure") - {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: [], - request_path: "/inbox", - params: params, - query_string: "" - }) - - assert {:cancel, {:error, :not_found}} = ReceiverWorker.perform(oban_job) + assert {:cancel, {:error, :not_found}} = perform_incoming(params) end test "when request returns a 410" do @@ -159,16 +115,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do insert(:note_activity).data |> Map.put("actor", "https://springfield.social/users/hankscorpio") - {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: [], - request_path: "/inbox", - params: params, - query_string: "" - }) - - assert {:cancel, {:error, :not_found}} = ReceiverWorker.perform(oban_job) + assert {:cancel, {:error, :not_found}} = perform_incoming(params) end test "when user account is disabled" do @@ -182,86 +129,16 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do {:ok, %User{}} = User.set_activation(user, false) - {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: [], - request_path: "/inbox", - params: params, - query_string: "" - }) - - assert {:cancel, {:user_active, false}} = ReceiverWorker.perform(oban_job) + assert {:cancel, {:user_active, false}} = perform_incoming(params) end end - test "it can validate the signature" do - Tesla.Mock.mock(fn - %{url: "https://phpc.social/users/denniskoch"} -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/denniskoch.json"), - headers: [{"content-type", "application/activity+json"}] - } - - %{url: "https://phpc.social/users/denniskoch/collections/featured"} -> - %Tesla.Env{ - status: 200, - headers: [{"content-type", "application/activity+json"}], - body: - File.read!("test/fixtures/users_mock/masto_featured.json") - |> String.replace("{{domain}}", "phpc.social") - |> String.replace("{{nickname}}", "denniskoch") - } - end) - - params = - File.read!("test/fixtures/receiver_worker_signature_activity.json") |> Jason.decode!() - - req_headers = [ - ["accept-encoding", "gzip"], - ["content-length", "5184"], - ["content-type", "application/activity+json"], - ["date", "Thu, 25 Jul 2024 13:33:31 GMT"], - ["digest", "SHA-256=ouge/6HP2/QryG6F3JNtZ6vzs/hSwMk67xdxe87eH7A="], - ["host", "bikeshed.party"], - [ - "signature", - "keyId=\"https://mastodon.social/users/bastianallgeier#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"ymE3vn5Iw50N6ukSp8oIuXJB5SBjGAGjBasdTDvn+ahZIzq2SIJfmVCsIIzyqIROnhWyQoTbavTclVojEqdaeOx+Ejz2wBnRBmhz5oemJLk4RnnCH0lwMWyzeY98YAvxi9Rq57Gojuv/1lBqyGa+rDzynyJpAMyFk17XIZpjMKuTNMCbjMDy76ILHqArykAIL/v1zxkgwxY/+ELzxqMpNqtZ+kQ29znNMUBB3eVZ/mNAHAz6o33Y9VKxM2jw+08vtuIZOusXyiHbRiaj2g5HtN2WBUw1MzzfRfHF2/yy7rcipobeoyk5RvP5SyHV3WrIeZ3iyoNfmv33y8fxllF0EA==\"" - ], - [ - "user-agent", - "http.rb/5.2.0 (Mastodon/4.3.0-nightly.2024-07-25; +https://mastodon.social/)" - ] - ] - - {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: req_headers, - request_path: "/inbox", - params: params, - query_string: "" - }) - - assert {:ok, %Pleroma.Activity{}} = ReceiverWorker.perform(oban_job) - end - test "cancels due to origin containment" do params = insert(:note_activity).data |> Map.put("id", "https://notorigindomain.com/activity") - {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: [], - request_path: "/inbox", - params: params, - query_string: "" - }) - - assert {:cancel, :origin_containment_failed} = ReceiverWorker.perform(oban_job) + assert {:cancel, :origin_containment_failed} = perform_incoming(params) end test "canceled due to deleted object" do @@ -277,16 +154,98 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do } end) - {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: [], - request_path: "/inbox", - params: params, - query_string: "" - }) + assert {:cancel, _} = perform_incoming(params) + end - assert {:cancel, _} = ReceiverWorker.perform(oban_job) + test "delegates legacy failed-signature metadata jobs instead of processing them as trusted" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + object_id = "https://two.com/objects/legacy-forged-note" + + create = %{ + "type" => "Create", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/legacy-forged-create", + "context" => "https://two.com/contexts/legacy-forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => object_id, + "actor" => bob.ap_id, + "attributedTo" => bob.ap_id, + "context" => "https://two.com/contexts/legacy-forged-create", + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + assert {:cancel, :actor_signature_mismatch} = + ReceiverWorker.perform(%Oban.Job{ + args: %{ + "op" => "incoming_ap_doc", + "method" => "POST", + "params" => create, + "req_headers" => signature_headers_for(alice), + "request_path" => "/inbox", + "query_string" => "" + } + }) + + refute Pleroma.Activity.get_by_ap_id(create["id"]) + refute Pleroma.Object.get_by_ap_id(object_id) + end + + test "fails closed for the old persisted failed-signature job shape" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + object_id = "https://two.com/objects/old-shape-forged-note" + + create = %{ + "type" => "Create", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/old-shape-forged-create", + "context" => "https://two.com/contexts/old-shape-forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => object_id, + "actor" => bob.ap_id, + "attributedTo" => bob.ap_id, + "context" => "https://two.com/contexts/old-shape-forged-create", + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + assert {:cancel, :missing_signature_retry_metadata} = + ReceiverWorker.perform(%Oban.Job{ + args: %{ + "op" => "incoming_ap_doc", + "params" => create, + "req_headers" => signature_headers_for(alice), + "timeout" => 20_000 + } + }) + + refute Pleroma.Activity.get_by_ap_id(create["id"]) + refute Pleroma.Object.get_by_ap_id(object_id) + end + + test "fails closed for malformed legacy metadata jobs without params" do + assert {:cancel, :missing_signature_retry_metadata} = + ReceiverWorker.perform(%Oban.Job{ + args: %{ + "op" => "incoming_ap_doc", + "req_headers" => [], + "timeout" => 20_000 + } + }) end describe "Server reachability:" do @@ -346,262 +305,4 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end end end - - test "cancels when signature actor does not match payload actor" do - alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - - note = - insert(:note, - user: bob, - object_local: false, - data: %{"id" => "https://two.com/objects/malicious-update-note"} - ) - - update = %{ - "type" => "Update", - "actor" => bob.ap_id, - "id" => "https://two.com/activities/malicious-update", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [], - "object" => note.data - } - - req_headers = [ - ["host", "local.test"], - ["date", "Thu, 25 Jul 2024 13:33:31 GMT"], - ["digest", "SHA-256=fake-digest"], - ["content-type", "application/activity+json"], - [ - "signature", - "keyId=\"https://one.com/users/alice#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" - ] - ] - - oban_job = %Oban.Job{ - args: %{ - "op" => "incoming_ap_doc", - "method" => "POST", - "params" => update, - "req_headers" => req_headers, - "request_path" => "/inbox", - "query_string" => "" - } - } - - expect_signature_from(alice) - - assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) - end - - test "Federator preserves request metadata needed for ReceiverWorker signature checks" do - params = insert(:note_activity).data - - req_headers = [ - {"host", "local.test"}, - {"signature", "keyId=\"https://one.com/users/alice#main-key\""} - ] - - assert {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: req_headers, - request_path: "/inbox", - params: params, - query_string: "foo=bar" - }) - - assert %{ - "method" => "POST", - "req_headers" => ^req_headers, - "request_path" => "/inbox", - "params" => ^params, - "query_string" => "foo=bar" - } = oban_job.args - end - - test "cancels signature actor mismatch through Federator-created jobs" do - alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - - note = - insert(:note, - user: bob, - object_local: false, - data: %{"id" => "https://two.com/objects/federator-malicious-note"} - ) - - update = %{ - "type" => "Update", - "actor" => bob.ap_id, - "id" => "https://two.com/activities/federator-malicious-update", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [], - "object" => note.data - } - - assert_mismatched_signature_cancelled(update, alice) - end - - test "cancels signature actor mismatch before processing a forged Create" do - alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - - create = %{ - "type" => "Create", - "actor" => bob.ap_id, - "id" => "https://two.com/activities/forged-create", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [], - "object" => %{ - "type" => "Note", - "id" => "https://two.com/objects/forged-note", - "actor" => bob.ap_id, - "attributedTo" => bob.ap_id, - "content" => "forged post", - "published" => "2024-07-25T13:33:31Z", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [] - } - } - - assert_mismatched_signature_cancelled(create, alice) - end - - test "cancels signature actor mismatch before actually creating a forged post" do - alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - - object_id = "https://two.com/objects/actually-forged-note" - - create = %{ - "type" => "Create", - "actor" => bob.ap_id, - "id" => "https://two.com/activities/actually-forged-create", - "context" => "https://two.com/contexts/actually-forged-create", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [], - "object" => %{ - "type" => "Note", - "id" => object_id, - "actor" => bob.ap_id, - "attributedTo" => bob.ap_id, - "context" => "https://two.com/contexts/actually-forged-create", - "content" => "forged post", - "published" => "2024-07-25T13:33:31Z", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [] - } - } - - expect_signature_from(alice) - - assert {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: mismatched_signature_headers(), - request_path: "/inbox", - params: create, - query_string: "" - }) - - assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) - refute Pleroma.Object.get_by_ap_id(object_id) - end - - test "cancels signature actor mismatch before processing a forged Like" do - alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - note = insert(:note) - - like = %{ - "type" => "Like", - "actor" => bob.ap_id, - "id" => "https://two.com/activities/forged-like", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [], - "object" => note.data["id"] - } - - assert_mismatched_signature_cancelled(like, alice) - end - - test "cancels signature actor mismatch before actually creating a forged Like" do - alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - note = insert(:note) - - like = %{ - "type" => "Like", - "actor" => bob.ap_id, - "id" => "https://two.com/activities/actually-forged-like", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [], - "object" => note.data["id"] - } - - expect_signature_from(alice) - - assert {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: mismatched_signature_headers(), - request_path: "/inbox", - params: like, - query_string: "" - }) - - assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) - refute Pleroma.Activity.get_by_ap_id(like["id"]) - end - - test "cancels signature actor mismatch before processing a forged Announce" do - alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - note = insert(:note) - - announce = %{ - "type" => "Announce", - "actor" => bob.ap_id, - "id" => "https://two.com/activities/forged-announce", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [], - "object" => note.data["id"] - } - - assert_mismatched_signature_cancelled(announce, alice) - end - - test "cancels signature actor mismatch before processing a forged Follow" do - alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - followed = insert(:user) - - follow = %{ - "type" => "Follow", - "actor" => bob.ap_id, - "id" => "https://two.com/activities/forged-follow", - "to" => [followed.ap_id], - "cc" => [], - "object" => followed.ap_id - } - - assert_mismatched_signature_cancelled(follow, alice) - end - - test "cancels signature actor mismatch before processing a forged Undo" do - alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - - undo = %{ - "type" => "Undo", - "actor" => bob.ap_id, - "id" => "https://two.com/activities/forged-undo", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [], - "object" => "https://two.com/activities/existing-bob-activity" - } - - assert_mismatched_signature_cancelled(undo, alice) - end end diff --git a/test/pleroma/workers/signature_retry_worker_test.exs b/test/pleroma/workers/signature_retry_worker_test.exs new file mode 100644 index 000000000..02706ebad --- /dev/null +++ b/test/pleroma/workers/signature_retry_worker_test.exs @@ -0,0 +1,469 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.SignatureRetryWorkerTest do + use Pleroma.DataCase, async: false + use Oban.Testing, repo: Pleroma.Repo + + import Pleroma.Factory + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Signature + alias Pleroma.User + alias Pleroma.Web.ActivityPub.UserView + alias Pleroma.Web.Federator + alias Pleroma.Workers.SignatureRetryWorker + + defp signature_headers_for(%User{} = signer) do + [ + {"host", "local.test"}, + {"date", "Thu, 25 Jul 2024 13:33:31 GMT"}, + {"digest", "SHA-256=fake-digest"}, + {"content-type", "application/activity+json"}, + { + "signature", + "keyId=\"#{signer.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" + } + ] + end + + defp stub_actor_fetch(%User{} = signer) do + signer_json = UserView.render("user.json", %{user: signer}) |> Map.delete("featured") + + Tesla.Mock.mock(fn + %{url: url} when url == signer.ap_id -> + %Tesla.Env{ + status: 200, + body: Jason.encode!(signer_json), + headers: HttpRequestMock.activitypub_object_headers() + } + end) + end + + defp expect_signature_from(%User{} = signer) do + stub_actor_fetch(signer) + Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end) + end + + defp enqueue_failed_signature(params, signer) do + Federator.incoming_failed_signature_ap_doc(%{ + method: "POST", + req_headers: signature_headers_for(signer), + request_path: "/inbox", + params: params, + query_string: "" + }) + end + + defp failed_signature_job(params, req_headers, opts \\ []) do + %Oban.Job{ + args: %{ + "op" => "incoming_failed_signature_ap_doc", + "method" => Keyword.get(opts, :method, "POST"), + "req_headers" => req_headers, + "request_path" => Keyword.get(opts, :request_path, "/inbox"), + "params" => params, + "query_string" => Keyword.get(opts, :query_string, "") + } + } + end + + defp assert_mismatched_signature_cancelled(params, signer) do + assert {:ok, oban_job} = enqueue_failed_signature(params, signer) + + assert {:cancel, :actor_signature_mismatch} = SignatureRetryWorker.perform(oban_job) + end + + test "Federator preserves request metadata for failed-signature retry jobs" do + params = insert(:note_activity).data + + req_headers = [ + {"host", "local.test"}, + {"signature", "keyId=\"https://one.com/users/alice#main-key\""} + ] + + assert {:ok, oban_job} = + Federator.incoming_failed_signature_ap_doc(%{ + method: "POST", + req_headers: req_headers, + request_path: "/inbox", + params: params, + query_string: "foo=bar" + }) + + assert oban_job.worker == "Pleroma.Workers.SignatureRetryWorker" + + assert %{ + "op" => "incoming_failed_signature_ap_doc", + "method" => "POST", + "req_headers" => ^req_headers, + "request_path" => "/inbox", + "params" => ^params, + "query_string" => "foo=bar" + } = oban_job.args + end + + test "cancels retry jobs without request metadata" do + params = insert(:note_activity).data + + assert {:cancel, :missing_signature_retry_metadata} = + SignatureRetryWorker.perform(%Oban.Job{ + args: %{"op" => "incoming_failed_signature_ap_doc", "params" => params} + }) + end + + test "cancels retry jobs with malformed serialized request headers" do + params = insert(:note_activity).data + + assert {:cancel, :invalid_signature_retry_metadata} = + SignatureRetryWorker.perform(failed_signature_job(params, [["signature"]])) + end + + test "cancels retry jobs without a signature header" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + params = insert(:note_activity, user: alice).data + + assert {:cancel, :invalid_signature} = + SignatureRetryWorker.perform(failed_signature_job(params, [{"host", "local.test"}])) + end + + test "cancels missing signature before fetching an unavailable payload actor" do + params = + insert(:note_activity).data + |> Map.put("actor", "https://unavailable.example/users/bob") + + assert {:cancel, :invalid_signature} = + SignatureRetryWorker.perform(failed_signature_job(params, [{"host", "local.test"}])) + end + + test "cancels signer mismatch before fetching an unavailable payload actor" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + + params = + insert(:note_activity).data + |> Map.put("actor", "https://unavailable.example/users/bob") + + assert {:cancel, :actor_signature_mismatch} = + SignatureRetryWorker.perform( + failed_signature_job(params, signature_headers_for(alice)) + ) + end + + test "cancels retry jobs with a signature header without keyId" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + params = insert(:note_activity, user: alice).data + + req_headers = [{"signature", "algorithm=\"rsa-sha256\",signature=\"fake-signature\""}] + + assert {:cancel, :invalid_signature} = + SignatureRetryWorker.perform(failed_signature_job(params, req_headers)) + end + + test "cancels retry jobs with an unparsable signature keyId" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + params = insert(:note_activity, user: alice).data + req_headers = [{"signature", "keyId=\"not an activitypub id\",signature=\"fake-signature\""}] + + assert {:cancel, :invalid_signature} = + SignatureRetryWorker.perform(failed_signature_job(params, req_headers)) + end + + test "cancels when the refetched key still cannot validate the signature" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + + create = %{ + "type" => "Create", + "actor" => alice.ap_id, + "id" => "https://one.com/activities/invalid-signature-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => "https://one.com/objects/invalid-signature-note", + "actor" => alice.ap_id, + "attributedTo" => alice.ap_id, + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + stub_actor_fetch(alice) + + assert {:ok, oban_job} = enqueue_failed_signature(create, alice) + assert {:cancel, :invalid_signature} = SignatureRetryWorker.perform(oban_job) + refute Activity.get_by_ap_id(create["id"]) + end + + test "processes the activity after refetching a valid matching signature" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + + create = %{ + "type" => "Create", + "actor" => alice.ap_id, + "id" => "https://one.com/activities/valid-signature-create", + "context" => "https://one.com/contexts/valid-signature-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => "https://one.com/objects/valid-signature-note", + "actor" => alice.ap_id, + "attributedTo" => alice.ap_id, + "context" => "https://one.com/contexts/valid-signature-create", + "content" => "valid post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + expect_signature_from(alice) + + assert {:ok, oban_job} = enqueue_failed_signature(create, alice) + assert {:ok, %Activity{}} = SignatureRetryWorker.perform(oban_job) + assert Activity.get_by_ap_id(create["id"]) + end + + test "processes the activity when a real signature validates with a query string" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + + create = %{ + "type" => "Create", + "actor" => alice.ap_id, + "id" => "https://one.com/activities/valid-query-signature-create", + "context" => "https://one.com/contexts/valid-query-signature-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => "https://one.com/objects/valid-query-signature-note", + "actor" => alice.ap_id, + "attributedTo" => alice.ap_id, + "context" => "https://one.com/contexts/valid-query-signature-create", + "content" => "valid signed post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + stub_actor_fetch(alice) + + date = "Thu, 25 Jul 2024 13:33:31 GMT" + digest = "SHA-256=fake-digest" + + signature = + Signature.sign(alice, %{ + "(request-target)" => "post /inbox?foo=bar", + "content-type" => "application/activity+json", + date: date, + digest: digest, + host: "local.test" + }) + + req_headers = [ + ["host", "local.test"], + ["date", date], + ["digest", digest], + ["content-type", "application/activity+json"], + ["signature", signature] + ] + + assert {:ok, %Activity{}} = + SignatureRetryWorker.perform( + failed_signature_job(create, req_headers, query_string: "foo=bar") + ) + + assert Activity.get_by_ap_id(create["id"]) + end + + test "cancels when signature actor does not match payload actor" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + + note = + insert(:note, + user: bob, + object_local: false, + data: %{"id" => "https://two.com/objects/malicious-update-note"} + ) + + update = %{ + "type" => "Update", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/malicious-update", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data + } + + assert_mismatched_signature_cancelled(update, alice) + end + + test "cancels signature actor mismatch through Federator-created jobs" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + + note = + insert(:note, + user: bob, + object_local: false, + data: %{"id" => "https://two.com/objects/federator-malicious-note"} + ) + + update = %{ + "type" => "Update", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/federator-malicious-update", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data + } + + assert_mismatched_signature_cancelled(update, alice) + end + + test "cancels signature actor mismatch before processing a forged Create" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + + create = %{ + "type" => "Create", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => "https://two.com/objects/forged-note", + "actor" => bob.ap_id, + "attributedTo" => bob.ap_id, + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + assert_mismatched_signature_cancelled(create, alice) + end + + test "cancels signature actor mismatch before actually creating a forged post" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + + object_id = "https://two.com/objects/actually-forged-note" + + create = %{ + "type" => "Create", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/actually-forged-create", + "context" => "https://two.com/contexts/actually-forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => object_id, + "actor" => bob.ap_id, + "attributedTo" => bob.ap_id, + "context" => "https://two.com/contexts/actually-forged-create", + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + assert_mismatched_signature_cancelled(create, alice) + refute Object.get_by_ap_id(object_id) + end + + test "cancels signature actor mismatch before processing a forged Like" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + note = insert(:note) + + like = %{ + "type" => "Like", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/forged-like", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data["id"] + } + + assert_mismatched_signature_cancelled(like, alice) + end + + test "cancels signature actor mismatch before actually creating a forged Like" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + note = insert(:note) + + like = %{ + "type" => "Like", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/actually-forged-like", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data["id"] + } + + assert_mismatched_signature_cancelled(like, alice) + refute Activity.get_by_ap_id(like["id"]) + end + + test "cancels signature actor mismatch before processing a forged Announce" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + note = insert(:note) + + announce = %{ + "type" => "Announce", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/forged-announce", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data["id"] + } + + assert_mismatched_signature_cancelled(announce, alice) + end + + test "cancels signature actor mismatch before processing a forged Follow" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + followed = insert(:user) + + follow = %{ + "type" => "Follow", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/forged-follow", + "to" => [followed.ap_id], + "cc" => [], + "object" => followed.ap_id + } + + assert_mismatched_signature_cancelled(follow, alice) + end + + test "cancels signature actor mismatch before processing a forged Undo" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + + undo = %{ + "type" => "Undo", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/forged-undo", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => "https://two.com/activities/existing-bob-activity" + } + + assert_mismatched_signature_cancelled(undo, alice) + end +end From 4337e0eb1b40942c7860b9d32e5ecf1ddafdc0ba Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Fri, 1 May 2026 12:33:26 +0400 Subject: [PATCH 34/55] Fail closed on unresolved signed payloads Reject unknown remote Update targets and invalidate signed payloads when their signer identity cannot be mapped, avoiding crashes and fail-open signature state. --- .../object_validators/update_validator.ex | 4 ++++ .../plugs/mapped_signature_to_identity_plug.ex | 4 ++-- .../object_validators/update_handling_test.exs | 17 +++++++++++++++++ .../mapped_signature_to_identity_plug_test.exs | 12 ++++++++++++ 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex index 5586b74cf..ad3c0e3e2 100644 --- a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex @@ -101,6 +101,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do |> add_error(:object, "Can't be updated by this actor") end + nil -> + cng + |> add_error(:object, "Can't be updated by this actor") + true -> cng |> add_error(:object, "Update is neither for Object or Actor") diff --git a/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex b/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex index c6d531086..a688b1780 100644 --- a/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex +++ b/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex @@ -32,8 +32,8 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do # remove me once testsuite uses mapped capabilities instead of what we do now {:user, nil} -> Logger.debug("Failed to map identity from signature (lookup failure)") - Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{actor}") - conn + Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{inspect(actor)}") + assign(conn, :valid_signature, false) end end diff --git a/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs b/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs index f04f9cc61..9dec315b3 100644 --- a/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs @@ -90,6 +90,23 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do refute cng.valid? assert Keyword.has_key?(cng.errors, :object) end + + test "returns an error if the remote update target IRI is unknown" do + remote_user = insert(:user, local: false, ap_id: "https://example.com/users/alice") + + update = %{ + "type" => "Update", + "actor" => remote_user.ap_id, + "id" => "https://example.com/activities/update-unknown-object-iri", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => "https://example.com/objects/unknown-iri" + } + + assert {:error, %Ecto.Changeset{} = cng} = ObjectValidator.validate(update, local: false) + refute cng.valid? + assert Keyword.has_key?(cng.errors, :object) + end end describe "update note" do diff --git a/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs b/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs index 81c6b0c5d..df713762c 100644 --- a/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs +++ b/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs @@ -58,4 +58,16 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlugTest do assert conn.assigns.valid_signature == false refute Map.has_key?(conn.assigns, :user) end + + test "it considers a mapped identity to be invalid when embedded actor identity cannot be found" do + actor = "http://niu.moe/users/rye" + + conn = + build_conn(:post, "/doesntmattter", %{"actor" => %{"id" => actor}}) + |> set_signature(actor) + |> MappedSignatureToIdentityPlug.call(%{}) + + assert conn.assigns.valid_signature == false + refute Map.has_key?(conn.assigns, :user) + end end From 99b614a52e9e804f8e3e642fe9104a18cbcfe3e1 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Fri, 1 May 2026 13:39:59 +0400 Subject: [PATCH 35/55] Add spoofing fixes changelog entry --- changelog.d/activitypub-spoofing.security | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/activitypub-spoofing.security diff --git a/changelog.d/activitypub-spoofing.security b/changelog.d/activitypub-spoofing.security new file mode 100644 index 000000000..3e6baffb6 --- /dev/null +++ b/changelog.d/activitypub-spoofing.security @@ -0,0 +1 @@ +ActivityPub: Fixed failed-signature inbox retry handling and signer identity checks to prevent spoofed remote activities from being processed From a35aa6551e79539f9452455cc54aeed63c9ba8d4 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 2 May 2026 10:39:49 +0400 Subject: [PATCH 36/55] Fix Woodpecker path filters --- .woodpecker/lint.yaml | 4 ++-- .woodpecker/unit-testing-elixir-1.15.yaml | 4 ++-- .woodpecker/unit-testing-elixir-1.18.yaml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index b96d584ee..54de53873 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -1,9 +1,9 @@ when: - event: pull_request - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ] - event: push branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ] steps: mix-format: diff --git a/.woodpecker/unit-testing-elixir-1.15.yaml b/.woodpecker/unit-testing-elixir-1.15.yaml index 84046be41..43f30262a 100644 --- a/.woodpecker/unit-testing-elixir-1.15.yaml +++ b/.woodpecker/unit-testing-elixir-1.15.yaml @@ -1,9 +1,9 @@ when: - event: pull_request - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ] - event: push branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ] depends_on: - lint diff --git a/.woodpecker/unit-testing-elixir-1.18.yaml b/.woodpecker/unit-testing-elixir-1.18.yaml index 8dcec62fb..1963b36a5 100644 --- a/.woodpecker/unit-testing-elixir-1.18.yaml +++ b/.woodpecker/unit-testing-elixir-1.18.yaml @@ -1,9 +1,9 @@ when: - event: pull_request - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ] - event: push branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ] depends_on: - lint From 3dbc570471862ad1edad323f65215f04fa0b83bf Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 2 May 2026 11:57:04 +0400 Subject: [PATCH 37/55] Woodpecker CI: Publish update-compatible OTP releases --- .woodpecker/docker-combine.yaml | 2 +- .woodpecker/docker.yaml | 2 +- .woodpecker/otp-musl.yaml | 140 +++++++++++++- .woodpecker/otp.yaml | 140 +++++++++++++- rel/files/bin/pleroma_ctl | 23 ++- test/pleroma/pleroma_ctl_test.exs | 293 ++++++++++++++++++++++++++++++ 6 files changed, 578 insertions(+), 22 deletions(-) create mode 100644 test/pleroma/pleroma_ctl_test.exs diff --git a/.woodpecker/docker-combine.yaml b/.woodpecker/docker-combine.yaml index be8583763..161987d64 100644 --- a/.woodpecker/docker-combine.yaml +++ b/.woodpecker/docker-combine.yaml @@ -1,7 +1,7 @@ when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] + path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - event: tag - event: manual branch: stable diff --git a/.woodpecker/docker.yaml b/.woodpecker/docker.yaml index 317cb5fbb..4053be91d 100644 --- a/.woodpecker/docker.yaml +++ b/.woodpecker/docker.yaml @@ -1,7 +1,7 @@ when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] + path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - event: tag - event: manual branch: stable diff --git a/.woodpecker/otp-musl.yaml b/.woodpecker/otp-musl.yaml index c58eee66b..e0fb0fb99 100644 --- a/.woodpecker/otp-musl.yaml +++ b/.woodpecker/otp-musl.yaml @@ -1,7 +1,7 @@ when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ] - event: tag - event: manual branch: stable @@ -9,6 +9,7 @@ when: matrix: platform: - linux/amd64 + - linux/arm - linux/arm64 # This is needed for the when clauses below. @@ -27,6 +28,7 @@ variables: - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} - mix release --path release build_image_amd64: &build_image_amd64 docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 + build_image_arm: &build_image_arm docker.io/arm32v7/elixir:1.17.3-alpine build_image_arm64: &build_image_arm64 docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 artifacts_uploader_image: &artifacts_uploader_image docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 artifacts_uploader_settings: &artifacts_uploader_settings @@ -66,6 +68,32 @@ steps: - <<: *build_cmds - zip -9rq stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip release + otp-develop-arm-musl: + image: *build_image_arm + when: + - evaluate: 'platform == "linux/arm" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + environment: *env + commands: &arm_build + - <<: *build_cmds + - zip -9rq ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm-musl.zip release + + otp-stable-arm-musl: + image: *build_image_arm + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' + environment: *env + commands: *arm_build + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + otp-stable-tag-arm-musl: + image: *build_image_arm + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"' + environment: *env + commands: + - <<: *build_cmds + - zip -9rq stable-${CI_COMMIT_SHA:0:8}-arm-musl.zip release + otp-develop-arm64-musl: image: *build_image_arm64 when: @@ -102,7 +130,20 @@ steps: package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64-musl package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip - file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + + upload-latest-amd64-musl: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64-musl + package_version: latest + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + file_name: pleroma.zip + update: 'true' # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable upload-artifacts-tag-amd64-musl: @@ -114,7 +155,70 @@ steps: package_name: pleroma-otp-stable-amd64-musl package_version: stable-${CI_COMMIT_SHA:0:8}-amd64-musl file_source: ./stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip - file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-latest-tag-amd64-musl: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-amd64-musl + package_version: latest + file_source: ./stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + file_name: pleroma.zip + update: 'true' + + upload-artifacts-arm-musl: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm-musl + package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm-musl + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm-musl.zip + file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm-musl.zip + + upload-latest-arm-musl: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm-musl + package_version: latest + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm-musl.zip + file_name: pleroma.zip + update: 'true' + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-artifacts-tag-arm-musl: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-arm-musl + package_version: stable-${CI_COMMIT_SHA:0:8}-arm-musl + file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm-musl.zip + file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-arm-musl.zip + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-latest-tag-arm-musl: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-arm-musl + package_version: latest + file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm-musl.zip + file_name: pleroma.zip + update: 'true' upload-artifacts-arm64-musl: image: *artifacts_uploader_image @@ -126,7 +230,20 @@ steps: package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64-musl package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip - file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + + upload-latest-arm64-musl: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64-musl + package_version: latest + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + file_name: pleroma.zip + update: 'true' # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable upload-artifacts-tag-arm64-musl: @@ -138,4 +255,17 @@ steps: package_name: pleroma-otp-stable-arm64-musl package_version: stable-${CI_COMMIT_SHA:0:8}-arm64-musl file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip - file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-latest-tag-arm64-musl: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-arm64-musl + package_version: latest + file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + file_name: pleroma.zip + update: 'true' diff --git a/.woodpecker/otp.yaml b/.woodpecker/otp.yaml index c01c2e557..3c8b71814 100644 --- a/.woodpecker/otp.yaml +++ b/.woodpecker/otp.yaml @@ -1,7 +1,7 @@ when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ] - event: tag - event: manual branch: stable @@ -9,6 +9,7 @@ when: matrix: platform: - linux/amd64 + - linux/arm - linux/arm64 # This is needed for the when clauses below. @@ -27,6 +28,7 @@ variables: - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} - mix release --path release build_image_amd64: &build_image_amd64 docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 + build_image_arm: &build_image_arm docker.io/arm32v7/elixir:1.17.3 build_image_arm64: &build_image_arm64 docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 artifacts_uploader_image: &artifacts_uploader_image docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 artifacts_uploader_settings: &artifacts_uploader_settings @@ -67,6 +69,32 @@ steps: - <<: *build_cmds - zip -9rq stable-${CI_COMMIT_SHA:0:8}-amd64.zip release + otp-develop-arm: + image: *build_image_arm + when: + - evaluate: 'platform == "linux/arm" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + environment: *env + commands: &arm_build + - <<: *build_cmds + - zip -9rq ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm.zip release + + otp-stable-arm: + image: *build_image_arm + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' + environment: *env + commands: *arm_build + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + otp-stable-tag-arm: + image: *build_image_arm + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"' + environment: *env + commands: + - <<: *build_cmds + - zip -9rq stable-${CI_COMMIT_SHA:0:8}-arm.zip release + otp-develop-arm64: image: *build_image_arm64 when: @@ -103,7 +131,20 @@ steps: package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64 package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64 file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip - file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip + file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip + + upload-latest-amd64: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64 + package_version: latest + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip + file_name: pleroma.zip + update: 'true' # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable upload-artifacts-tag-amd64: @@ -115,7 +156,70 @@ steps: package_name: pleroma-otp-stable-amd64 package_version: stable-${CI_COMMIT_SHA:0:8}-amd64 file_source: ./stable-${CI_COMMIT_SHA:0:8}-amd64.zip - file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64.zip + file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64.zip + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-latest-tag-amd64: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-amd64 + package_version: latest + file_source: ./stable-${CI_COMMIT_SHA:0:8}-amd64.zip + file_name: pleroma.zip + update: 'true' + + upload-artifacts-arm: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm + package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm.zip + file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm.zip + + upload-latest-arm: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm + package_version: latest + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm.zip + file_name: pleroma.zip + update: 'true' + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-artifacts-tag-arm: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-arm + package_version: stable-${CI_COMMIT_SHA:0:8}-arm + file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm.zip + file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-arm.zip + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-latest-tag-arm: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-arm + package_version: latest + file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm.zip + file_name: pleroma.zip + update: 'true' upload-artifacts-arm64: image: *artifacts_uploader_image @@ -127,7 +231,20 @@ steps: package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64 package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64 file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip - file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip + file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip + + upload-latest-arm64: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64 + package_version: latest + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip + file_name: pleroma.zip + update: 'true' # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable upload-artifacts-tag-arm64: @@ -139,4 +256,17 @@ steps: package_name: pleroma-otp-stable-arm64 package_version: stable-${CI_COMMIT_SHA:0:8}-arm64 file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm64.zip - file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64.zip + file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64.zip + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-latest-tag-arm64: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-arm64 + package_version: latest + file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm64.zip + file_name: pleroma.zip + update: 'true' diff --git a/rel/files/bin/pleroma_ctl b/rel/files/bin/pleroma_ctl index 2e114ff50..f5d3f321b 100755 --- a/rel/files/bin/pleroma_ctl +++ b/rel/files/bin/pleroma_ctl @@ -79,18 +79,18 @@ update() { RELEASE_ROOT=$(dirname "$SCRIPTPATH") uri="https://git.pleroma.social" project_name="pleroma" - api_base="${uri}/api/v1/packages/${project_name}/generic" package_base="${uri}/api/packages/${project_name}/generic" - project_branch="${BRANCH:-$(detect_branch)}" - flavour="${FLAVOUR:-$(detect_flavour)}" - # API responds in JSON, optimistically try to make it one object per line - ver=$(curl -s "${api_base}"/pleroma-otp-"${project_branch}"-"${flavour}"/-/latest | tr ',' '\n' | grep '"version":' | cut -d':' -f2 | tr -d '"') - file=$(curl -s "${api_base}"/pleroma-otp-"${project_branch}"-"${flavour}"/"${ver}"/files | tr ',' '\n' | grep '"name":' | cut -d':' -f2 | tr -d '"') - full_uri=${FULL_URI:-"${package_base}"/pleroma-otp-"${project_branch}"-"${flavour}"/"${ver}"/"${file}"} + if [ -n "$FULL_URI" ]; then + full_uri="$FULL_URI" + else + project_branch="${BRANCH:-$(detect_branch)}" + flavour="${FLAVOUR:-$(detect_flavour)}" + full_uri="${package_base}"/pleroma-otp-"${project_branch}"-"${flavour}"/latest/pleroma.zip + fi tmp="${TMP_DIR:-/tmp}" artifact="$tmp/pleroma.zip" echo "Downloading the artifact from ${full_uri} to ${artifact}" - curl "$full_uri" -o "${artifact}" + curl -fL "$full_uri" -o "${artifact}" echo "Unpacking ${artifact} to ${tmp}" unzip -q "$artifact" -d "$tmp" echo "Copying files over to $RELEASE_ROOT" @@ -144,9 +144,12 @@ else # HACK: Script arguments need to be sent as an array to Mix tasks, otherwise they will break (quoted) arguments with whitespace. # Previously it was sent as string, which would get split on whitespace on the task side. - # Escaping does not help including non-POSIX printf %q + # Encode as Elixir binary literals to avoid string escaping and interpolation issues. PREPARED_ARGS="" - for arg in "$@"; do PREPARED_ARGS="$PREPARED_ARGS \"$arg\","; done + for arg in "$@"; do + bytes=$(printf '%s' "$arg" | od -An -v -tu1 | tr -s '[:space:]' ',' | sed 's/^,//; s/,$//') + PREPARED_ARGS="$PREPARED_ARGS <<$bytes>>," + done ACTION="$1" if [ $# -gt 0 ]; then diff --git a/test/pleroma/pleroma_ctl_test.exs b/test/pleroma/pleroma_ctl_test.exs new file mode 100644 index 000000000..d96396399 --- /dev/null +++ b/test/pleroma/pleroma_ctl_test.exs @@ -0,0 +1,293 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.PleromaCtlTest do + use ExUnit.Case, async: false + + @pleroma_ctl Path.expand("../../rel/files/bin/pleroma_ctl", __DIR__) + + setup do + tmp_dir = + Path.join(System.tmp_dir!(), "pleroma_ctl_test_#{System.unique_integer([:positive])}") + + release_root = Path.join(tmp_dir, "release") + bin_dir = Path.join(release_root, "bin") + + File.mkdir_p!(bin_dir) + File.cp!(@pleroma_ctl, Path.join(bin_dir, "pleroma_ctl")) + File.chmod!(Path.join(bin_dir, "pleroma_ctl"), 0o755) + + on_exit(fn -> File.rm_rf!(tmp_dir) end) + + {:ok, tmp_dir: tmp_dir, release_root: release_root, bin_dir: bin_dir} + end + + test "update downloads branch-scoped latest OTP package", %{ + tmp_dir: tmp_dir, + bin_dir: bin_dir, + release_root: release_root + } do + stubs_dir = Path.join(tmp_dir, "stubs") + curl_args_path = Path.join(tmp_dir, "curl.args") + File.mkdir_p!(stubs_dir) + write_update_stubs(stubs_dir, curl_args_path, "unused", "glibc") + + update_tmp_dir = Path.join(tmp_dir, "update_tmp") + File.mkdir_p!(update_tmp_dir) + + {output, status} = + System.cmd( + Path.join(bin_dir, "pleroma_ctl"), + [ + "update", + "--branch", + "develop", + "--flavour", + "amd64", + "--tmp-dir", + update_tmp_dir, + "--no-rm" + ], + env: [{"PATH", stubs_dir <> ":" <> System.get_env("PATH", "")}], + stderr_to_stdout: true + ) + + assert status == 0, output + assert File.exists?(Path.join(release_root, "bin/marker")) + + assert ["-fL", url, "-o", artifact_path] = + curl_args_path + |> File.read!() + |> String.split("\n", trim: true) + + assert url == + "https://git.pleroma.social/api/packages/pleroma/generic/pleroma-otp-develop-amd64/latest/pleroma.zip" + + assert artifact_path == Path.join(update_tmp_dir, "pleroma.zip") + end + + test "update detects stable branch and local flavour", %{ + tmp_dir: tmp_dir, + bin_dir: bin_dir, + release_root: release_root + } do + stubs_dir = Path.join(tmp_dir, "stubs") + curl_args_path = Path.join(tmp_dir, "curl.args") + File.mkdir_p!(stubs_dir) + write_update_stubs(stubs_dir, curl_args_path, "x86_64", "glibc") + write_start_erl_data(release_root, "2.10.0") + + {output, status} = + System.cmd( + Path.join(bin_dir, "pleroma_ctl"), + ["update", "--tmp-dir", create_update_tmp_dir(tmp_dir), "--no-rm"], + env: [{"PATH", stubs_dir <> ":" <> System.get_env("PATH", "")}], + stderr_to_stdout: true + ) + + assert status == 0, output + + assert curl_url(curl_args_path) == + "https://git.pleroma.social/api/packages/pleroma/generic/pleroma-otp-stable-amd64/latest/pleroma.zip" + end + + test "update detects develop branch and musl arm flavour", %{ + tmp_dir: tmp_dir, + bin_dir: bin_dir, + release_root: release_root + } do + stubs_dir = Path.join(tmp_dir, "stubs") + curl_args_path = Path.join(tmp_dir, "curl.args") + File.mkdir_p!(stubs_dir) + write_update_stubs(stubs_dir, curl_args_path, "armv7l", "musl") + write_start_erl_data(release_root, "2.10.0.develop") + + {output, status} = + System.cmd( + Path.join(bin_dir, "pleroma_ctl"), + ["update", "--tmp-dir", create_update_tmp_dir(tmp_dir), "--no-rm"], + env: [{"PATH", stubs_dir <> ":" <> System.get_env("PATH", "")}], + stderr_to_stdout: true + ) + + assert status == 0, output + + assert curl_url(curl_args_path) == + "https://git.pleroma.social/api/packages/pleroma/generic/pleroma-otp-develop-arm-musl/latest/pleroma.zip" + end + + test "update with zip URL bypasses branch and flavour detection", %{ + tmp_dir: tmp_dir, + bin_dir: bin_dir, + release_root: release_root + } do + stubs_dir = Path.join(tmp_dir, "stubs") + curl_args_path = Path.join(tmp_dir, "curl.args") + File.mkdir_p!(stubs_dir) + write_update_stubs(stubs_dir, curl_args_path, "unsupported-arch", "unsupported-libc") + write_start_erl_data(release_root, "2.10.0.custombranch") + + custom_url = "https://example.test/custom.zip" + + {output, status} = + System.cmd( + Path.join(bin_dir, "pleroma_ctl"), + [ + "update", + "--zip-url", + custom_url, + "--tmp-dir", + create_update_tmp_dir(tmp_dir), + "--no-rm" + ], + env: [{"PATH", stubs_dir <> ":" <> System.get_env("PATH", "")}], + stderr_to_stdout: true + ) + + assert status == 0, output + assert curl_url(curl_args_path) == custom_url + end + + test "passes arguments with spaces and Elixir string metacharacters", %{ + tmp_dir: tmp_dir, + bin_dir: bin_dir + } do + capture_path = Path.join(tmp_dir, "captured_args") + eval_path = Path.join(tmp_dir, "pleroma_ctl_eval.exs") + + write_executable(Path.join(bin_dir, "pleroma"), """ + #!/bin/sh + { + printf '%s\n' 'defmodule Pleroma.ReleaseTasks do' + printf '%s\n' ' def run(args), do: File.write!(System.fetch_env!("PLEROMA_CTL_CAPTURE"), :erlang.term_to_binary(args))' + printf '%s\n' 'end' + printf '%s\n' "$2" + } > "$PLEROMA_CTL_EVAL_FILE" + + exec elixir "$PLEROMA_CTL_EVAL_FILE" + """) + + {output, status} = + System.cmd( + Path.join(bin_dir, "pleroma_ctl"), + [ + "user", + "", + "has space", + ~s(has "quote"), + ~s(has \\ backslash), + ~S(#{:not_interpolated}) + ], + env: [ + {"PLEROMA_CTL_CAPTURE", capture_path}, + {"PLEROMA_CTL_EVAL_FILE", eval_path} + ], + stderr_to_stdout: true + ) + + assert status == 0, output + + assert capture_path + |> File.read!() + |> :erlang.binary_to_term() == [ + "user", + "", + "has space", + ~s(has "quote"), + ~s(has \\ backslash), + ~S(#{:not_interpolated}) + ] + end + + defp write_executable(path, contents) do + File.write!(path, contents) + File.chmod!(path, 0o755) + end + + defp write_start_erl_data(release_root, version) do + releases_dir = Path.join(release_root, "releases") + File.mkdir_p!(releases_dir) + File.write!(Path.join(releases_dir, "start_erl.data"), "erts-15.0 #{version}\n") + end + + defp create_update_tmp_dir(tmp_dir) do + update_tmp_dir = Path.join(tmp_dir, "update_tmp") + File.mkdir_p!(update_tmp_dir) + update_tmp_dir + end + + defp write_update_stubs(stubs_dir, curl_args_path, arch, libc) do + write_executable(Path.join(stubs_dir, "curl"), """ + #!/bin/sh + printf '%s\n' "$@" > "#{curl_args_path}" + + while [ $# -gt 0 ]; do + case "$1" in + -o) + artifact="$2" + shift 2 + ;; + *) + shift + ;; + esac + done + + : > "$artifact" + """) + + write_executable(Path.join(stubs_dir, "unzip"), """ + #!/bin/sh + while [ $# -gt 0 ]; do + case "$1" in + -d) + dest="$2" + shift 2 + ;; + *) + shift + ;; + esac + done + + mkdir -p "$dest/release/bin" + printf 'marker' > "$dest/release/bin/marker" + """) + + write_executable(Path.join(stubs_dir, "uname"), """ + #!/bin/sh + printf '%s\n' '#{arch}' + """) + + write_executable(Path.join(stubs_dir, "getconf"), getconf_stub(libc)) + + write_executable(Path.join(stubs_dir, "ldd"), """ + #!/bin/sh + printf '%s\n' 'musl libc (mock)' + """) + end + + defp getconf_stub("glibc") do + """ + #!/bin/sh + printf '%s\n' 'glibc 2.40' + """ + end + + defp getconf_stub(_libc) do + """ + #!/bin/sh + exit 1 + """ + end + + defp curl_url(curl_args_path) do + ["-fL", url, "-o", _artifact_path] = + curl_args_path + |> File.read!() + |> String.split("\n", trim: true) + + url + end +end From 47e6dbfadece58a277280a9910b340ec589e6507 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 2 May 2026 13:38:56 +0400 Subject: [PATCH 38/55] Woodpecker CI: Work around script entrypoint truncation --- .woodpecker/changelog.yaml | 7 +++++++ .woodpecker/lint.yaml | 9 +++++++++ .woodpecker/otp-musl.yaml | 13 +++++++++++++ .woodpecker/otp.yaml | 13 +++++++++++++ .woodpecker/unit-testing-elixir-1.15.yaml | 7 +++++++ .woodpecker/unit-testing-elixir-1.18.yaml | 7 +++++++ 6 files changed, 56 insertions(+) diff --git a/.woodpecker/changelog.yaml b/.woodpecker/changelog.yaml index 4f38ce618..1d65e8e9f 100644 --- a/.woodpecker/changelog.yaml +++ b/.woodpecker/changelog.yaml @@ -1,9 +1,16 @@ when: - event: pull_request +variables: + script_file_entrypoint: &script_file_entrypoint + - /bin/sh + - -c + - 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh' + steps: check-changelog: image: docker.io/alpine:3.23 + entrypoint: *script_file_entrypoint commands: - apk add --no-cache git - sh ./tools/check-changelog diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index b96d584ee..95325d779 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -5,10 +5,17 @@ when: branch: ${CI_REPO_DEFAULT_BRANCH} path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] +variables: + script_file_entrypoint: &script_file_entrypoint + - /bin/sh + - -c + - 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh' + steps: mix-format: image: &elixir-image docker.io/elixir:1.15-alpine + entrypoint: *script_file_entrypoint failure: ignore commands: - | @@ -19,6 +26,7 @@ steps: credo: image: *elixir-image + entrypoint: *script_file_entrypoint failure: ignore environment: MIX_ENV: test @@ -55,6 +63,7 @@ steps: ensure-status: image: *elixir-image + entrypoint: *script_file_entrypoint commands: | if test -f fail.stamp; then echo "One or more previous steps fails. Failing workflow..." diff --git a/.woodpecker/otp-musl.yaml b/.woodpecker/otp-musl.yaml index e0fb0fb99..877860255 100644 --- a/.woodpecker/otp-musl.yaml +++ b/.woodpecker/otp-musl.yaml @@ -18,6 +18,10 @@ labels: platform: ${platform} variables: + script_file_entrypoint: &script_file_entrypoint + - /bin/sh + - -c + - 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh' build_cmds: &build_cmds - apk add git build-base cmake file-dev openssl vips-dev zip - echo "import Config" > config/prod.secret.exs @@ -44,6 +48,7 @@ variables: steps: otp-develop-amd64-musl: image: *build_image_amd64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/amd64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' environment: *env @@ -53,6 +58,7 @@ steps: otp-stable-amd64-musl: image: *build_image_amd64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' environment: *env @@ -61,6 +67,7 @@ steps: # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable otp-stable-tag-amd64-musl: image: *build_image_amd64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' environment: *env @@ -70,6 +77,7 @@ steps: otp-develop-arm-musl: image: *build_image_arm + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' environment: *env @@ -79,6 +87,7 @@ steps: otp-stable-arm-musl: image: *build_image_arm + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' environment: *env @@ -87,6 +96,7 @@ steps: # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable otp-stable-tag-arm-musl: image: *build_image_arm + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"' environment: *env @@ -96,6 +106,7 @@ steps: otp-develop-arm64-musl: image: *build_image_arm64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' environment: *env @@ -105,6 +116,7 @@ steps: otp-stable-arm64-musl: image: *build_image_arm64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' environment: *env @@ -113,6 +125,7 @@ steps: # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable otp-stable-tag-arm64-musl: image: *build_image_arm64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' environment: *env diff --git a/.woodpecker/otp.yaml b/.woodpecker/otp.yaml index 3c8b71814..766e42e03 100644 --- a/.woodpecker/otp.yaml +++ b/.woodpecker/otp.yaml @@ -18,6 +18,10 @@ labels: platform: ${platform} variables: + script_file_entrypoint: &script_file_entrypoint + - /bin/sh + - -c + - 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh' build_cmds: &build_cmds - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - echo "import Config" > config/prod.secret.exs @@ -45,6 +49,7 @@ variables: steps: otp-develop-amd64: image: *build_image_amd64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/amd64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' environment: *env @@ -54,6 +59,7 @@ steps: otp-stable-amd64: image: *build_image_amd64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' environment: *env @@ -62,6 +68,7 @@ steps: # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable otp-stable-tag-amd64: image: *build_image_amd64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' environment: *env @@ -71,6 +78,7 @@ steps: otp-develop-arm: image: *build_image_arm + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' environment: *env @@ -80,6 +88,7 @@ steps: otp-stable-arm: image: *build_image_arm + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' environment: *env @@ -88,6 +97,7 @@ steps: # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable otp-stable-tag-arm: image: *build_image_arm + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"' environment: *env @@ -97,6 +107,7 @@ steps: otp-develop-arm64: image: *build_image_arm64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' environment: *env @@ -106,6 +117,7 @@ steps: otp-stable-arm64: image: *build_image_arm64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' environment: *env @@ -114,6 +126,7 @@ steps: # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable otp-stable-tag-arm64: image: *build_image_arm64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' environment: *env diff --git a/.woodpecker/unit-testing-elixir-1.15.yaml b/.woodpecker/unit-testing-elixir-1.15.yaml index 84046be41..9d5cf7102 100644 --- a/.woodpecker/unit-testing-elixir-1.15.yaml +++ b/.woodpecker/unit-testing-elixir-1.15.yaml @@ -8,9 +8,16 @@ when: depends_on: - lint +variables: + script_file_entrypoint: &script_file_entrypoint + - /bin/sh + - -c + - 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh' + steps: unit-testing-elixir-1.15: image: elixir:1.15-alpine + entrypoint: *script_file_entrypoint environment: MIX_ENV: test DB_HOST: postgres diff --git a/.woodpecker/unit-testing-elixir-1.18.yaml b/.woodpecker/unit-testing-elixir-1.18.yaml index 8dcec62fb..8b0451ef0 100644 --- a/.woodpecker/unit-testing-elixir-1.18.yaml +++ b/.woodpecker/unit-testing-elixir-1.18.yaml @@ -8,9 +8,16 @@ when: depends_on: - lint +variables: + script_file_entrypoint: &script_file_entrypoint + - /bin/sh + - -c + - 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh' + steps: unit-testing-elixir-1.18: image: elixir:1.18-otp-27-alpine + entrypoint: *script_file_entrypoint environment: MIX_ENV: test DB_HOST: postgres From ee18feef7c2a3e0953880b7d7350d49aa463fc55 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 2 May 2026 15:04:40 +0400 Subject: [PATCH 39/55] Woodpecker CI: Allow manual develop release runs --- .woodpecker/docker-combine.yaml | 4 ++++ .woodpecker/docker.yaml | 2 ++ .woodpecker/otp-musl.yaml | 2 ++ .woodpecker/otp.yaml | 2 ++ 4 files changed, 10 insertions(+) diff --git a/.woodpecker/docker-combine.yaml b/.woodpecker/docker-combine.yaml index 161987d64..c6493df1c 100644 --- a/.woodpecker/docker-combine.yaml +++ b/.woodpecker/docker-combine.yaml @@ -3,6 +3,8 @@ when: branch: ${CI_REPO_DEFAULT_BRANCH} path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - event: tag + - event: manual + branch: ${CI_REPO_DEFAULT_BRANCH} - event: manual branch: stable @@ -17,6 +19,8 @@ steps: when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} + - event: manual + branch: ${CI_REPO_DEFAULT_BRANCH} settings: &docker_settings registry: "git.pleroma.social" image: "pleroma/pleroma" diff --git a/.woodpecker/docker.yaml b/.woodpecker/docker.yaml index 4053be91d..abc6bfa3b 100644 --- a/.woodpecker/docker.yaml +++ b/.woodpecker/docker.yaml @@ -3,6 +3,8 @@ when: branch: ${CI_REPO_DEFAULT_BRANCH} path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - event: tag + - event: manual + branch: ${CI_REPO_DEFAULT_BRANCH} - event: manual branch: stable diff --git a/.woodpecker/otp-musl.yaml b/.woodpecker/otp-musl.yaml index 877860255..6f007a127 100644 --- a/.woodpecker/otp-musl.yaml +++ b/.woodpecker/otp-musl.yaml @@ -3,6 +3,8 @@ when: branch: ${CI_REPO_DEFAULT_BRANCH} path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ] - event: tag + - event: manual + branch: ${CI_REPO_DEFAULT_BRANCH} - event: manual branch: stable diff --git a/.woodpecker/otp.yaml b/.woodpecker/otp.yaml index 766e42e03..7e1bad611 100644 --- a/.woodpecker/otp.yaml +++ b/.woodpecker/otp.yaml @@ -3,6 +3,8 @@ when: branch: ${CI_REPO_DEFAULT_BRANCH} path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ] - event: tag + - event: manual + branch: ${CI_REPO_DEFAULT_BRANCH} - event: manual branch: stable From 9fdad779b5f316d9a5c3c5f8d966a8c4c99fc40d Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 2 May 2026 16:12:47 +0400 Subject: [PATCH 40/55] Woodpecker CI: Run Docker manifest combine on amd64 --- .woodpecker/docker-combine.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.woodpecker/docker-combine.yaml b/.woodpecker/docker-combine.yaml index c6493df1c..6c91d26e9 100644 --- a/.woodpecker/docker-combine.yaml +++ b/.woodpecker/docker-combine.yaml @@ -13,6 +13,9 @@ depends_on: skip_clone: true +labels: + platform: linux/amd64 + steps: docker-develop-combine: image: git.fluffytail.org/phnt/wpc-docker-tagger:latest From 50651284a2988a5e806a0e802ad2257a9595f3a2 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 2 May 2026 17:21:37 +0400 Subject: [PATCH 41/55] Woodpecker CI: Run generic workflows on amd64 --- .woodpecker/changelog.yaml | 3 +++ .woodpecker/lint.yaml | 3 +++ .woodpecker/unit-testing-elixir-1.15.yaml | 3 +++ .woodpecker/unit-testing-elixir-1.18.yaml | 3 +++ 4 files changed, 12 insertions(+) diff --git a/.woodpecker/changelog.yaml b/.woodpecker/changelog.yaml index 1d65e8e9f..64062f17e 100644 --- a/.woodpecker/changelog.yaml +++ b/.woodpecker/changelog.yaml @@ -1,6 +1,9 @@ when: - event: pull_request +labels: + platform: linux/amd64 + variables: script_file_entrypoint: &script_file_entrypoint - /bin/sh diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index 95325d779..7b2a1683c 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -5,6 +5,9 @@ when: branch: ${CI_REPO_DEFAULT_BRANCH} path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] +labels: + platform: linux/amd64 + variables: script_file_entrypoint: &script_file_entrypoint - /bin/sh diff --git a/.woodpecker/unit-testing-elixir-1.15.yaml b/.woodpecker/unit-testing-elixir-1.15.yaml index 9d5cf7102..5c6be764d 100644 --- a/.woodpecker/unit-testing-elixir-1.15.yaml +++ b/.woodpecker/unit-testing-elixir-1.15.yaml @@ -8,6 +8,9 @@ when: depends_on: - lint +labels: + platform: linux/amd64 + variables: script_file_entrypoint: &script_file_entrypoint - /bin/sh diff --git a/.woodpecker/unit-testing-elixir-1.18.yaml b/.woodpecker/unit-testing-elixir-1.18.yaml index 8b0451ef0..2a40f41f7 100644 --- a/.woodpecker/unit-testing-elixir-1.18.yaml +++ b/.woodpecker/unit-testing-elixir-1.18.yaml @@ -8,6 +8,9 @@ when: depends_on: - lint +labels: + platform: linux/amd64 + variables: script_file_entrypoint: &script_file_entrypoint - /bin/sh From 4acd8c4e7204e3eee50568f23f53bff053b9206e Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 2 May 2026 21:08:04 +0400 Subject: [PATCH 42/55] Log failed-signature retry rejections --- lib/pleroma/workers/signature_retry_worker.ex | 224 +++++++++++++----- .../workers/signature_retry_worker_test.exs | 100 +++++++- 2 files changed, 257 insertions(+), 67 deletions(-) diff --git a/lib/pleroma/workers/signature_retry_worker.ex b/lib/pleroma/workers/signature_retry_worker.ex index 56673a514..2c4c097dd 100644 --- a/lib/pleroma/workers/signature_retry_worker.ex +++ b/lib/pleroma/workers/signature_retry_worker.ex @@ -10,6 +10,8 @@ defmodule Pleroma.Workers.SignatureRetryWorker do alias Pleroma.Web.Federator alias Pleroma.Web.Plugs.MappedSignatureToIdentityPlug + require Logger + use Oban.Worker, queue: :federator_incoming, max_attempts: 5, unique: [period: :infinity] @impl true @@ -25,37 +27,55 @@ defmodule Pleroma.Workers.SignatureRetryWorker do }) when is_binary(method) and is_map(params) and is_list(req_headers) and is_binary(request_path) and is_binary(query_string) do - with {:ok, req_headers} <- normalize_req_headers(req_headers), - conn_data = %Plug.Conn{ - assigns: %{valid_signature: true}, - method: method, - params: params, - req_headers: req_headers, - request_path: request_path, - query_string: query_string - }, - actor_id = Utils.get_ap_id(params["actor"]), - {:signature_actor, {:ok, signature_actor_id}} <- - {:signature_actor, signature_actor_id(conn_data)}, - {:same_actor, true} <- {:same_actor, signature_actor_id == actor_id}, - {:ok, %User{}} <- User.get_or_fetch_by_ap_id(actor_id), - {:ok, _public_key} <- Signature.refetch_public_key(conn_data), - {:signature, true} <- {:signature, validate_signature(conn_data)}, - {:same_actor, true} <- {:same_actor, validate_same_actor(conn_data)}, - {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do - unless Instances.reachable?(params["actor"]) do - domain = URI.parse(params["actor"]).host - Oban.insert(Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain})) - end + case normalize_req_headers(req_headers) do + {:ok, req_headers} -> + conn_data = %Plug.Conn{ + assigns: %{valid_signature: true}, + method: method, + params: params, + req_headers: req_headers, + request_path: request_path, + query_string: query_string + } - {:ok, res} - else - e -> process_errors(e) + signature_actor_result = signature_actor_id(conn_data) + + with actor_id = Utils.get_ap_id(params["actor"]), + {:signature_actor, {:ok, signature_actor_id}} <- + {:signature_actor, signature_actor_result}, + {:same_actor, true} <- {:same_actor, signature_actor_id == actor_id}, + {:ok, %User{}} <- User.get_or_fetch_by_ap_id(actor_id), + {:ok, _public_key} <- Signature.refetch_public_key(conn_data), + {:signature, true} <- {:signature, validate_signature(conn_data)}, + {:same_actor, true} <- {:same_actor, validate_same_actor(conn_data)}, + {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do + unless Instances.reachable?(params["actor"]) do + domain = URI.parse(params["actor"]).host + Oban.insert(Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain})) + end + + {:ok, res} + else + e -> process_errors(e, retry_log_context(params, request_path, signature_actor_result)) + end + + e -> + process_errors(e, retry_log_context(params, request_path, nil)) end end - def perform(%Job{args: %{"op" => "incoming_failed_signature_ap_doc"}}) do - process_errors(:missing_signature_retry_metadata) + def perform(%Job{args: %{"op" => "incoming_failed_signature_ap_doc"} = args}) do + process_errors( + :missing_signature_retry_metadata, + retry_log_context(Map.get(args, "params"), Map.get(args, "request_path"), nil) + ) + end + + def perform(%Job{args: args}) when is_map(args) do + process_errors( + :missing_signature_retry_metadata, + retry_log_context(Map.get(args, "params"), Map.get(args, "request_path"), nil) + ) end def perform(%Job{}), do: process_errors(:missing_signature_retry_metadata) @@ -109,36 +129,126 @@ defmodule Pleroma.Workers.SignatureRetryWorker do _, _ -> {:error, :invalid_signature} end - defp process_errors({:error, {:error, _} = error}), do: process_errors(error) + defp process_errors(errors, context \\ %{}) - defp process_errors(errors) do - case errors do - # User fetch failures - {:error, :not_found} = reason -> {:cancel, reason} - {:error, :forbidden} = reason -> {:cancel, reason} - # Inactive user - {:error, {:user_active, false} = reason} -> {:cancel, reason} - # Validator will error and return a changeset error - # e.g., duplicate activities or if the object was deleted - {:error, {:validate, {:error, _changeset} = reason}} -> {:cancel, reason} - # Duplicate detection during Normalization - {:error, :already_present} -> {:cancel, :already_present} - # MRFs will return a reject - {:error, {:reject, _} = reason} -> {:cancel, reason} - # HTTP Sigs - {:signature_actor, {:error, _}} -> {:cancel, :invalid_signature} - {:signature, false} -> {:cancel, :invalid_signature} - {:same_actor, false} -> {:cancel, :actor_signature_mismatch} - # Origin / URL validation failed somewhere possibly due to spoofing - {:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed} - # Unclear if this can be reached - {:error, {:side_effects, {:error, :no_object_actor}} = reason} -> {:cancel, reason} - # Fail closed if the retry cannot reconstruct the original request. - :missing_signature_retry_metadata -> {:cancel, :missing_signature_retry_metadata} - {:error, :invalid_signature_retry_metadata} -> {:cancel, :invalid_signature_retry_metadata} - # Catchall - {:error, _} = e -> e - e -> {:error, e} - end + defp process_errors({:error, {:error, _} = error}, context), do: process_errors(error, context) + + defp process_errors(errors, context) do + result = + case errors do + # User fetch failures + {:error, :not_found} = reason -> + {:cancel, reason} + + {:error, :forbidden} = reason -> + {:cancel, reason} + + # Inactive user + {:error, {:user_active, false} = reason} -> + {:cancel, reason} + + # Validator will error and return a changeset error + # e.g., duplicate activities or if the object was deleted + {:error, {:validate, {:error, _changeset} = reason}} -> + {:cancel, reason} + + # Duplicate detection during Normalization + {:error, :already_present} -> + {:cancel, :already_present} + + # MRFs will return a reject + {:error, {:reject, _} = reason} -> + {:cancel, reason} + + # HTTP Sigs + {:signature_actor, {:error, _}} -> + {:cancel, :invalid_signature} + + {:signature, false} -> + {:cancel, :invalid_signature} + + {:same_actor, false} -> + {:cancel, :actor_signature_mismatch} + + # Origin / URL validation failed somewhere possibly due to spoofing + {:error, :origin_containment_failed} -> + {:cancel, :origin_containment_failed} + + # Unclear if this can be reached + {:error, {:side_effects, {:error, :no_object_actor}} = reason} -> + {:cancel, reason} + + # Fail closed if the retry cannot reconstruct the original request. + :missing_signature_retry_metadata -> + {:cancel, :missing_signature_retry_metadata} + + {:error, :invalid_signature_retry_metadata} -> + {:cancel, :invalid_signature_retry_metadata} + + # Catchall + {:error, _} = e -> + e + + e -> + {:error, e} + end + + log_signature_retry_rejection(result, context) + result end + + defp retry_log_context(params, request_path, signature_actor_result) when is_map(params) do + signature_actor = + case signature_actor_result do + {:ok, actor} when is_binary(actor) -> actor + actor when is_binary(actor) -> actor + _ -> nil + end + + %{ + activity_id: params["id"], + payload_actor: Utils.get_ap_id(params["actor"]), + request_path: request_path, + signature_actor: signature_actor, + type: params["type"] + } + end + + defp retry_log_context(_params, request_path, signature_actor_result) do + signature_actor = + case signature_actor_result do + {:ok, actor} when is_binary(actor) -> actor + actor when is_binary(actor) -> actor + _ -> nil + end + + %{ + activity_id: nil, + payload_actor: nil, + request_path: request_path, + signature_actor: signature_actor, + type: nil + } + end + + defp log_signature_retry_rejection({:cancel, reason}, context) + when reason in [ + :actor_signature_mismatch, + :invalid_signature, + :invalid_signature_retry_metadata, + :missing_signature_retry_metadata, + :origin_containment_failed + ] do + Logger.warning( + "Failed-signature inbox retry rejected " <> + "reason=#{inspect(reason)} " <> + "payload_actor=#{inspect(context[:payload_actor])} " <> + "signature_actor=#{inspect(context[:signature_actor])} " <> + "activity_id=#{inspect(context[:activity_id])} " <> + "type=#{inspect(context[:type])} " <> + "request_path=#{inspect(context[:request_path])}" + ) + end + + defp log_signature_retry_rejection(_result, _context), do: :ok end diff --git a/test/pleroma/workers/signature_retry_worker_test.exs b/test/pleroma/workers/signature_retry_worker_test.exs index 02706ebad..f4ec0e2e3 100644 --- a/test/pleroma/workers/signature_retry_worker_test.exs +++ b/test/pleroma/workers/signature_retry_worker_test.exs @@ -6,8 +6,11 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do use Pleroma.DataCase, async: false use Oban.Testing, repo: Pleroma.Repo + import ExUnit.CaptureLog import Pleroma.Factory + @moduletag capture_log: true + alias Pleroma.Activity alias Pleroma.Object alias Pleroma.Signature @@ -73,7 +76,9 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do defp assert_mismatched_signature_cancelled(params, signer) do assert {:ok, oban_job} = enqueue_failed_signature(params, signer) - assert {:cancel, :actor_signature_mismatch} = SignatureRetryWorker.perform(oban_job) + capture_log([level: :warning], fn -> + assert {:cancel, :actor_signature_mismatch} = SignatureRetryWorker.perform(oban_job) + end) end test "Federator preserves request metadata for failed-signature retry jobs" do @@ -108,25 +113,54 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do test "cancels retry jobs without request metadata" do params = insert(:note_activity).data - assert {:cancel, :missing_signature_retry_metadata} = - SignatureRetryWorker.perform(%Oban.Job{ - args: %{"op" => "incoming_failed_signature_ap_doc", "params" => params} - }) + log = + capture_log([level: :warning], fn -> + assert {:cancel, :missing_signature_retry_metadata} = + SignatureRetryWorker.perform(%Oban.Job{ + args: %{"op" => "incoming_failed_signature_ap_doc", "params" => params} + }) + end) + + assert log =~ "Failed-signature inbox retry rejected" + assert log =~ "reason=:missing_signature_retry_metadata" + assert log =~ "payload_actor=#{inspect(params["actor"])}" + assert log =~ "activity_id=#{inspect(params["id"])}" + assert log =~ "type=#{inspect(params["type"])}" + assert log =~ "request_path=nil" end test "cancels retry jobs with malformed serialized request headers" do params = insert(:note_activity).data - assert {:cancel, :invalid_signature_retry_metadata} = - SignatureRetryWorker.perform(failed_signature_job(params, [["signature"]])) + log = + capture_log([level: :warning], fn -> + assert {:cancel, :invalid_signature_retry_metadata} = + SignatureRetryWorker.perform(failed_signature_job(params, [["signature"]])) + end) + + assert log =~ "Failed-signature inbox retry rejected" + assert log =~ "reason=:invalid_signature_retry_metadata" + assert log =~ "signature_actor=nil" + assert log =~ "request_path=\"/inbox\"" end test "cancels retry jobs without a signature header" do alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") params = insert(:note_activity, user: alice).data - assert {:cancel, :invalid_signature} = - SignatureRetryWorker.perform(failed_signature_job(params, [{"host", "local.test"}])) + log = + capture_log([level: :warning], fn -> + assert {:cancel, :invalid_signature} = + SignatureRetryWorker.perform( + failed_signature_job(params, [{"host", "local.test"}]) + ) + end) + + assert log =~ "Failed-signature inbox retry rejected" + assert log =~ "reason=:invalid_signature" + assert log =~ "payload_actor=#{inspect(params["actor"])}" + assert log =~ "signature_actor=nil" + assert log =~ "request_path=\"/inbox\"" end test "cancels missing signature before fetching an unavailable payload actor" do @@ -194,7 +228,20 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do stub_actor_fetch(alice) assert {:ok, oban_job} = enqueue_failed_signature(create, alice) - assert {:cancel, :invalid_signature} = SignatureRetryWorker.perform(oban_job) + + log = + capture_log([level: :warning], fn -> + assert {:cancel, :invalid_signature} = SignatureRetryWorker.perform(oban_job) + end) + + assert log =~ "Failed-signature inbox retry rejected" + assert log =~ "reason=:invalid_signature" + assert log =~ "payload_actor=\"https://one.com/users/alice\"" + assert log =~ "signature_actor=\"https://one.com/users/alice\"" + assert log =~ "activity_id=\"https://one.com/activities/invalid-signature-create\"" + assert log =~ "type=\"Create\"" + assert log =~ "request_path=\"/inbox\"" + refute Activity.get_by_ap_id(create["id"]) end @@ -352,6 +399,39 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do assert_mismatched_signature_cancelled(create, alice) end + test "logs signature actor mismatch retry rejections" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + + create = %{ + "type" => "Create", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/logged-forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => "https://two.com/objects/logged-forged-note", + "actor" => bob.ap_id, + "attributedTo" => bob.ap_id, + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + log = assert_mismatched_signature_cancelled(create, alice) + + assert log =~ "Failed-signature inbox retry rejected" + assert log =~ "reason=:actor_signature_mismatch" + assert log =~ "payload_actor=\"https://two.com/users/bob\"" + assert log =~ "signature_actor=\"https://one.com/users/alice\"" + assert log =~ "activity_id=\"https://two.com/activities/logged-forged-create\"" + assert log =~ "type=\"Create\"" + assert log =~ "request_path=\"/inbox\"" + end + test "cancels signature actor mismatch before actually creating a forged post" do alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") From 00dd1b5103afce3e4e57cb6908d05ea6d94edeaf Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sun, 3 May 2026 10:19:33 +0400 Subject: [PATCH 43/55] Add failed-signature retry regression tests --- .../object_validators/update_validator.ex | 2 +- .../activity_pub_controller_test.exs | 33 +++++++++++++++++++ test/pleroma/workers/receiver_worker_test.exs | 16 +++++++++ .../workers/signature_retry_worker_test.exs | 25 ++++++++++++++ 4 files changed, 75 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex index ad3c0e3e2..4c0d9dff7 100644 --- a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex @@ -105,7 +105,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do cng |> add_error(:object, "Can't be updated by this actor") - true -> + _ -> cng |> add_error(:object, "Update is neither for Object or Actor") end diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index 62c1dd830..b8af1e31b 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -819,6 +819,39 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do refute Activity.get_by_ap_id(data["id"]) end + test "does not delete an object after failed signature retry", %{conn: conn} do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + note = insert(:note) + object_id = note.data["id"] + + data = %{ + "type" => "Delete", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/inbox-forged-delete", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => object_id + } + + expect_signature_retry_from(alice) + + conn = + conn + |> assign(:valid_signature, false) + |> put_req_header("content-type", "application/activity+json") + |> put_req_header("signature", "keyId=\"https://one.com/users/alice#main-key\"") + |> post("/inbox", data) + + assert "ok" == json_response(conn, 200) + + assert [{:cancel, :actor_signature_mismatch}] = + ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker)) + + refute Activity.get_by_ap_id(data["id"]) + assert %Object{data: %{"type" => "Note"}} = Object.get_by_ap_id(object_id) + end + test "does not create a forged post signed by a different actor", %{conn: conn} do alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") diff --git a/test/pleroma/workers/receiver_worker_test.exs b/test/pleroma/workers/receiver_worker_test.exs index 67a3f902e..ea05f38f1 100644 --- a/test/pleroma/workers/receiver_worker_test.exs +++ b/test/pleroma/workers/receiver_worker_test.exs @@ -237,6 +237,22 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do refute Pleroma.Object.get_by_ap_id(object_id) end + test "fails closed for legacy retry jobs missing one metadata field" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + params = insert(:note_activity).data + + assert {:cancel, :missing_signature_retry_metadata} = + ReceiverWorker.perform(%Oban.Job{ + args: %{ + "op" => "incoming_ap_doc", + "method" => "POST", + "params" => params, + "req_headers" => signature_headers_for(alice), + "request_path" => "/inbox" + } + }) + end + test "fails closed for malformed legacy metadata jobs without params" do assert {:cancel, :missing_signature_retry_metadata} = ReceiverWorker.perform(%Oban.Job{ diff --git a/test/pleroma/workers/signature_retry_worker_test.exs b/test/pleroma/workers/signature_retry_worker_test.exs index f4ec0e2e3..94dd5f6c1 100644 --- a/test/pleroma/workers/signature_retry_worker_test.exs +++ b/test/pleroma/workers/signature_retry_worker_test.exs @@ -399,6 +399,31 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do assert_mismatched_signature_cancelled(create, alice) end + test "cancels signature actor mismatch when payload actor is embedded" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + + create = %{ + "type" => "Create", + "actor" => %{"id" => bob.ap_id}, + "id" => "https://two.com/activities/embedded-actor-forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => "https://two.com/objects/embedded-actor-forged-note", + "actor" => bob.ap_id, + "attributedTo" => bob.ap_id, + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + assert_mismatched_signature_cancelled(create, alice) + end + test "logs signature actor mismatch retry rejections" do alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") From 6ae02d71bd10006710e24a97bcb6b9a895ed50cd Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sun, 3 May 2026 10:33:42 +0400 Subject: [PATCH 44/55] Align inbox controller tests with signer mapping --- .../activity_pub_controller_test.exs | 59 ++++++++++++------- 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index b8af1e31b..3988c3912 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -37,6 +37,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do setup do: clear_config([:instance, :federating], true) + defp assign_valid_signature_for_actor(conn, %User{ap_id: actor_id}) do + assign_valid_signature_for_actor(conn, actor_id) + end + + defp assign_valid_signature_for_actor(conn, actor) do + actor_id = Utils.get_ap_id(actor) + + conn + |> assign(:valid_signature, true) + |> put_req_header("signature", "keyId=\"#{actor_id}#main-key\"") + end + defp expect_signature_retry_from(%User{} = signer) do signer_json = UserView.render("user.json", %{user: signer}) |> Map.delete("featured") @@ -707,7 +719,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn = conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/inbox", data) @@ -735,7 +747,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn = conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/inbox", data) @@ -954,7 +966,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert "ok" == conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(followed_relay) |> put_req_header("content-type", "application/activity+json") |> post("/inbox", accept) |> json_response(200) @@ -1034,16 +1046,19 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do test "Unknown activity types are discarded", %{conn: conn} do unknown_types = ["Poke", "Read", "Dazzle"] + actor = + insert(:user, local: false, ap_id: "https://unknown.mastodon.instance/users/somebody") + Enum.each(unknown_types, fn bad_type -> params = %{ "type" => bad_type, - "actor" => "https://unknown.mastodon.instance/users/somebody" + "actor" => actor.ap_id } |> Jason.encode!() conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(actor) |> put_req_header("content-type", "application/activity+json") |> post("/inbox", params) |> json_response(400) @@ -1112,7 +1127,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert "ok" == conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/inbox", data) |> json_response(200) @@ -1133,7 +1148,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert "ok" == conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/inbox", data) |> json_response(200) @@ -1202,7 +1217,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert "ok" == conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/inbox", data) |> json_response(200) @@ -1221,7 +1236,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert "ok" == conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/inbox", data) |> json_response(200) @@ -1252,7 +1267,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn = conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/inbox", data) @@ -1273,7 +1288,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn = conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/inbox", data) @@ -1294,7 +1309,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn = conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/inbox", data) @@ -1318,7 +1333,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn = conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/inbox", data) @@ -1345,7 +1360,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn = conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/inbox", data) @@ -1375,7 +1390,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn = conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{recipient.nickname}/inbox", data) @@ -1440,7 +1455,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do } conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{recipient.nickname}/inbox", data) |> json_response(200) @@ -1530,7 +1545,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do } conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{reported_user.nickname}/inbox", data) |> json_response(200) @@ -1584,7 +1599,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do } conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{reported_user.nickname}/inbox", data) |> json_response(200) @@ -1617,7 +1632,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn = conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/inbox", data) @@ -1640,7 +1655,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn = conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/inbox", data) @@ -1663,7 +1678,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn = conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/inbox", data) @@ -2783,6 +2798,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do setup do: clear_config([:media_proxy]) setup do: clear_config([Pleroma.Upload]) + # majic's libmagic port is unavailable on local Darwin runs; Linux CI still runs this test. + @tag :skip_darwin test "POST /api/ap/upload_media", %{conn: conn} do user = insert(:user) From 1a8d585cbfe8320ac6b35f86c05559592457f133 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sun, 3 May 2026 12:10:23 +0400 Subject: [PATCH 45/55] Woodpecker CI: Allow rerunning OTP package uploads --- .woodpecker/otp-musl.yaml | 6 ++++++ .woodpecker/otp.yaml | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/.woodpecker/otp-musl.yaml b/.woodpecker/otp-musl.yaml index 6f007a127..60558b893 100644 --- a/.woodpecker/otp-musl.yaml +++ b/.woodpecker/otp-musl.yaml @@ -146,6 +146,7 @@ steps: package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + update: 'true' upload-latest-amd64-musl: image: *artifacts_uploader_image @@ -171,6 +172,7 @@ steps: package_version: stable-${CI_COMMIT_SHA:0:8}-amd64-musl file_source: ./stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + update: 'true' # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable upload-latest-tag-amd64-musl: @@ -196,6 +198,7 @@ steps: package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm-musl file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm-musl.zip file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm-musl.zip + update: 'true' upload-latest-arm-musl: image: *artifacts_uploader_image @@ -221,6 +224,7 @@ steps: package_version: stable-${CI_COMMIT_SHA:0:8}-arm-musl file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm-musl.zip file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-arm-musl.zip + update: 'true' # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable upload-latest-tag-arm-musl: @@ -246,6 +250,7 @@ steps: package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + update: 'true' upload-latest-arm64-musl: image: *artifacts_uploader_image @@ -271,6 +276,7 @@ steps: package_version: stable-${CI_COMMIT_SHA:0:8}-arm64-musl file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + update: 'true' # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable upload-latest-tag-arm64-musl: diff --git a/.woodpecker/otp.yaml b/.woodpecker/otp.yaml index 7e1bad611..9a33c228e 100644 --- a/.woodpecker/otp.yaml +++ b/.woodpecker/otp.yaml @@ -147,6 +147,7 @@ steps: package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64 file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip + update: 'true' upload-latest-amd64: image: *artifacts_uploader_image @@ -172,6 +173,7 @@ steps: package_version: stable-${CI_COMMIT_SHA:0:8}-amd64 file_source: ./stable-${CI_COMMIT_SHA:0:8}-amd64.zip file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64.zip + update: 'true' # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable upload-latest-tag-amd64: @@ -197,6 +199,7 @@ steps: package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm.zip file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm.zip + update: 'true' upload-latest-arm: image: *artifacts_uploader_image @@ -222,6 +225,7 @@ steps: package_version: stable-${CI_COMMIT_SHA:0:8}-arm file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm.zip file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-arm.zip + update: 'true' # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable upload-latest-tag-arm: @@ -247,6 +251,7 @@ steps: package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64 file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip + update: 'true' upload-latest-arm64: image: *artifacts_uploader_image @@ -272,6 +277,7 @@ steps: package_version: stable-${CI_COMMIT_SHA:0:8}-arm64 file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm64.zip file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64.zip + update: 'true' # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable upload-latest-tag-arm64: From 6553ba24aa6b1a835e923019bb2e0f277abd85f9 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sun, 3 May 2026 16:56:01 +0400 Subject: [PATCH 46/55] Prepare 2.10.1 release --- CHANGELOG.md | 48 +++++++++++++++++++ changelog.d/assign-users.add | 1 - .../avatar-description-mastodon-api.change | 1 - changelog.d/bandit.change | 1 - changelog.d/bookmark-folders.ignore | 1 - changelog.d/boost-visibilities.add | 1 - changelog.d/cache-control-immutable.add | 1 - changelog.d/ci-artifacts.skip | 0 changelog.d/credo-aliases-sort-fixes.skip | 0 changelog.d/database-config-whitelist.add | 1 - changelog.d/elixir-1.19-cherrypicks.change | 1 - changelog.d/email_digest.fix | 1 - changelog.d/emoji-reaction-url-escape.fix | 1 - changelog.d/exclusive-lists.add | 1 - .../gopher-genserver-crash-on-boot.fix | 1 - changelog.d/hackney-downgrade.change | 1 - changelog.d/hackney-mediaproxy.change | 1 - changelog.d/hackney.change | 1 - changelog.d/hubzilla-alsoknownas.fix | 1 - changelog.d/inappropriate-docs.remove | 1 - changelog.d/instance-domain-blocks.add | 1 - changelog.d/instance-profile-fields.add | 1 - changelog.d/lint-warnings.skip | 0 changelog.d/live-dashboard-redirect.fix | 1 - changelog.d/map-side-effects.skip | 0 changelog.d/missing-static-file.fix | 1 - changelog.d/mix-exs-fix.skip | 0 changelog.d/mix-exs-update.skip | 0 .../oauth-registration-redirect_uris.fix | 1 - changelog.d/oban-web.add | 1 - changelog.d/old-migrations.fix | 1 - changelog.d/paginate-follow-requests.change | 1 - changelog.d/phoenix-livedashboard-move.change | 1 - changelog.d/plug-test-typo.skip | 0 changelog.d/poll-voters-count.fix | 1 - changelog.d/prune-hashtag-follow-3376.fix | 1 - changelog.d/rate-limiter-hardening.fix | 1 - changelog.d/reduce-flaky-tests.skip | 1 - changelog.d/relationship-expires-at.change | 1 - changelog.d/release-to-docker.add | 1 - changelog.d/restore-embeds.fix | 1 - .../reverseproxy-recursive-redirect.fix | 1 - changelog.d/search-indexing.change | 1 - changelog.d/search-indexing.skip | 0 changelog.d/twitter-api.skip | 0 changelog.d/update-comment.ignore | 1 - changelog.d/user-view.ignore | 1 - changelog.d/vix-0.36.0.fix | 1 - changelog.d/woodpecker-pr-pipeline.skip | 0 changelog.d/woodpecker-release-pipeline.skip | 0 mix.exs | 2 +- 51 files changed, 49 insertions(+), 39 deletions(-) delete mode 100644 changelog.d/assign-users.add delete mode 100644 changelog.d/avatar-description-mastodon-api.change delete mode 100644 changelog.d/bandit.change delete mode 100644 changelog.d/bookmark-folders.ignore delete mode 100644 changelog.d/boost-visibilities.add delete mode 100644 changelog.d/cache-control-immutable.add delete mode 100644 changelog.d/ci-artifacts.skip delete mode 100644 changelog.d/credo-aliases-sort-fixes.skip delete mode 100644 changelog.d/database-config-whitelist.add delete mode 100644 changelog.d/elixir-1.19-cherrypicks.change delete mode 100644 changelog.d/email_digest.fix delete mode 100644 changelog.d/emoji-reaction-url-escape.fix delete mode 100644 changelog.d/exclusive-lists.add delete mode 100644 changelog.d/gopher-genserver-crash-on-boot.fix delete mode 100644 changelog.d/hackney-downgrade.change delete mode 100644 changelog.d/hackney-mediaproxy.change delete mode 100644 changelog.d/hackney.change delete mode 100644 changelog.d/hubzilla-alsoknownas.fix delete mode 100644 changelog.d/inappropriate-docs.remove delete mode 100644 changelog.d/instance-domain-blocks.add delete mode 100644 changelog.d/instance-profile-fields.add delete mode 100644 changelog.d/lint-warnings.skip delete mode 100644 changelog.d/live-dashboard-redirect.fix delete mode 100644 changelog.d/map-side-effects.skip delete mode 100644 changelog.d/missing-static-file.fix delete mode 100644 changelog.d/mix-exs-fix.skip delete mode 100644 changelog.d/mix-exs-update.skip delete mode 100644 changelog.d/oauth-registration-redirect_uris.fix delete mode 100644 changelog.d/oban-web.add delete mode 100644 changelog.d/old-migrations.fix delete mode 100644 changelog.d/paginate-follow-requests.change delete mode 100644 changelog.d/phoenix-livedashboard-move.change delete mode 100644 changelog.d/plug-test-typo.skip delete mode 100644 changelog.d/poll-voters-count.fix delete mode 100644 changelog.d/prune-hashtag-follow-3376.fix delete mode 100644 changelog.d/rate-limiter-hardening.fix delete mode 100644 changelog.d/reduce-flaky-tests.skip delete mode 100644 changelog.d/relationship-expires-at.change delete mode 100644 changelog.d/release-to-docker.add delete mode 100644 changelog.d/restore-embeds.fix delete mode 100644 changelog.d/reverseproxy-recursive-redirect.fix delete mode 100644 changelog.d/search-indexing.change delete mode 100644 changelog.d/search-indexing.skip delete mode 100644 changelog.d/twitter-api.skip delete mode 100644 changelog.d/update-comment.ignore delete mode 100644 changelog.d/user-view.ignore delete mode 100644 changelog.d/vix-0.36.0.fix delete mode 100644 changelog.d/woodpecker-pr-pipeline.skip delete mode 100644 changelog.d/woodpecker-release-pipeline.skip diff --git a/CHANGELOG.md b/CHANGELOG.md index adc76c767..c7bb9b09d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,54 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## 2.10.1 + +### Changed + +- Move avatar_description and header_description fields to the account object +- Update Bandit to 1.10.4 +- No-op code correctness improvements detected by Elixir 1.19 compiler +- Downgrade Hackney to 1.20.1 +- Use a custom redirect handler to ensure MediaProxy redirects are followed with Hackney +- Update Hackney, the default HTTP client, to the latest release which supports Happy Eyeballs for improved IPv6 federation +- Paginate follow requests +- Moved Phoenix LiveDashboard to /pleroma/live_dashboard +- Add mute/block expiry to the relationship object +- Filter indexable activities before inserting indexing jobs into the queue. + +### Added + +- Allow assigning users to reports +- Allow fine-grained announce visibilities +- Add immutable tag on cache-control header for several endpoints that's serving the same exact things. +- Add reasonable defaults for :database_config_whitelist +- Support lists `exclusive` param +- Add v1/instance/domain_blocks endpoint +- Add /api/v2/instance profile fields limits info used by Mastodon +- Added Oban Web dashboard located at /pleroma/oban +- Add instructions on how to run a release in docker, to make it easier to run on older distros. + +### Fixed + +- Fix the daily email digest job which was not executing +- Encode custom emoji URLs in EmojiReact activity tags. +- Gopher: Fix Ranch listener not being stopped properly on Pleroma restart when database configuration is enabled +- Fix fetching Hubzilla Actors with alsoKnownAs as string +- Fix /phoenix/live_dashboard redirect not working when user added a path segment +- Fix 404 error codes for missing static files +- Fix OAuth app registration to accept `redirect_uris` as an array of strings (RFC 7591), while keeping backwards compatibility with string input. +- Correct old migrations for expiring activities and user access tokens. +- Federate `votersCount` correctly +- DB prune: Check if user follows hashtag with no objects before deletion +- Stop the rate limiter from crashing when run with wrong settings. +- Restore embeds route +- ReverseProxy: Recursively follow redirects until redirect_limit is reached +- Fix compilation with vips-8.18.0 with bumping to vix 0.36.0 + +### Removed + +- Docs: Removed outdated, incorrect, unmaintained and inappropriate installation documentation (Arch, NetBSD, NixOS) + ## 2.10 ### Security diff --git a/changelog.d/assign-users.add b/changelog.d/assign-users.add deleted file mode 100644 index f50ad94c6..000000000 --- a/changelog.d/assign-users.add +++ /dev/null @@ -1 +0,0 @@ -Allow assigning users to reports \ No newline at end of file diff --git a/changelog.d/avatar-description-mastodon-api.change b/changelog.d/avatar-description-mastodon-api.change deleted file mode 100644 index 6a454c01e..000000000 --- a/changelog.d/avatar-description-mastodon-api.change +++ /dev/null @@ -1 +0,0 @@ -Move avatar_description and header_description fields to the account object diff --git a/changelog.d/bandit.change b/changelog.d/bandit.change deleted file mode 100644 index 3831a02c2..000000000 --- a/changelog.d/bandit.change +++ /dev/null @@ -1 +0,0 @@ -Update Bandit to 1.10.4 diff --git a/changelog.d/bookmark-folders.ignore b/changelog.d/bookmark-folders.ignore deleted file mode 100644 index 8705ac00b..000000000 --- a/changelog.d/bookmark-folders.ignore +++ /dev/null @@ -1 +0,0 @@ -Various bookmark folders-related improvements diff --git a/changelog.d/boost-visibilities.add b/changelog.d/boost-visibilities.add deleted file mode 100644 index 317d9840d..000000000 --- a/changelog.d/boost-visibilities.add +++ /dev/null @@ -1 +0,0 @@ -Allow fine-grained announce visibilities diff --git a/changelog.d/cache-control-immutable.add b/changelog.d/cache-control-immutable.add deleted file mode 100644 index 516db67bf..000000000 --- a/changelog.d/cache-control-immutable.add +++ /dev/null @@ -1 +0,0 @@ -Add immutable tag on cache-control header for several endpoints that's serving the same exact things. \ No newline at end of file diff --git a/changelog.d/ci-artifacts.skip b/changelog.d/ci-artifacts.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/credo-aliases-sort-fixes.skip b/changelog.d/credo-aliases-sort-fixes.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/database-config-whitelist.add b/changelog.d/database-config-whitelist.add deleted file mode 100644 index a78960c98..000000000 --- a/changelog.d/database-config-whitelist.add +++ /dev/null @@ -1 +0,0 @@ -Add reasonable defaults for :database_config_whitelist \ No newline at end of file diff --git a/changelog.d/elixir-1.19-cherrypicks.change b/changelog.d/elixir-1.19-cherrypicks.change deleted file mode 100644 index 7e56be008..000000000 --- a/changelog.d/elixir-1.19-cherrypicks.change +++ /dev/null @@ -1 +0,0 @@ -No-op code correctness improvements detected by Elixir 1.19 compiler diff --git a/changelog.d/email_digest.fix b/changelog.d/email_digest.fix deleted file mode 100644 index cd15874a2..000000000 --- a/changelog.d/email_digest.fix +++ /dev/null @@ -1 +0,0 @@ -Fix the daily email digest job which was not executing diff --git a/changelog.d/emoji-reaction-url-escape.fix b/changelog.d/emoji-reaction-url-escape.fix deleted file mode 100644 index c3a1c8823..000000000 --- a/changelog.d/emoji-reaction-url-escape.fix +++ /dev/null @@ -1 +0,0 @@ -Encode custom emoji URLs in EmojiReact activity tags. diff --git a/changelog.d/exclusive-lists.add b/changelog.d/exclusive-lists.add deleted file mode 100644 index bbd722f07..000000000 --- a/changelog.d/exclusive-lists.add +++ /dev/null @@ -1 +0,0 @@ -Support lists `exclusive` param diff --git a/changelog.d/gopher-genserver-crash-on-boot.fix b/changelog.d/gopher-genserver-crash-on-boot.fix deleted file mode 100644 index 3b51662be..000000000 --- a/changelog.d/gopher-genserver-crash-on-boot.fix +++ /dev/null @@ -1 +0,0 @@ -Gopher: Fix Ranch listener not being stopped properly on Pleroma restart when database configuration is enabled diff --git a/changelog.d/hackney-downgrade.change b/changelog.d/hackney-downgrade.change deleted file mode 100644 index a98710692..000000000 --- a/changelog.d/hackney-downgrade.change +++ /dev/null @@ -1 +0,0 @@ -Downgrade Hackney to 1.20.1 diff --git a/changelog.d/hackney-mediaproxy.change b/changelog.d/hackney-mediaproxy.change deleted file mode 100644 index 10dfb0775..000000000 --- a/changelog.d/hackney-mediaproxy.change +++ /dev/null @@ -1 +0,0 @@ -Use a custom redirect handler to ensure MediaProxy redirects are followed with Hackney diff --git a/changelog.d/hackney.change b/changelog.d/hackney.change deleted file mode 100644 index 3158cfc77..000000000 --- a/changelog.d/hackney.change +++ /dev/null @@ -1 +0,0 @@ -Update Hackney, the default HTTP client, to the latest release which supports Happy Eyeballs for improved IPv6 federation diff --git a/changelog.d/hubzilla-alsoknownas.fix b/changelog.d/hubzilla-alsoknownas.fix deleted file mode 100644 index 2a2969807..000000000 --- a/changelog.d/hubzilla-alsoknownas.fix +++ /dev/null @@ -1 +0,0 @@ -Fix fetching Hubzilla Actors with alsoKnownAs as string diff --git a/changelog.d/inappropriate-docs.remove b/changelog.d/inappropriate-docs.remove deleted file mode 100644 index 699c9186a..000000000 --- a/changelog.d/inappropriate-docs.remove +++ /dev/null @@ -1 +0,0 @@ -Docs: Removed outdated, incorrect, unmaintained and inappropriate installation documentation (Arch, NetBSD, NixOS) diff --git a/changelog.d/instance-domain-blocks.add b/changelog.d/instance-domain-blocks.add deleted file mode 100644 index 85f01c5c2..000000000 --- a/changelog.d/instance-domain-blocks.add +++ /dev/null @@ -1 +0,0 @@ -Add v1/instance/domain_blocks endpoint diff --git a/changelog.d/instance-profile-fields.add b/changelog.d/instance-profile-fields.add deleted file mode 100644 index 712bd68d9..000000000 --- a/changelog.d/instance-profile-fields.add +++ /dev/null @@ -1 +0,0 @@ -Add /api/v2/instance profile fields limits info used by Mastodon diff --git a/changelog.d/lint-warnings.skip b/changelog.d/lint-warnings.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/live-dashboard-redirect.fix b/changelog.d/live-dashboard-redirect.fix deleted file mode 100644 index 10588d89e..000000000 --- a/changelog.d/live-dashboard-redirect.fix +++ /dev/null @@ -1 +0,0 @@ -Fix /phoenix/live_dashboard redirect not working when user added a path segment diff --git a/changelog.d/map-side-effects.skip b/changelog.d/map-side-effects.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/missing-static-file.fix b/changelog.d/missing-static-file.fix deleted file mode 100644 index c7ef805aa..000000000 --- a/changelog.d/missing-static-file.fix +++ /dev/null @@ -1 +0,0 @@ -Fix 404 error codes for missing static files diff --git a/changelog.d/mix-exs-fix.skip b/changelog.d/mix-exs-fix.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/mix-exs-update.skip b/changelog.d/mix-exs-update.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/oauth-registration-redirect_uris.fix b/changelog.d/oauth-registration-redirect_uris.fix deleted file mode 100644 index 76ace55df..000000000 --- a/changelog.d/oauth-registration-redirect_uris.fix +++ /dev/null @@ -1 +0,0 @@ -Fix OAuth app registration to accept `redirect_uris` as an array of strings (RFC 7591), while keeping backwards compatibility with string input. diff --git a/changelog.d/oban-web.add b/changelog.d/oban-web.add deleted file mode 100644 index c59e2ebca..000000000 --- a/changelog.d/oban-web.add +++ /dev/null @@ -1 +0,0 @@ -Added Oban Web dashboard located at /pleroma/oban diff --git a/changelog.d/old-migrations.fix b/changelog.d/old-migrations.fix deleted file mode 100644 index 49566c896..000000000 --- a/changelog.d/old-migrations.fix +++ /dev/null @@ -1 +0,0 @@ -Correct old migrations for expiring activities and user access tokens. diff --git a/changelog.d/paginate-follow-requests.change b/changelog.d/paginate-follow-requests.change deleted file mode 100644 index 1a88995b7..000000000 --- a/changelog.d/paginate-follow-requests.change +++ /dev/null @@ -1 +0,0 @@ -Paginate follow requests diff --git a/changelog.d/phoenix-livedashboard-move.change b/changelog.d/phoenix-livedashboard-move.change deleted file mode 100644 index 116b1523a..000000000 --- a/changelog.d/phoenix-livedashboard-move.change +++ /dev/null @@ -1 +0,0 @@ -Moved Phoenix LiveDashboard to /pleroma/live_dashboard diff --git a/changelog.d/plug-test-typo.skip b/changelog.d/plug-test-typo.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/poll-voters-count.fix b/changelog.d/poll-voters-count.fix deleted file mode 100644 index 2dbc81b5d..000000000 --- a/changelog.d/poll-voters-count.fix +++ /dev/null @@ -1 +0,0 @@ -Federate `votersCount` correctly diff --git a/changelog.d/prune-hashtag-follow-3376.fix b/changelog.d/prune-hashtag-follow-3376.fix deleted file mode 100644 index cdb4e9a79..000000000 --- a/changelog.d/prune-hashtag-follow-3376.fix +++ /dev/null @@ -1 +0,0 @@ -DB prune: Check if user follows hashtag with no objects before deletion diff --git a/changelog.d/rate-limiter-hardening.fix b/changelog.d/rate-limiter-hardening.fix deleted file mode 100644 index a3af8fcc4..000000000 --- a/changelog.d/rate-limiter-hardening.fix +++ /dev/null @@ -1 +0,0 @@ -Stop the rate limiter from crashing when run with wrong settings. diff --git a/changelog.d/reduce-flaky-tests.skip b/changelog.d/reduce-flaky-tests.skip deleted file mode 100644 index 0375762c0..000000000 --- a/changelog.d/reduce-flaky-tests.skip +++ /dev/null @@ -1 +0,0 @@ -Reduce the number of flaky tests by making them sync if they affect the global state, and silence noisy test output. diff --git a/changelog.d/relationship-expires-at.change b/changelog.d/relationship-expires-at.change deleted file mode 100644 index 286dba197..000000000 --- a/changelog.d/relationship-expires-at.change +++ /dev/null @@ -1 +0,0 @@ -Add mute/block expiry to the relationship object diff --git a/changelog.d/release-to-docker.add b/changelog.d/release-to-docker.add deleted file mode 100644 index 5fbf611a5..000000000 --- a/changelog.d/release-to-docker.add +++ /dev/null @@ -1 +0,0 @@ -Add instructions on how to run a release in docker, to make it easier to run on older distros. diff --git a/changelog.d/restore-embeds.fix b/changelog.d/restore-embeds.fix deleted file mode 100644 index 5a2a1c4fe..000000000 --- a/changelog.d/restore-embeds.fix +++ /dev/null @@ -1 +0,0 @@ -Restore embeds route diff --git a/changelog.d/reverseproxy-recursive-redirect.fix b/changelog.d/reverseproxy-recursive-redirect.fix deleted file mode 100644 index 744109fd6..000000000 --- a/changelog.d/reverseproxy-recursive-redirect.fix +++ /dev/null @@ -1 +0,0 @@ -ReverseProxy: Recursively follow redirects until redirect_limit is reached diff --git a/changelog.d/search-indexing.change b/changelog.d/search-indexing.change deleted file mode 100644 index 766934f3f..000000000 --- a/changelog.d/search-indexing.change +++ /dev/null @@ -1 +0,0 @@ -Filter indexable activities before inserting indexing jobs into the queue. diff --git a/changelog.d/search-indexing.skip b/changelog.d/search-indexing.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/twitter-api.skip b/changelog.d/twitter-api.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/update-comment.ignore b/changelog.d/update-comment.ignore deleted file mode 100644 index 733e813b3..000000000 --- a/changelog.d/update-comment.ignore +++ /dev/null @@ -1 +0,0 @@ -Update comment for prepare_object, rename prepare_outgoing diff --git a/changelog.d/user-view.ignore b/changelog.d/user-view.ignore deleted file mode 100644 index 37e9a7e09..000000000 --- a/changelog.d/user-view.ignore +++ /dev/null @@ -1 +0,0 @@ -Avoid code duplication in UserView diff --git a/changelog.d/vix-0.36.0.fix b/changelog.d/vix-0.36.0.fix deleted file mode 100644 index 43a8dd8f8..000000000 --- a/changelog.d/vix-0.36.0.fix +++ /dev/null @@ -1 +0,0 @@ -Fix compilation with vips-8.18.0 with bumping to vix 0.36.0 diff --git a/changelog.d/woodpecker-pr-pipeline.skip b/changelog.d/woodpecker-pr-pipeline.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/woodpecker-release-pipeline.skip b/changelog.d/woodpecker-release-pipeline.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/mix.exs b/mix.exs index 5d84c6e09..5da2ca657 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do def project do [ app: :pleroma, - version: version("2.10.0"), + version: version("2.10.1"), elixir: "~> 1.15", elixirc_paths: elixirc_paths(Mix.env()), compilers: Mix.compilers(), From 621d86a31da0900cc6ddd5bae1b6c65204b7b5c8 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sun, 3 May 2026 18:02:59 +0400 Subject: [PATCH 47/55] Validate WebFinger nicknames against actors --- lib/pleroma/web/activity_pub/activity_pub.ex | 106 +++++++++++++------ test/pleroma/user_test.exs | 95 +++++++++++++++-- 2 files changed, 161 insertions(+), 40 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 071d634db..0b513ee16 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1677,44 +1677,80 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do show_birthday = !!birthday - # if WebFinger request was already done, we probably have acct, otherwise - # we request WebFinger here - nickname = additional[:nickname_from_acct] || generate_nickname(data) + with {:ok, nickname} <- nickname_from_actor(data, additional) do + {:ok, + %{ + ap_id: data["id"], + uri: get_actor_url(data["url"]), + banner: normalize_image(data["image"]), + fields: fields, + emoji: emojis, + is_locked: is_locked, + is_discoverable: is_discoverable, + invisible: invisible, + avatar: normalize_image(data["icon"]), + name: data["name"], + follower_address: data["followers"], + following_address: data["following"], + featured_address: featured_address, + bio: data["summary"] || "", + actor_type: actor_type, + also_known_as: normalize_also_known_as(data["alsoKnownAs"]), + public_key: public_key, + inbox: data["inbox"], + shared_inbox: shared_inbox, + accepts_chat_messages: accepts_chat_messages, + birthday: birthday, + show_birthday: show_birthday, + pinned_objects: pinned_objects, + nickname: nickname + }} + end + end - %{ - ap_id: data["id"], - uri: get_actor_url(data["url"]), - banner: normalize_image(data["image"]), - fields: fields, - emoji: emojis, - is_locked: is_locked, - is_discoverable: is_discoverable, - invisible: invisible, - avatar: normalize_image(data["icon"]), - name: data["name"], - follower_address: data["followers"], - following_address: data["following"], - featured_address: featured_address, - bio: data["summary"] || "", - actor_type: actor_type, - also_known_as: normalize_also_known_as(data["alsoKnownAs"]), - public_key: public_key, - inbox: data["inbox"], - shared_inbox: shared_inbox, - accepts_chat_messages: accepts_chat_messages, - birthday: birthday, - show_birthday: show_birthday, - pinned_objects: pinned_objects, - nickname: nickname - } + defp nickname_from_actor(data, additional) do + generated = generated_nickname(data) + + case additional[:nickname_from_acct] do + ^generated when is_binary(generated) -> + {:ok, generated} + + acct when is_binary(acct) -> + with ^acct <- webfinger_nickname(data) do + {:ok, acct} + else + _ -> {:error, {:webfinger_actor_mismatch, acct, data["id"]}} + end + + _ -> + {:ok, generate_nickname(data)} + end + end + + defp generated_nickname(%{"preferredUsername" => username, "id" => ap_id}) + when is_binary(username) and is_binary(ap_id) do + case URI.parse(ap_id) do + %URI{host: host} when is_binary(host) -> "#{username}@#{host}" + _ -> nil + end + end + + defp generated_nickname(_), do: nil + + defp webfinger_nickname(data) do + with generated when is_binary(generated) <- generated_nickname(data), + {:ok, %{"subject" => "acct:" <> acct, "ap_id" => ap_id}} <- WebFinger.finger(generated), + true <- ap_id == data["id"] do + acct + end end defp generate_nickname(%{"preferredUsername" => username} = data) when is_binary(username) do - generated = "#{username}@#{URI.parse(data["id"]).host}" + generated = generated_nickname(data) if Config.get([WebFinger, :update_nickname_on_user_fetch]) do - case WebFinger.finger(generated) do - {:ok, %{"subject" => "acct:" <> acct}} -> acct + case webfinger_nickname(data) do + acct when is_binary(acct) -> acct _ -> generated end else @@ -1794,9 +1830,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp collection_private(_data), do: {:ok, true} def user_data_from_user_object(data, additional \\ []) do - with {:ok, data} <- MRF.filter(data) do - {:ok, object_to_user_data(data, additional)} + with {:ok, data} <- MRF.filter(data), + {:ok, data} <- object_to_user_data(data, additional) do + {:ok, data} else + {:error, _} = e -> e e -> {:error, e} end end diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs index b2533e9f1..de735cdb7 100644 --- a/test/pleroma/user_test.exs +++ b/test/pleroma/user_test.exs @@ -876,17 +876,17 @@ defmodule Pleroma.UserTest do describe "get_or_fetch/1 remote users with tld, while BE is running on a subdomain" do setup do: clear_config([Pleroma.Web.WebFinger, :update_nickname_on_user_fetch], true) - test "for mastodon" do - ap_id = "a@mastodon.example" - {:ok, fetched_user} = User.get_or_fetch(ap_id) + test "fetches a mastodon split-domain nickname" do + nickname = "a@mastodon.example" + {:ok, fetched_user} = User.get_or_fetch(nickname) assert fetched_user.ap_id == "https://sub.mastodon.example/users/a" assert fetched_user.nickname == "a@mastodon.example" end - test "for pleroma" do - ap_id = "a@pleroma.example" - {:ok, fetched_user} = User.get_or_fetch(ap_id) + test "fetches a pleroma split-domain nickname" do + nickname = "a@pleroma.example" + {:ok, fetched_user} = User.get_or_fetch(nickname) assert fetched_user.ap_id == "https://sub.pleroma.example/users/a" assert fetched_user.nickname == "a@pleroma.example" @@ -936,6 +936,89 @@ defmodule Pleroma.UserTest do assert fetched_user == "not found nonexistent" end + test "does not rename an existing remote actor from rogue WebFinger data" do + clear_config([Pleroma.Web.WebFinger, :update_nickname_on_user_fetch], true) + + actor_id = "https://legit-actor.example/users/alice" + + Tesla.Mock.mock(fn + %{url: "https://evil-webfinger.example/.well-known/host-meta"} -> + {:ok, %Tesla.Env{status: 404}} + + %{ + url: + "https://evil-webfinger.example/.well-known/webfinger?resource=acct:claimed@evil-webfinger.example" + } -> + Tesla.Mock.json(%{ + "subject" => "acct:claimed@evil-webfinger.example", + "links" => [ + %{ + "rel" => "self", + "type" => "application/activity+json", + "href" => actor_id + } + ] + }) + + %{url: ^actor_id} -> + {:ok, + %Tesla.Env{ + status: 200, + headers: [{"content-type", "application/activity+json"}], + body: + Jason.encode!(%{ + "id" => actor_id, + "type" => "Person", + "preferredUsername" => "alice", + "name" => "Alice", + "summary" => "", + "inbox" => "https://legit-actor.example/users/alice/inbox", + "outbox" => "https://legit-actor.example/users/alice/outbox", + "followers" => "https://legit-actor.example/users/alice/followers", + "following" => "https://legit-actor.example/users/alice/following" + }) + }} + + %{url: "https://legit-actor.example/.well-known/host-meta"} -> + {:ok, %Tesla.Env{status: 404}} + + %{ + url: + "https://legit-actor.example/.well-known/webfinger?resource=acct:alice@legit-actor.example" + } -> + Tesla.Mock.json(%{ + "subject" => "acct:alice@legit-actor.example", + "links" => [ + %{ + "rel" => "self", + "type" => "application/activity+json", + "href" => actor_id + } + ] + }) + end) + + assert {:error, {:webfinger_actor_mismatch, "claimed@evil-webfinger.example", ^actor_id}} = + ActivityPub.make_user_from_nickname("claimed@evil-webfinger.example") + + refute User.get_by_ap_id(actor_id) + refute User.get_by_nickname("claimed@evil-webfinger.example") + + orig_user = + insert(:user, + local: false, + nickname: "alice@legit-actor.example", + ap_id: actor_id + ) + + assert {:error, {:webfinger_actor_mismatch, "claimed@evil-webfinger.example", ^actor_id}} = + ActivityPub.make_user_from_nickname("claimed@evil-webfinger.example") + + assert {:error, _} = User.get_or_fetch_by_nickname("claimed@evil-webfinger.example") + assert User.get_by_id(orig_user.id).nickname == "alice@legit-actor.example" + refute User.get_by_nickname("claimed@evil-webfinger.example") + end + test "updates an existing user, if stale" do a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800) From 8ccdd98914e3ce11948d877b05a015bfddeebe4b Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sun, 3 May 2026 20:23:35 +0400 Subject: [PATCH 48/55] Prepare 2.10.2 release --- CHANGELOG.md | 6 ++++++ changelog.d/activitypub-spoofing.security | 1 - mix.exs | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) delete mode 100644 changelog.d/activitypub-spoofing.security diff --git a/CHANGELOG.md b/CHANGELOG.md index c7bb9b09d..f03be3f29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## 2.10.2 + +### Security + +- ActivityPub: Fixed failed-signature inbox retry handling and signer identity checks to prevent spoofed remote activities from being processed + ## 2.10.1 ### Changed diff --git a/changelog.d/activitypub-spoofing.security b/changelog.d/activitypub-spoofing.security deleted file mode 100644 index 3e6baffb6..000000000 --- a/changelog.d/activitypub-spoofing.security +++ /dev/null @@ -1 +0,0 @@ -ActivityPub: Fixed failed-signature inbox retry handling and signer identity checks to prevent spoofed remote activities from being processed diff --git a/mix.exs b/mix.exs index 5da2ca657..bb7bfb9da 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do def project do [ app: :pleroma, - version: version("2.10.1"), + version: version("2.10.2"), elixir: "~> 1.15", elixirc_paths: elixirc_paths(Mix.env()), compilers: Mix.compilers(), From aec0deef8b3018ddcced65c7bd883514922b7539 Mon Sep 17 00:00:00 2001 From: Yonle Date: Tue, 5 May 2026 13:49:29 +0700 Subject: [PATCH 49/55] poll_view: try to read votersCount first, and then manually count local voters. --- lib/pleroma/web/mastodon_api/views/poll_view.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/poll_view.ex b/lib/pleroma/web/mastodon_api/views/poll_view.ex index 3b4271227..67f874b46 100644 --- a/lib/pleroma/web/mastodon_api/views/poll_view.ex +++ b/lib/pleroma/web/mastodon_api/views/poll_view.ex @@ -71,12 +71,12 @@ defmodule Pleroma.Web.MastodonAPI.PollView do end) end + defp voters_count(%{data: %{"votersCount" => voters}}), do: voters + defp voters_count(%{data: %{"voters" => [_ | _] = voters}}) do length(voters) end - defp voters_count(%{data: %{"votersCount" => voters}}), do: voters - defp voters_count(_), do: 0 defp voted_and_own_votes(%{object: object} = params, options) do From 727e9e77497f20e2b61bd6267555015d2c308696 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Wed, 6 May 2026 11:33:34 +0400 Subject: [PATCH 50/55] Fix votersCount inflation in multiple-choice polls increase_vote_count/3 was incrementing votersCount on every vote activity, causing inflation when a single voter picks multiple options. Now only increments when the actor is a new unique voter, and preserves existing votersCount otherwise. Also adds is_integer guard to voters_count/1 to handle nil safely, and adds tests for the voters_count clause ordering and edge cases. --- lib/pleroma/object.ex | 18 +- .../web/mastodon_api/views/poll_view.ex | 2 +- .../web/mastodon_api/views/poll_view_test.exs | 175 ++++++++++++++++++ 3 files changed, 189 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index f1e07a257..5e9314446 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -372,13 +372,21 @@ defmodule Pleroma.Object do option end) - voters = [actor | object.data["voters"] || []] |> Enum.uniq() + existing_voters = object.data["voters"] || [] + voters = [actor | existing_voters] |> Enum.uniq() + new_voter? = actor not in existing_voters + existing_voters_count = object.data["votersCount"] voters_count = - if Map.has_key?(object.data, "votersCount") do - object.data["votersCount"] + 1 - else - length(voters) + cond do + is_integer(existing_voters_count) and new_voter? -> + existing_voters_count + 1 + + is_integer(existing_voters_count) -> + existing_voters_count + + true -> + length(voters) end data = diff --git a/lib/pleroma/web/mastodon_api/views/poll_view.ex b/lib/pleroma/web/mastodon_api/views/poll_view.ex index 67f874b46..f047804e2 100644 --- a/lib/pleroma/web/mastodon_api/views/poll_view.ex +++ b/lib/pleroma/web/mastodon_api/views/poll_view.ex @@ -71,7 +71,7 @@ defmodule Pleroma.Web.MastodonAPI.PollView do end) end - defp voters_count(%{data: %{"votersCount" => voters}}), do: voters + defp voters_count(%{data: %{"votersCount" => voters}}) when is_integer(voters), do: voters defp voters_count(%{data: %{"voters" => [_ | _] = voters}}) do length(voters) diff --git a/test/pleroma/web/mastodon_api/views/poll_view_test.exs b/test/pleroma/web/mastodon_api/views/poll_view_test.exs index 16281393d..6cb5934de 100644 --- a/test/pleroma/web/mastodon_api/views/poll_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/poll_view_test.exs @@ -180,4 +180,179 @@ defmodule Pleroma.Web.MastodonAPI.PollViewTest do assert result[:pleroma][:non_anonymous] == true end + + test "prefers votersCount over voters list when both are present" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Which flavor?", + poll: %{options: ["chocolate", "vanilla"], expires_in: 20} + }) + + object = Object.normalize(activity, fetch: false) + + voter = insert(:user) + {:ok, _, object} = CommonAPI.vote(object, voter, [0]) + + assert object.data["votersCount"] == 1 + assert length(object.data["voters"]) == 1 + + object = %{ + object + | data: Map.put(object.data, "votersCount", 42) + } + + result = PollView.render("show.json", %{object: object}) + + assert result[:voters_count] == 42 + end + + test "falls back to voters list when votersCount is absent" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Which flavor?", + poll: %{options: ["chocolate", "vanilla"], expires_in: 20} + }) + + object = Object.normalize(activity, fetch: false) + + voter = insert(:user) + {:ok, _, object} = CommonAPI.vote(object, voter, [0]) + + assert length(object.data["voters"]) == 1 + + data = Map.delete(object.data, "votersCount") + object = %{object | data: data} + + result = PollView.render("show.json", %{object: object}) + + assert result[:voters_count] == 1 + end + + test "returns 0 when both votersCount and voters are absent" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Which flavor?", + poll: %{options: ["chocolate", "vanilla"], expires_in: 20} + }) + + object = Object.normalize(activity, fetch: false) + + data = + object.data + |> Map.delete("votersCount") + |> Map.delete("voters") + + object = %{object | data: data} + + result = PollView.render("show.json", %{object: object}) + + assert result[:voters_count] == 0 + end + + test "returns 0 when voters list is empty" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Which flavor?", + poll: %{options: ["chocolate", "vanilla"], expires_in: 20} + }) + + object = Object.normalize(activity, fetch: false) + + data = + object.data + |> Map.delete("votersCount") + |> Map.put("voters", []) + + object = %{object | data: data} + + result = PollView.render("show.json", %{object: object}) + + assert result[:voters_count] == 0 + end + + test "does not inflate votersCount when same voter picks multiple options" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Pick several", + poll: %{options: ["a", "b", "c"], expires_in: 20, multiple: true} + }) + + object = Object.normalize(activity, fetch: false) + + voter = insert(:user) + {:ok, _, object} = CommonAPI.vote(object, voter, [0, 2]) + + assert object.data["votersCount"] == 1 + assert length(object.data["voters"]) == 1 + end + + test "preserves votersCount from remote source when existing voter picks another option" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Pick several", + poll: %{options: ["a", "b"], expires_in: 20, multiple: true} + }) + + object = Object.normalize(activity, fetch: false) + + voter = insert(:user) + {:ok, _, object} = CommonAPI.vote(object, voter, [0, 1]) + + object = %{object | data: Map.put(object.data, "votersCount", 14)} + + result = PollView.render("show.json", %{object: object}) + + assert result[:voters_count] == 14 + end + + test "returns 0 when votersCount is explicitly 0" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Pick one", + poll: %{options: ["a", "b"], expires_in: 20} + }) + + object = Object.normalize(activity, fetch: false) + + object = %{object | data: Map.put(object.data, "votersCount", 0)} + + result = PollView.render("show.json", %{object: object}) + + assert result[:voters_count] == 0 + end + + test "falls back to voters list when votersCount is nil" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Pick one", + poll: %{options: ["a", "b"], expires_in: 20} + }) + + object = Object.normalize(activity, fetch: false) + + voter = insert(:user) + {:ok, _, object} = CommonAPI.vote(object, voter, [0]) + + object = %{object | data: Map.put(object.data, "votersCount", nil)} + + result = PollView.render("show.json", %{object: object}) + + assert result[:voters_count] == length(object.data["voters"]) + end end From c62d1919867b287338f497d089acc81cd5b9a31b Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Wed, 6 May 2026 11:48:20 +0400 Subject: [PATCH 51/55] Add changelog for votersCount inflation fix --- changelog.d/poll-voters-count-inflation.fix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/poll-voters-count-inflation.fix diff --git a/changelog.d/poll-voters-count-inflation.fix b/changelog.d/poll-voters-count-inflation.fix new file mode 100644 index 000000000..7eae41f13 --- /dev/null +++ b/changelog.d/poll-voters-count-inflation.fix @@ -0,0 +1 @@ +Fix votersCount inflation when same voter picks multiple options From 4873991983ca396a86a7a86c2b423ff29936afa2 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 6 May 2026 15:40:47 +0200 Subject: [PATCH 52/55] Update Pleroma-FE build artifacts URL --- changelog.d/pleroma-fe-link.fix | 1 + config/config.exs | 2 +- lib/pleroma/frontend.ex | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog.d/pleroma-fe-link.fix diff --git a/changelog.d/pleroma-fe-link.fix b/changelog.d/pleroma-fe-link.fix new file mode 100644 index 000000000..de93f86dd --- /dev/null +++ b/changelog.d/pleroma-fe-link.fix @@ -0,0 +1 @@ +Updated Pleroma-FE build URL after Forgejo migration diff --git a/config/config.exs b/config/config.exs index 5bf2c5c2e..82bdd7750 100644 --- a/config/config.exs +++ b/config/config.exs @@ -775,7 +775,7 @@ config :pleroma, :frontends, "name" => "pleroma-fe", "git" => "https://git.pleroma.social/pleroma/pleroma-fe", "build_url" => - "https://git.pleroma.social/pleroma/pleroma-fe/-/jobs/artifacts/${ref}/download?job=build", + "https://git.pleroma.social/api/packages/pleroma/generic/pleroma-fe-builds/${ref}/latest.zip", "ref" => "develop" }, "fedi-fe" => %{ diff --git a/lib/pleroma/frontend.ex b/lib/pleroma/frontend.ex index e651d7d9d..e37a3fefe 100644 --- a/lib/pleroma/frontend.ex +++ b/lib/pleroma/frontend.ex @@ -75,8 +75,8 @@ defmodule Pleroma.Frontend do end defp download_build(frontend_info, dest) do - Logger.info("Downloading pre-built bundle for #{frontend_info["name"]}") url = String.replace(frontend_info["build_url"], "${ref}", frontend_info["ref"]) + Logger.info("Downloading pre-built bundle for #{frontend_info["name"]} from #{url}") with {:ok, %{status: 200, body: zip_body}} <- Pleroma.HTTP.get(url, [], pool: :media, recv_timeout: 120_000) do From 6b86e31e5d9d5d6c2175c12c3ea086de101ea33b Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 11 May 2026 14:53:06 +0400 Subject: [PATCH 53/55] Add backend MFM support --- config/config.exs | 3 +- config/description.exs | 11 +- lib/pleroma/formatter.ex | 11 ++ .../article_note_page_validator.ex | 113 ++++++++++++ .../object_validators/common_fields.ex | 1 + lib/pleroma/web/activity_pub/utils.ex | 3 +- lib/pleroma/web/common_api/activity_draft.ex | 7 +- lib/pleroma/web/common_api/utils.ex | 18 ++ mix.exs | 3 + mix.lock | 1 + priv/scrubbers/default.ex | 42 ++++- test/mix/tasks/pleroma/config_test.exs | 8 +- .../article_note_page_validator_test.exs | 165 ++++++++++++++++++ test/pleroma/web/activity_pub/utils_test.exs | 6 +- test/pleroma/web/common_api_test.exs | 41 +++++ 15 files changed, 423 insertions(+), 10 deletions(-) diff --git a/config/config.exs b/config/config.exs index 82bdd7750..2d38e3ebe 100644 --- a/config/config.exs +++ b/config/config.exs @@ -203,7 +203,8 @@ config :pleroma, :instance, "text/plain", "text/html", "text/markdown", - "text/bbcode" + "text/bbcode", + "text/x.misskeymarkdown" ], autofollowed_nicknames: [], autofollowing_nicknames: [], diff --git a/config/description.exs b/config/description.exs index c388d17c3..6e4348907 100644 --- a/config/description.exs +++ b/config/description.exs @@ -815,7 +815,8 @@ config :pleroma, :config_description, [ "text/plain", "text/html", "text/markdown", - "text/bbcode" + "text/bbcode", + "text/x.misskeymarkdown" ] }, %{ @@ -1394,7 +1395,13 @@ config :pleroma, :config_description, [ label: "Post Content Type", type: {:dropdown, :atom}, description: "Default post formatting option", - suggestions: ["text/plain", "text/html", "text/markdown", "text/bbcode"] + suggestions: [ + "text/plain", + "text/html", + "text/markdown", + "text/bbcode", + "text/x.misskeymarkdown" + ] }, %{ key: :redirectRootNoLogin, diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex index 11d5af2fb..4bf2f6b95 100644 --- a/lib/pleroma/formatter.ex +++ b/lib/pleroma/formatter.ex @@ -127,6 +127,13 @@ defmodule Pleroma.Formatter do Earmark.as_html!(text, %Earmark.Options{compact_output: true, smartypants: false}) end + def markdown_to_html(text, opts) do + Earmark.as_html!( + text, + %Earmark.Options{compact_output: true, smartypants: false} |> Map.merge(opts) + ) + end + def html_escape({text, mentions, hashtags}, type) do {html_escape(text, type), mentions, hashtags} end @@ -135,6 +142,10 @@ defmodule Pleroma.Formatter do HTML.filter_tags(text) end + def html_escape(text, "text/x.misskeymarkdown") do + HTML.filter_tags(text) + end + def html_escape(text, "text/plain") do Regex.split(@link_regex, text, include_captures: true) |> Enum.map_every(2, fn chunk -> diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex index c0626ce4d..ec9259f95 100644 --- a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex @@ -6,6 +6,9 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do use Ecto.Schema alias Pleroma.EctoType.ActivityPub.ObjectValidators + alias Pleroma.HTML + alias Pleroma.User + alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations alias Pleroma.Web.ActivityPub.Transmogrifier @@ -26,6 +29,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do end field(:replies, {:array, ObjectValidators.ObjectID}, default: []) + field(:source, :map) end def cast_and_apply(data) do @@ -80,6 +84,113 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do def fix_attachments(data), do: data + defp remote_mention_resolver( + %{"id" => ap_id, "tag" => tags}, + "@" <> nickname = mention, + buffer, + opts, + acc + ) + when is_binary(ap_id) and is_list(tags) do + initial_host = + ap_id + |> URI.parse() + |> Map.get(:host) + + with mention_tag when not is_nil(mention_tag) <- + Enum.find(tags, &mention_tag?(&1, mention, initial_host)), + href when is_binary(href) <- mention_tag["href"], + %User{} = user <- User.get_cached_by_ap_id(href) do + link = Pleroma.Formatter.mention_from_user(user, opts) + {link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, user})}} + else + _ -> {buffer, acc} + end + end + + defp remote_mention_resolver(_object, _mention, buffer, _opts, acc), do: {buffer, acc} + + defp mention_tag?(%{"type" => "Mention", "name" => name}, mention, initial_host) + when is_binary(name) do + name == mention || mention == "#{name}@#{initial_host}" + end + + defp mention_tag?(_tag, _mention, _initial_host), do: false + + defp scrub_content(%{"content" => content} = object) when is_binary(content) do + Map.put(object, "content", HTML.filter_tags(content)) + end + + defp scrub_content(object), do: object + + defp mfm_parse_limit do + min(Pleroma.Config.get([:instance, :limit]), Pleroma.Config.get([:instance, :remote_limit])) + end + + defp normalize_source(%{"source" => source} = object) when is_binary(source) do + object + |> Map.put("source", %{"content" => source}) + |> normalize_source() + end + + defp normalize_source(%{"source" => source} = object) when is_map(source) do + source = + case source["content"] do + content when is_binary(content) -> + if String.length(content) <= mfm_parse_limit() do + source + else + Map.delete(source, "content") + end + + nil -> + source + + _ -> + Map.delete(source, "content") + end + + Map.put(object, "source", source) + end + + defp normalize_source(object), do: object + + defp fix_misskey_content(%{"htmlMfm" => true, "content" => content} = object) + when is_binary(content) do + Map.put(object, "content", HTML.filter_tags(content)) + end + + defp fix_misskey_content(%{"htmlMfm" => true} = object), do: object + + defp fix_misskey_content( + %{"source" => %{"mediaType" => "text/x.misskeymarkdown", "content" => content}} = object + ) + when is_binary(content) do + mention_handler = fn nick, buffer, opts, acc -> + remote_mention_resolver(object, nick, buffer, opts, acc) + end + + {linked, _mentions, _tags} = + Utils.format_input(content, "text/x.misskeymarkdown", mention_handler: mention_handler) + + Map.put(object, "content", linked) + end + + defp fix_misskey_content(%{"source" => %{"mediaType" => "text/x.misskeymarkdown"}} = object), + do: scrub_content(object) + + defp fix_misskey_content(%{"_misskey_content" => content} = object) when is_binary(content) do + object + |> Map.put("source", %{ + "content" => content, + "mediaType" => "text/x.misskeymarkdown" + }) + |> Map.delete("_misskey_content") + |> fix_misskey_content() + end + + defp fix_misskey_content(object), do: object + defp fix(data) do data |> CommonFixes.fix_actor() @@ -88,6 +199,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do |> fix_tag() |> fix_replies() |> fix_attachments() + |> normalize_source() + |> fix_misskey_content() |> CommonFixes.fix_quote_url() |> CommonFixes.fix_likes() |> Transmogrifier.fix_emoji() diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex index 22cf0cc05..9b8580200 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex @@ -32,6 +32,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do quote bind_quoted: binding() do field(:content, :string) field(:contentMap, ObjectValidators.ContentLanguageMap) + field(:htmlMfm, :boolean) field(:published, ObjectValidators.DateTime) field(:updated, ObjectValidators.DateTime) diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 43c0f456d..0af4ceaf5 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -120,7 +120,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do "https://www.w3.org/ns/activitystreams", "#{Endpoint.url()}/schemas/litepub-0.1.jsonld", %{ - "@language" => get_language(data) + "@language" => get_language(data), + "htmlMfm" => "https://w3id.org/fep/c16b#htmlMfm" } ] } diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index 16489663a..6072fff6b 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -317,6 +317,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do emoji = Map.merge(emoji, summary_emoji) + media_type = Utils.get_content_type(draft.params[:content_type]) {:ok, note_data, _meta} = Builder.note(draft) object = @@ -324,14 +325,18 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do |> Map.put("emoji", emoji) |> Map.put("source", %{ "content" => draft.status, - "mediaType" => Utils.get_content_type(draft.params[:content_type]) + "mediaType" => media_type }) + |> maybe_put("htmlMfm", true, media_type == "text/x.misskeymarkdown") |> Map.put("generator", draft.params[:generator]) |> Map.put("language", draft.language) %{draft | object: object} end + defp maybe_put(map, key, value, true), do: Map.put(map, key, value) + defp maybe_put(map, _key, _value, _condition), do: map + defp preview?(%__MODULE__{} = draft) do preview? = Pleroma.Web.Utils.Params.truthy_param?(draft.params[:preview]) %{draft | preview?: preview?} diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 32572a721..26034d685 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -322,6 +322,14 @@ defmodule Pleroma.Web.CommonAPI.Utils do |> Formatter.linkify(options) end + def format_input(text, "text/x.misskeymarkdown", options) do + text + |> Formatter.markdown_to_html(%{breaks: true}) + |> safe_mfm_to_html() + |> Formatter.linkify(options) + |> Formatter.html_escape("text/x.misskeymarkdown") + end + def format_input(text, "text/markdown", options) do text |> Formatter.mentions_escape(options) @@ -330,6 +338,16 @@ defmodule Pleroma.Web.CommonAPI.Utils do |> Formatter.html_escape("text/html") end + defp safe_mfm_to_html(html) do + html + |> MfmParser.Parser.parse() + |> MfmParser.Encoder.to_html() + rescue + _ -> html + catch + _, _ -> html + end + def format_naive_asctime(date) do date |> DateTime.from_naive!("Etc/UTC") |> format_asctime end diff --git a/mix.exs b/mix.exs index bb7bfb9da..88f98d54e 100644 --- a/mix.exs +++ b/mix.exs @@ -160,6 +160,9 @@ defmodule Pleroma.Mixfile do {:sweet_xml, "~> 0.7.5"}, {:earmark, "1.4.46"}, {:bbcode_pleroma, "~> 0.2.0"}, + {:mfm_parser, + git: "https://akkoma.dev/AkkomaGang/mfm-parser.git", + ref: "360a30267a847810a63ab48f606ba227b2ca05f0"}, {:cors_plug, "~> 2.0"}, {:web_push_encryption, "~> 0.3.1"}, {:swoosh, "~> 1.16.12"}, diff --git a/mix.lock b/mix.lock index dd5f65f11..976a856b0 100644 --- a/mix.lock +++ b/mix.lock @@ -79,6 +79,7 @@ "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, + "mfm_parser": {:git, "https://akkoma.dev/AkkomaGang/mfm-parser.git", "360a30267a847810a63ab48f606ba227b2ca05f0", [ref: "360a30267a847810a63ab48f606ba227b2ca05f0"]}, "mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"}, "mimerl": {:hex, :mimerl, "1.5.0", "f35aca6f23242339b3666e0ac0702379e362b469d0aea167f6cc713547e777ed", [:rebar3], [], "hexpm", "db648ce065bae14ea84ca8b5dd123f42f49417cef693541110bf6f9e9be9ecc4"}, "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, diff --git a/priv/scrubbers/default.ex b/priv/scrubbers/default.ex index 0defdc74e..342ef9944 100644 --- a/priv/scrubbers/default.ex +++ b/priv/scrubbers/default.ex @@ -82,12 +82,50 @@ defmodule Pleroma.HTML.Scrubber.Default do "recipients-inline", "quote-inline", "invisible", - "ellipsis" + "ellipsis", + "mfm-center", + "mfm-flip", + "mfm-font", + "mfm-blur", + "mfm-rotate", + "mfm-x2", + "mfm-x3", + "mfm-x4", + "mfm-position", + "mfm-scale", + "mfm-fg", + "mfm-bg", + "mfm-jelly", + "mfm-twitch", + "mfm-shake", + "mfm-spin", + "mfm-jump", + "mfm-bounce", + "mfm-rainbow", + "mfm-tada", + "mfm-sparkle" ]) Meta.allow_tag_with_this_attribute_values(:p, "class", ["quote-inline"]) - Meta.allow_tag_with_these_attributes(:span, ["lang"]) + Meta.allow_tag_with_these_attributes(:span, [ + "lang", + "data-mfm-h", + "data-mfm-v", + "data-mfm-x", + "data-mfm-y", + "data-mfm-alternate", + "data-mfm-speed", + "data-mfm-deg", + "data-mfm-left", + "data-mfm-serif", + "data-mfm-monospace", + "data-mfm-cursive", + "data-mfm-fantasy", + "data-mfm-emoji", + "data-mfm-math", + "data-mfm-color" + ]) Meta.allow_tag_with_this_attribute_values(:code, "class", ["inline"]) diff --git a/test/mix/tasks/pleroma/config_test.exs b/test/mix/tasks/pleroma/config_test.exs index ef1adc235..f672d8c13 100644 --- a/test/mix/tasks/pleroma/config_test.exs +++ b/test/mix/tasks/pleroma/config_test.exs @@ -144,7 +144,13 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do quarantined_instances: [], managed_config: true, static_dir: "instance/static/", - allowed_post_formats: ["text/plain", "text/html", "text/markdown", "text/bbcode"], + allowed_post_formats: [ + "text/plain", + "text/html", + "text/markdown", + "text/bbcode", + "text/x.misskeymarkdown" + ], autofollowed_nicknames: [], max_pinned_statuses: 1, attachment_links: false, diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs index c32811c5b..bf9c70fb6 100644 --- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs @@ -149,6 +149,171 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest %{valid?: true} = ArticleNotePageValidator.cast_and_validate(note) end + test "a Misskey MFM note is rendered from source content" do + user = insert(:user, ap_id: "https://misskey.example/users/alice") + + note = %{ + "id" => "https://misskey.example/notes/1", + "type" => "Note", + "actor" => user.ap_id, + "attributedTo" => user.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "content" => "original content", + "context" => Utils.generate_context_id(), + "source" => %{ + "content" => "$[spin.speed=1s mfm goes here] ", + "mediaType" => "text/x.misskeymarkdown" + } + } + + %{valid?: true, changes: %{content: content, source: source}} = + ArticleNotePageValidator.cast_and_validate(note) + + assert source["mediaType"] == "text/x.misskeymarkdown" + assert content =~ ~s(class="mfm-spin") + assert content =~ ~s(data-mfm-speed="1s") + assert content =~ "mfm goes here" + refute content =~ "original content" + refute content =~ " "https://misskey.example/notes/3", + "type" => "Note", + "actor" => remote_user.ap_id, + "attributedTo" => remote_user.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "content" => "original content", + "context" => Utils.generate_context_id(), + "tag" => [ + %{ + "type" => "Mention", + "name" => "@local_user", + "href" => local_user.ap_id + }, + %{ + "type" => "Mention", + "name" => "@uncached", + "href" => "https://misskey.example/users/uncached" + } + ], + "source" => %{ + "content" => "@local_user @uncached $[spin hello]", + "mediaType" => "text/x.misskeymarkdown" + } + } + + %{valid?: true, changes: %{content: content}} = + ArticleNotePageValidator.cast_and_validate(note) + + assert content =~ local_user.ap_id + assert content =~ "@uncached" + end + + test "a Misskey MFM note drops oversized source content instead of parsing it" do + user = insert(:user, ap_id: "https://misskey.example/users/oversized") + + note = %{ + "id" => "https://misskey.example/notes/4", + "type" => "Note", + "actor" => user.ap_id, + "attributedTo" => user.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "content" => "safe fallback", + "context" => Utils.generate_context_id(), + "source" => %{ + "content" => String.duplicate("x", 5_001), + "mediaType" => "text/x.misskeymarkdown" + } + } + + %{valid?: true, changes: %{content: content, source: source}} = + ArticleNotePageValidator.cast_and_validate(note) + + assert content == "safe fallback" + refute Map.has_key?(source, "content") + end + + test "a note drops oversized non-MFM source content" do + user = insert(:user, ap_id: "https://example.com/users/source") + + note = %{ + "id" => "https://example.com/notes/1", + "type" => "Note", + "actor" => user.ap_id, + "attributedTo" => user.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "content" => "regular content", + "context" => Utils.generate_context_id(), + "source" => %{ + "content" => String.duplicate("x", 5_001), + "mediaType" => "text/markdown" + } + } + + %{valid?: true, changes: %{source: source}} = ArticleNotePageValidator.cast_and_validate(note) + + assert source == %{"mediaType" => "text/markdown"} + end + + test "a Misskey MFM note with legacy _misskey_content is rendered" do + user = insert(:user, ap_id: "https://misskey.example/users/legacy") + + note = %{ + "id" => "https://misskey.example/notes/5", + "type" => "Note", + "actor" => user.ap_id, + "attributedTo" => user.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "content" => "original content", + "context" => Utils.generate_context_id(), + "_misskey_content" => "$[spin legacy]" + } + + %{valid?: true, changes: %{content: content, source: source}} = + ArticleNotePageValidator.cast_and_validate(note) + + assert source == %{"content" => "$[spin legacy]", "mediaType" => "text/x.misskeymarkdown"} + assert content =~ ~s(class="mfm-spin") + assert content =~ "legacy" + end + + test "a Misskey MFM note with htmlMfm is scrubbed but not rendered from source content" do + user = insert(:user, ap_id: "https://misskey.example/users/bob") + + note = %{ + "id" => "https://misskey.example/notes/2", + "type" => "Note", + "actor" => user.ap_id, + "attributedTo" => user.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "content" => + "already rendered", + "htmlMfm" => true, + "context" => Utils.generate_context_id(), + "source" => %{ + "content" => String.duplicate("x", 5_001), + "mediaType" => "text/x.misskeymarkdown" + } + } + + %{valid?: true, changes: %{content: content, htmlMfm: true, source: source}} = + ArticleNotePageValidator.cast_and_validate(note) + + assert content == "already renderedalert('xss')" + refute Map.has_key?(source, "content") + end + test "a Note with validated likes collection validates" do insert(:user, ap_id: "https://pol.social/users/mkljczk") diff --git a/test/pleroma/web/activity_pub/utils_test.exs b/test/pleroma/web/activity_pub/utils_test.exs index 3b77f0867..93234a015 100644 --- a/test/pleroma/web/activity_pub/utils_test.exs +++ b/test/pleroma/web/activity_pub/utils_test.exs @@ -180,7 +180,8 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do "https://www.w3.org/ns/activitystreams", "http://localhost:4001/schemas/litepub-0.1.jsonld", %{ - "@language" => "und" + "@language" => "und", + "htmlMfm" => "https://w3id.org/fep/c16b#htmlMfm" } ] } @@ -192,7 +193,8 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do "https://www.w3.org/ns/activitystreams", "http://localhost:4001/schemas/litepub-0.1.jsonld", %{ - "@language" => "pl" + "@language" => "pl", + "htmlMfm" => "https://w3id.org/fep/c16b#htmlMfm" } ] } diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index 017fac696..ea1795c0b 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -709,6 +709,47 @@ defmodule Pleroma.Web.CommonAPITest do assert object.data["source"]["content"] == post end + test "it renders MFM posts and marks their ActivityPub representation" do + user = insert(:user) + + post = "

$[spin.speed=1s 13:37]

" + + {:ok, activity} = + CommonAPI.post(user, %{ + status: post, + content_type: "text/x.misskeymarkdown" + }) + + object = Object.normalize(activity, fetch: false) + + assert object.data["htmlMfm"] == true + + assert object.data["source"] == %{ + "content" => post, + "mediaType" => "text/x.misskeymarkdown" + } + + assert object.data["content"] =~ ~s(class="mfm-spin") + assert object.data["content"] =~ ~s(data-mfm-speed="1s") + assert object.data["content"] =~ "13:37" + refute object.data["content"] =~ "scrub-this" + end + + test "it falls back safely for malformed MFM" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "$[spin.speed=1s=boom malformed]", + content_type: "text/x.misskeymarkdown" + }) + + object = Object.normalize(activity, fetch: false) + + refute object.data["content"] =~ ~s(class="mfm-spin") + assert object.data["content"] =~ "malformed" + end + test "it does not allow replies to direct messages that are not direct messages themselves" do user = insert(:user) From c780298ce71577a1bffb755d6f183f049e6edf6e Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 11 May 2026 15:05:00 +0400 Subject: [PATCH 54/55] Add changelog for MFM support --- changelog.d/mfm-backend.add | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/mfm-backend.add diff --git a/changelog.d/mfm-backend.add b/changelog.d/mfm-backend.add new file mode 100644 index 000000000..f815c6828 --- /dev/null +++ b/changelog.d/mfm-backend.add @@ -0,0 +1 @@ +Add backend support for Misskey Markdown (MFM) posts From 47021b5aba4151284aefeeddbc8663ac80d5e19b Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 11 May 2026 16:37:59 +0400 Subject: [PATCH 55/55] Fix MFM validator alias ordering --- .../object_validators/article_note_page_validator.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex index ec9259f95..844ad2c8f 100644 --- a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex @@ -8,10 +8,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.HTML alias Pleroma.User - alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.CommonAPI.Utils import Ecto.Changeset