From 906e3897f286772dda0d0c40a3cc1a37af2a0570 Mon Sep 17 00:00:00 2001 From: Jiwon Kwon Date: Wed, 4 Mar 2026 16:52:23 +0900 Subject: [PATCH 1/5] Add interoperability smoke tests for Mastodon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an end-to-end smoke test suite that spins up a Mastodon instance via Docker Compose and verifies that Fedify can correctly exchange ActivityPub messages with it. The suite includes a lightweight Fedify test harness that runs inside the Docker network alongside Mastodon, and an orchestrator that drives six scenarios through the Mastodon API and harness backdoor endpoints: - Mastodon → Fedify (Follow) - Fedify → Mastodon (Follow) - Fedify → Mastodon (Create Note) - Mastodon → Fedify (Reply) - Mastodon → Fedify (Unfollow) - Fedify → Mastodon (Unfollow) Each follow scenario includes precondition checks (ensureNotFollowing / assertNotFollowing) to verify the relationship starts clean. Mastodon-specific files live in test/smoke/mastodon/ to support adding other server targets (e.g. Misskey) in sibling directories later. See: https://github.com/fedify-dev/fedify/issues/481 Co-Authored-By: Claude Opus 4.6 --- deno.lock | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/deno.lock b/deno.lock index fff0bfa37..131741962 100644 --- a/deno.lock +++ b/deno.lock @@ -78,7 +78,6 @@ "npm:@jimp/core@^1.6.0": "1.6.0", "npm:@jimp/wasm-webp@^1.6.0": "1.6.0", "npm:@js-temporal/polyfill@~0.5.1": "0.5.1", - "npm:@jsr/std__assert@0.226": "0.226.0", "npm:@mjackson/node-fetch-server@0.7": "0.7.0", "npm:@multiformats/base-x@^4.0.1": "4.0.1", "npm:@nestjs/common@^11.0.1": "11.1.17_reflect-metadata@0.2.2_rxjs@7.8.2", @@ -2272,17 +2271,6 @@ "wasm-feature-detect" ] }, - "@jsr/std__assert@0.226.0": { - "integrity": "sha512-xCuUFDfHkIZd96glKgjZbnYFqu6blu8Y53SyvDMlFDJm1Y/j+/FcW6xq7TzGFIaF5B9QecIlDfamfhzA8ZdVbg==", - "dependencies": [ - "@jsr/std__internal" - ], - "tarball": "https://npm.jsr.io/~/11/@jsr/std__assert/0.226.0.tgz" - }, - "@jsr/std__internal@1.0.12": { - "integrity": "sha512-6xReMW9p+paJgqoFRpOE2nogJFvzPfaLHLIlyADYjKMUcwDyjKZxryIbgcU+gxiTygn8yCjld1HoI0ET4/iZeA==", - "tarball": "https://npm.jsr.io/~/11/@jsr/std__internal/1.0.12.tgz" - }, "@logtape/logtape@1.3.6": { "integrity": "sha512-OaK8eal8zcjB0GZbllXKgUC2T9h/GyNLQyQXjJkf1yum7SZKTWs9gs/t8NMS0kVVaSnA7bhU0Sjws/Iy4e0/IQ==" }, @@ -7540,7 +7528,6 @@ "packageJson": { "dependencies": [ "npm:@js-temporal/polyfill@~0.5.1", - "npm:@jsr/std__assert@0.226", "npm:@types/node@^24.2.1", "npm:json-canon@^1.0.1", "npm:jsonld@9", From 49a296b591cccdec4bca98859978423db2efc167 Mon Sep 17 00:00:00 2001 From: Jiwon Kwon Date: Wed, 11 Mar 2026 17:32:19 +0900 Subject: [PATCH 2/5] Add strict-mode smoke test lane (HTTPS + signature verification) Add a second smoke test lane that validates Fedify's interoperability with Mastodon over HTTPS with HTTP signature verification enabled. The non-strict lane tests basic ActivityPub over HTTP; this lane adds TLS termination via Caddy proxies and a self-signed CA to verify the full signature chain works end-to-end. Architecture: - Standalone Docker Compose file with renamed backend services to avoid DNS collisions with Caddy TLS proxy aliases - Self-signed CA generated per CI run, trusted via SSL_CERT_FILE (Ruby) and DENO_CERT (Deno) - WebFinger-based account discovery in provisioning instead of DB pre-registration - STRICT_MODE env var toggles signature verification and URL scheme in the shared harness code Runs on nightly schedule and workflow_dispatch, not on every push. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/smoke-mastodon-strict.yml | 99 ++++++++++++ .gitignore | 2 + test/smoke/harness/backdoor.ts | 9 +- test/smoke/harness/federation.ts | 13 +- test/smoke/mastodon/Caddyfile.fedify-harness | 8 + test/smoke/mastodon/Caddyfile.mastodon | 8 + test/smoke/mastodon/docker-compose.strict.yml | 151 ++++++++++++++++++ test/smoke/mastodon/generate-certs.sh | 51 ++++++ test/smoke/mastodon/mastodon-strict.env | 24 +++ test/smoke/mastodon/provision-strict.sh | 89 +++++++++++ 10 files changed, 445 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/smoke-mastodon-strict.yml create mode 100644 test/smoke/mastodon/Caddyfile.fedify-harness create mode 100644 test/smoke/mastodon/Caddyfile.mastodon create mode 100644 test/smoke/mastodon/docker-compose.strict.yml create mode 100755 test/smoke/mastodon/generate-certs.sh create mode 100644 test/smoke/mastodon/mastodon-strict.env create mode 100755 test/smoke/mastodon/provision-strict.sh diff --git a/.github/workflows/smoke-mastodon-strict.yml b/.github/workflows/smoke-mastodon-strict.yml new file mode 100644 index 000000000..c7d70075a --- /dev/null +++ b/.github/workflows/smoke-mastodon-strict.yml @@ -0,0 +1,99 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json +# +# Strict-mode interoperability smoke tests (HTTPS + HTTP signature verification). +# Uses a standalone Docker Compose file with Caddy TLS proxies to verify that +# Fedify correctly signs and verifies requests over HTTPS. +# See: https://github.com/fedify-dev/fedify/issues/481 +name: smoke-mastodon-strict + +on: + schedule: + - cron: "0 6 * * *" + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + smoke: + runs-on: ubuntu-latest + timeout-minutes: 25 + + env: + COMPOSE: >- + docker compose + -f test/smoke/mastodon/docker-compose.strict.yml + + steps: + - uses: actions/checkout@v4 + + - uses: ./.github/actions/setup-mise + + - name: Generate TLS certificates + run: bash test/smoke/mastodon/generate-certs.sh test/smoke/mastodon/.certs + + - name: Verify certificates + run: | + openssl verify -CAfile test/smoke/mastodon/.certs/ca.crt \ + test/smoke/mastodon/.certs/fedify-harness.crt + openssl verify -CAfile test/smoke/mastodon/.certs/ca.crt \ + test/smoke/mastodon/.certs/mastodon.crt + + - name: Generate Mastodon secrets + run: | + IMAGE=ghcr.io/mastodon/mastodon:v4.3.9 + docker pull "$IMAGE" + + SECRET1=$(docker run --rm "$IMAGE" bundle exec rails secret) + SECRET2=$(docker run --rm "$IMAGE" bundle exec rails secret) + + { + echo "SECRET_KEY_BASE=$SECRET1" + echo "OTP_SECRET=$SECRET2" + docker run --rm "$IMAGE" bundle exec rails mastodon:webpush:generate_vapid_key \ + | grep -E '^[A-Z_]+=.+' + docker run --rm "$IMAGE" bundle exec rails db:encryption:init \ + | grep -E '^[A-Z_]+=.+' + } >> test/smoke/mastodon/mastodon-strict.env + + - name: Start database and redis + run: | + $COMPOSE up -d db redis + $COMPOSE exec -T db \ + sh -c 'until pg_isready -U mastodon; do sleep 1; done' + + - name: Run DB setup and migrations + run: | + $COMPOSE run --rm -T \ + mastodon-web-backend bundle exec rails db:setup + timeout-minutes: 5 + + - name: Start Mastodon stack + run: $COMPOSE up --wait + timeout-minutes: 12 + + - name: Provision Mastodon + run: bash test/smoke/mastodon/provision-strict.sh + + - name: Verify connectivity + run: | + echo "=== Harness health (from mastodon-sidekiq, via Caddy TLS) ===" + $COMPOSE exec -T mastodon-sidekiq \ + curl -sf https://fedify-harness/_test/health && echo " OK" || echo " FAIL" + + - name: Run smoke tests + run: | + set -a && source test/smoke/.env.test && set +a + deno run --allow-net --allow-env --unstable-temporal \ + test/smoke/orchestrator.ts + + - name: Collect logs on failure + if: failure() + run: | + echo "=== Docker Compose logs ===" + $COMPOSE logs --tail=500 + + - name: Teardown + if: always() + run: $COMPOSE down -v diff --git a/.gitignore b/.gitignore index 006d3b532..dad1a3cfd 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,9 @@ node_modules/ package-lock.json repomix-output.xml test/smoke/.env.test +test/smoke/mastodon/.certs/ test/smoke/mastodon/mastodon.env +test/smoke/mastodon/mastodon-strict.env smoke.log t.ts t2.ts diff --git a/test/smoke/harness/backdoor.ts b/test/smoke/harness/backdoor.ts index 316e826c2..091ff8192 100644 --- a/test/smoke/harness/backdoor.ts +++ b/test/smoke/harness/backdoor.ts @@ -9,14 +9,15 @@ function json(data: unknown, status = 200): Response { }); } -// Build recipient manually — Mastodon's WebFinger requires HTTPS but our -// harness only has HTTP. Parse the handle (user@domain) to construct the -// actor URI and inbox URL directly. +// Build recipient manually — in non-strict mode Mastodon's WebFinger requires +// HTTPS but our harness only has HTTP, so we use http:// for the inbox URL. +// In strict mode, Caddy terminates TLS, so we use https:// everywhere. function parseRecipient( handle: string, ): { inboxId: URL; actorId: URL } { const [user, domain] = handle.split("@"); - const inboxId = new URL(`http://${domain}/users/${user}/inbox`); + const scheme = Deno.env.get("STRICT_MODE") ? "https" : "http"; + const inboxId = new URL(`${scheme}://${domain}/users/${user}/inbox`); // Mastodon generates https:// actor URIs; use that as the canonical id const actorId = new URL(`https://${domain}/users/${user}`); return { inboxId, actorId }; diff --git a/test/smoke/harness/federation.ts b/test/smoke/harness/federation.ts index 5332681e1..25ecafb22 100644 --- a/test/smoke/harness/federation.ts +++ b/test/smoke/harness/federation.ts @@ -12,7 +12,7 @@ const federation = createFederation({ kv: new MemoryKvStore(), origin: ORIGIN, allowPrivateAddress: true, - skipSignatureVerification: true, + skipSignatureVerification: !Deno.env.get("STRICT_MODE"), }); federation @@ -48,12 +48,15 @@ federation if (!ctx.recipient || !followerUri) return; // Build the recipient manually instead of calling getActor(), because - // Mastodon generates https:// actor URIs but only serves HTTP. - // Rewrite the scheme so sendActivity POSTs over plain HTTP. - const httpActorUri = followerUri.href.replace(/^https:\/\//, "http://"); + // in non-strict mode Mastodon generates https:// actor URIs but only + // serves HTTP. In strict mode the Caddy proxy handles TLS, so we + // keep the original https:// scheme. + const actorUri = Deno.env.get("STRICT_MODE") + ? followerUri.href + : followerUri.href.replace(/^https:\/\//, "http://"); const recipient = { id: followerUri, - inboxId: new URL(`${httpActorUri}/inbox`), + inboxId: new URL(`${actorUri}/inbox`), }; const accept = new Accept({ diff --git a/test/smoke/mastodon/Caddyfile.fedify-harness b/test/smoke/mastodon/Caddyfile.fedify-harness new file mode 100644 index 000000000..b8f1ca64b --- /dev/null +++ b/test/smoke/mastodon/Caddyfile.fedify-harness @@ -0,0 +1,8 @@ +{ + auto_https off +} + +:443 { + tls /certs/fedify-harness.crt /certs/fedify-harness.key + reverse_proxy fedify-harness-backend:3001 +} diff --git a/test/smoke/mastodon/Caddyfile.mastodon b/test/smoke/mastodon/Caddyfile.mastodon new file mode 100644 index 000000000..86dc74d4f --- /dev/null +++ b/test/smoke/mastodon/Caddyfile.mastodon @@ -0,0 +1,8 @@ +{ + auto_https off +} + +:443 { + tls /certs/mastodon.crt /certs/mastodon.key + reverse_proxy mastodon-web-backend:3000 +} diff --git a/test/smoke/mastodon/docker-compose.strict.yml b/test/smoke/mastodon/docker-compose.strict.yml new file mode 100644 index 000000000..5d8aec78b --- /dev/null +++ b/test/smoke/mastodon/docker-compose.strict.yml @@ -0,0 +1,151 @@ +# Standalone Docker Compose for strict-mode smoke tests (HTTPS + signatures). +# Usage: docker compose -f docker-compose.strict.yml ... +# +# This is a standalone file (NOT an override) because Docker Compose merges +# network aliases additively and cannot remove a service's own DNS name from +# a network. Using an override would cause both the backend service and its +# Caddy proxy to resolve to the same hostname, breaking TLS routing. +# +# Architecture: +# - Backend services are renamed (e.g. fedify-harness-backend) so they +# don't collide with the TLS hostnames on the network +# - Caddy proxies claim the canonical hostnames (fedify-harness, mastodon) +# via network aliases and terminate TLS +# - All services share a single network for simplicity; DNS resolution +# is unambiguous because backend names differ from Caddy aliases + +volumes: + harness-node-modules: + +networks: + smoke: + driver: bridge + +services: + db: + image: postgres:15-alpine + environment: + POSTGRES_DB: mastodon + POSTGRES_USER: mastodon + POSTGRES_PASSWORD: mastodon + networks: [smoke] + healthcheck: + test: ["CMD", "pg_isready", "-U", "mastodon"] + interval: 5s + retries: 10 + + redis: + image: redis:7-alpine + networks: [smoke] + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + retries: 10 + + # Fedify test harness — renamed to avoid colliding with the Caddy alias. + fedify-harness-backend: + image: denoland/deno:2.7.1 + working_dir: /workspace + volumes: + - ../../../:/workspace + - harness-node-modules:/workspace/node_modules + - ./.certs:/certs:ro + command: + - run + - --allow-net + - --allow-env + - --allow-read + - --allow-write + - --unstable-temporal + - test/smoke/harness/main.ts + environment: + HARNESS_ORIGIN: "https://fedify-harness" + STRICT_MODE: "1" + DENO_CERT: "/certs/ca.crt" + networks: [smoke] + ports: ["3001:3001"] + healthcheck: + test: + [ + "CMD", + "deno", + "eval", + "const r = await fetch('http://localhost:3001/_test/health'); if (!r.ok) Deno.exit(1);", + ] + interval: 5s + retries: 30 + + # Caddy TLS proxy for the Fedify harness. + # Owns the "fedify-harness" hostname so other containers reach TLS. + caddy-harness: + image: caddy:2-alpine + volumes: + - ./Caddyfile.fedify-harness:/etc/caddy/Caddyfile:ro + - ./.certs:/certs:ro + networks: + smoke: + aliases: [fedify-harness] + depends_on: + fedify-harness-backend: { condition: service_healthy } + + # Mastodon web — renamed to avoid colliding with the Caddy alias. + mastodon-web-backend: + image: ghcr.io/mastodon/mastodon:v4.3.9 + command: + - bash + - -c + - | + cat /usr/lib/ssl/cert.pem /certs/ca.crt > /tmp/ca-bundle.crt + bundle exec rails s -p 3000 -b 0.0.0.0 + env_file: mastodon-strict.env + environment: + SSL_CERT_FILE: /tmp/ca-bundle.crt + volumes: + - ./disable_force_ssl.rb:/opt/mastodon/config/initializers/zz_disable_force_ssl.rb:ro + - ./.certs:/certs:ro + networks: [smoke] + ports: ["3000:3000"] + depends_on: + db: { condition: service_healthy } + redis: { condition: service_healthy } + healthcheck: + test: + [ + "CMD-SHELL", + "curl -sf http://localhost:3000/health | grep -q OK", + ] + interval: 10s + retries: 18 + + # Caddy TLS proxy for Mastodon. + # Owns the "mastodon" hostname so other containers reach TLS. + caddy-mastodon: + image: caddy:2-alpine + volumes: + - ./Caddyfile.mastodon:/etc/caddy/Caddyfile:ro + - ./.certs:/certs:ro + networks: + smoke: + aliases: [mastodon] + ports: ["4443:443"] + depends_on: + mastodon-web-backend: { condition: service_healthy } + + mastodon-sidekiq: + image: ghcr.io/mastodon/mastodon:v4.3.9 + command: + - bash + - -c + - | + cat /usr/lib/ssl/cert.pem /certs/ca.crt > /tmp/ca-bundle.crt + bundle exec sidekiq -q ingress -q default -q push + env_file: mastodon-strict.env + environment: + SSL_CERT_FILE: /tmp/ca-bundle.crt + volumes: + - ./disable_force_ssl.rb:/opt/mastodon/config/initializers/zz_disable_force_ssl.rb:ro + - ./.certs:/certs:ro + networks: [smoke] + depends_on: + mastodon-web-backend: { condition: service_healthy } + fedify-harness-backend: { condition: service_healthy } diff --git a/test/smoke/mastodon/generate-certs.sh b/test/smoke/mastodon/generate-certs.sh new file mode 100755 index 000000000..98e4c4838 --- /dev/null +++ b/test/smoke/mastodon/generate-certs.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# Generate a self-signed CA and leaf certificates for strict-mode smoke tests. +# Usage: bash generate-certs.sh [output-dir] +# +# Output directory defaults to .certs/ (relative to this script). +# The CA is ephemeral — generated fresh each run. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +OUT="${1:-$SCRIPT_DIR/.certs}" +mkdir -p "$OUT" + +HOSTS=(fedify-harness mastodon) + +echo "→ Generating CA key + certificate..." +openssl genrsa -out "$OUT/ca.key" 2048 2>/dev/null +openssl req -x509 -new -nodes \ + -key "$OUT/ca.key" \ + -sha256 -days 1 \ + -subj "/CN=Smoke Test CA" \ + -out "$OUT/ca.crt" 2>/dev/null + +for HOST in "${HOSTS[@]}"; do + echo "→ Generating certificate for $HOST..." + openssl genrsa -out "$OUT/$HOST.key" 2048 2>/dev/null + openssl req -new \ + -key "$OUT/$HOST.key" \ + -subj "/CN=$HOST" \ + -out "$OUT/$HOST.csr" 2>/dev/null + + # Create a SAN extension config so the cert is valid for the hostname + cat > "$OUT/$HOST.ext" </dev/null + + rm -f "$OUT/$HOST.csr" "$OUT/$HOST.ext" +done + +rm -f "$OUT/ca.srl" + +echo "✓ Certificates written to $OUT/" +ls -la "$OUT" diff --git a/test/smoke/mastodon/mastodon-strict.env b/test/smoke/mastodon/mastodon-strict.env new file mode 100644 index 000000000..3219bb72c --- /dev/null +++ b/test/smoke/mastodon/mastodon-strict.env @@ -0,0 +1,24 @@ +# Mastodon configuration for strict-mode smoke tests (HTTPS). +# SECRET_KEY_BASE, OTP_SECRET, VAPID_*, and ACTIVE_RECORD_ENCRYPTION_* +# are appended by CI (see .github/workflows/smoke-mastodon-strict.yml). + +LOCAL_DOMAIN=mastodon +ALTERNATE_DOMAINS=localhost:3000,localhost:4443 +LOCAL_HTTPS=true +RAILS_ENV=production +DB_HOST=db +DB_PORT=5432 +DB_NAME=mastodon +DB_USER=mastodon +DB_PASS=mastodon +REDIS_HOST=redis +REDIS_PORT=6379 +SMTP_SERVER=localhost +SMTP_PORT=25 +SMTP_FROM_ADDRESS=noreply@localhost +SMTP_AUTH_METHOD=none +SMTP_OPENSSL_VERIFY_MODE=none +SMTP_DELIVERY_METHOD=none +ES_ENABLED=false +RAILS_LOG_TO_STDOUT=true +ALLOWED_PRIVATE_ADDRESSES=0.0.0.0/0 diff --git a/test/smoke/mastodon/provision-strict.sh b/test/smoke/mastodon/provision-strict.sh new file mode 100755 index 000000000..e738c5200 --- /dev/null +++ b/test/smoke/mastodon/provision-strict.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# Provision Mastodon for strict-mode smoke tests (HTTPS + signature verification). +# +# Differences from provision.sh: +# - Uses WebFinger discovery (ResolveAccountService) instead of DB pre-registration +# - Writes HTTPS URLs to .env.test +# - Talks to mastodon-web backend directly (HTTP on port 3000) for API calls +set -euo pipefail + +COMPOSE="docker compose -f test/smoke/mastodon/docker-compose.strict.yml" + +echo "→ Creating test user..." +$COMPOSE exec -T mastodon-web-backend bin/tootctl accounts create \ + testuser --email=test@localhost --confirmed \ + || true # may already exist on re-run + +echo "→ Approving and activating test user..." +$COMPOSE exec -T mastodon-web-backend bin/rails runner - <<'RUBY' +user = Account.find_local('testuser').user +user.update!(approved: true, confirmed_at: Time.now.utc) +user.approve! if user.respond_to?(:approve!) +RUBY + +echo "→ Generating API token via Rails..." +RAW=$($COMPOSE exec -T mastodon-web-backend bin/rails runner - <<'RUBY' 2>&1 | tr -d '\r' +user = Account.find_local('testuser').user +app = Doorkeeper::Application.find_or_create_by!(name: 'smoke-test') do |a| + a.redirect_uri = 'urn:ietf:wg:oauth:2.0:oob' + a.scopes = 'read write follow' +end +token = Doorkeeper::AccessToken.find_or_create_for( + application: app, + resource_owner: user, + scopes: Doorkeeper::OAuth::Scopes.from_string('read write follow'), + expires_in: nil, + use_refresh_token: false +) +print "SMOKE_TOKEN=#{token.token}" +RUBY +) + +TOKEN=$(echo "$RAW" | grep -oP 'SMOKE_TOKEN=\K\S+' | tail -1) + +if [ -z "$TOKEN" ]; then + echo "✗ Failed to generate API token" + exit 1 +fi + +# Verify token works — talk directly to the backend (HTTP, port 3000) +echo "→ Verifying token..." +HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' \ + -H "Authorization: Bearer $TOKEN" \ + http://localhost:3000/api/v1/accounts/verify_credentials) +echo " verify_credentials → HTTP $HTTP_CODE" +if [ "$HTTP_CODE" != "200" ]; then + echo "✗ Token verification failed (HTTP $HTTP_CODE)" + exit 1 +fi + +echo "→ Resolving Fedify account via WebFinger (ResolveAccountService)..." +# Use Mastodon's built-in account resolution, which performs WebFinger over +# HTTPS to the Caddy-fronted harness. This validates that the full TLS + +# WebFinger chain works. +$COMPOSE exec -T mastodon-web-backend bin/rails runner - <<'RUBY' +account = ResolveAccountService.new.call('testuser@fedify-harness') +if account.nil? + abort "✗ ResolveAccountService returned nil — WebFinger discovery failed" +end +print "RESOLVED=#{account.id} (#{account.uri})" +RUBY + +echo "→ Creating follow relationship (Fedify → Mastodon) in DB..." +$COMPOSE exec -T mastodon-web-backend bin/rails runner - <<'RUBY' +fedify_account = Account.find_by!(username: 'testuser', domain: 'fedify-harness') +local_account = Account.find_local('testuser') +follow = Follow.find_or_create_by!(account: fedify_account, target_account: local_account) +print "FOLLOW=#{follow.id}" +RUBY + +echo "→ Writing test env..." +cat > test/smoke/.env.test < Date: Sat, 28 Mar 2026 16:41:05 +0900 Subject: [PATCH 3/5] Align strict-mode connectivity check with non-strict lane Add mastodon-web-backend health check alongside the existing mastodon-sidekiq check, and make curl failures fail the step immediately instead of silently continuing. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/smoke-mastodon-strict.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/smoke-mastodon-strict.yml b/.github/workflows/smoke-mastodon-strict.yml index c7d70075a..05d5802dd 100644 --- a/.github/workflows/smoke-mastodon-strict.yml +++ b/.github/workflows/smoke-mastodon-strict.yml @@ -78,9 +78,15 @@ jobs: - name: Verify connectivity run: | + echo "=== Harness health (from mastodon-web-backend, via Caddy TLS) ===" + $COMPOSE exec -T mastodon-web-backend \ + curl -sf https://fedify-harness/_test/health + echo " OK" + echo "=== Harness health (from mastodon-sidekiq, via Caddy TLS) ===" $COMPOSE exec -T mastodon-sidekiq \ - curl -sf https://fedify-harness/_test/health && echo " OK" || echo " FAIL" + curl -sf https://fedify-harness/_test/health + echo " OK" - name: Run smoke tests run: | From ae280f3a9cbd560965fa19b5c7ccbccf496bdd67 Mon Sep 17 00:00:00 2001 From: Jiwon Kwon Date: Sat, 28 Mar 2026 19:47:36 +0900 Subject: [PATCH 4/5] Pin Caddy images, add healthchecks, and fix sidekiq dependency - Pin Caddy images to 2.11.2-alpine for reproducible CI builds - Add healthchecks to caddy-harness and caddy-mastodon so docker compose --wait blocks until proxies are ready - Make mastodon-sidekiq depend on caddy-harness (instead of fedify-harness-backend directly) to ensure TLS proxy is ready before Sidekiq attempts HTTPS deliveries Co-Authored-By: Claude Opus 4.6 --- test/smoke/mastodon/docker-compose.strict.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/test/smoke/mastodon/docker-compose.strict.yml b/test/smoke/mastodon/docker-compose.strict.yml index 5d8aec78b..70bfa096f 100644 --- a/test/smoke/mastodon/docker-compose.strict.yml +++ b/test/smoke/mastodon/docker-compose.strict.yml @@ -78,7 +78,7 @@ services: # Caddy TLS proxy for the Fedify harness. # Owns the "fedify-harness" hostname so other containers reach TLS. caddy-harness: - image: caddy:2-alpine + image: caddy:2.11.2-alpine volumes: - ./Caddyfile.fedify-harness:/etc/caddy/Caddyfile:ro - ./.certs:/certs:ro @@ -87,6 +87,10 @@ services: aliases: [fedify-harness] depends_on: fedify-harness-backend: { condition: service_healthy } + healthcheck: + test: ["CMD", "caddy", "version"] + interval: 5s + retries: 5 # Mastodon web — renamed to avoid colliding with the Caddy alias. mastodon-web-backend: @@ -120,7 +124,7 @@ services: # Caddy TLS proxy for Mastodon. # Owns the "mastodon" hostname so other containers reach TLS. caddy-mastodon: - image: caddy:2-alpine + image: caddy:2.11.2-alpine volumes: - ./Caddyfile.mastodon:/etc/caddy/Caddyfile:ro - ./.certs:/certs:ro @@ -130,6 +134,10 @@ services: ports: ["4443:443"] depends_on: mastodon-web-backend: { condition: service_healthy } + healthcheck: + test: ["CMD", "caddy", "version"] + interval: 5s + retries: 5 mastodon-sidekiq: image: ghcr.io/mastodon/mastodon:v4.3.9 @@ -148,4 +156,4 @@ services: networks: [smoke] depends_on: mastodon-web-backend: { condition: service_healthy } - fedify-harness-backend: { condition: service_healthy } + caddy-harness: { condition: service_healthy } From c6db09a5636f4c1e32de51e67be9785191e7ac1a Mon Sep 17 00:00:00 2001 From: Jiwon Kwon Date: Sat, 28 Mar 2026 20:23:41 +0900 Subject: [PATCH 5/5] Update deno.lock Co-Authored-By: Claude Opus 4.6 --- deno.lock | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/deno.lock b/deno.lock index 131741962..fff0bfa37 100644 --- a/deno.lock +++ b/deno.lock @@ -78,6 +78,7 @@ "npm:@jimp/core@^1.6.0": "1.6.0", "npm:@jimp/wasm-webp@^1.6.0": "1.6.0", "npm:@js-temporal/polyfill@~0.5.1": "0.5.1", + "npm:@jsr/std__assert@0.226": "0.226.0", "npm:@mjackson/node-fetch-server@0.7": "0.7.0", "npm:@multiformats/base-x@^4.0.1": "4.0.1", "npm:@nestjs/common@^11.0.1": "11.1.17_reflect-metadata@0.2.2_rxjs@7.8.2", @@ -2271,6 +2272,17 @@ "wasm-feature-detect" ] }, + "@jsr/std__assert@0.226.0": { + "integrity": "sha512-xCuUFDfHkIZd96glKgjZbnYFqu6blu8Y53SyvDMlFDJm1Y/j+/FcW6xq7TzGFIaF5B9QecIlDfamfhzA8ZdVbg==", + "dependencies": [ + "@jsr/std__internal" + ], + "tarball": "https://npm.jsr.io/~/11/@jsr/std__assert/0.226.0.tgz" + }, + "@jsr/std__internal@1.0.12": { + "integrity": "sha512-6xReMW9p+paJgqoFRpOE2nogJFvzPfaLHLIlyADYjKMUcwDyjKZxryIbgcU+gxiTygn8yCjld1HoI0ET4/iZeA==", + "tarball": "https://npm.jsr.io/~/11/@jsr/std__internal/1.0.12.tgz" + }, "@logtape/logtape@1.3.6": { "integrity": "sha512-OaK8eal8zcjB0GZbllXKgUC2T9h/GyNLQyQXjJkf1yum7SZKTWs9gs/t8NMS0kVVaSnA7bhU0Sjws/Iy4e0/IQ==" }, @@ -7528,6 +7540,7 @@ "packageJson": { "dependencies": [ "npm:@js-temporal/polyfill@~0.5.1", + "npm:@jsr/std__assert@0.226", "npm:@types/node@^24.2.1", "npm:json-canon@^1.0.1", "npm:jsonld@9",