From a5905313eea18ce2bd1763ff23567f48c4defbea Mon Sep 17 00:00:00 2001 From: Nico Hinderling Date: Tue, 7 Apr 2026 13:33:38 -0700 Subject: [PATCH 1/8] feat: Add launchpad taskworker container Add a launchpad-taskworker service that shares the existing taskbroker infrastructure with the sentry taskworker. Activations are separated by the `application` value ("launchpad" vs "sentry") so workers only fetch their own tasks. --- .env | 2 ++ docker-compose.yml | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/.env b/.env index e1cb171a935..354ce11a2d0 100644 --- a/.env +++ b/.env @@ -11,11 +11,13 @@ SENTRY_BIND=9000 # SENTRY_MAIL_HOST=example.com # Parallel taskworker processes (higher values increase memory usage; >32 not recommended) SENTRY_TASKWORKER_CONCURRENCY=4 +LAUNCHPAD_TASKWORKER_CONCURRENCY=4 SENTRY_IMAGE=ghcr.io/getsentry/sentry:26.3.1 SNUBA_IMAGE=ghcr.io/getsentry/snuba:26.3.1 RELAY_IMAGE=ghcr.io/getsentry/relay:26.3.1 SYMBOLICATOR_IMAGE=ghcr.io/getsentry/symbolicator:26.3.1 TASKBROKER_IMAGE=ghcr.io/getsentry/taskbroker:26.3.1 +LAUNCHPAD_IMAGE=ghcr.io/getsentry/launchpad:nightly VROOM_IMAGE=ghcr.io/getsentry/vroom:26.3.1 UPTIME_CHECKER_IMAGE=ghcr.io/getsentry/uptime-checker:26.3.1 HEALTHCHECK_INTERVAL=30s diff --git a/docker-compose.yml b/docker-compose.yml index b364f667b48..d9fc2edd242 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -773,6 +773,27 @@ services: command: run taskworker --concurrency=$SENTRY_TASKWORKER_CONCURRENCY --rpc-host=taskbroker:50051 --health-check-file-path=/tmp/health.txt healthcheck: <<: *file_healthcheck_defaults + launchpad-taskworker: + <<: [*restart_policy, *pull_policy] + image: "$LAUNCHPAD_IMAGE" + command: worker --verbose + environment: + LAUNCHPAD_WORKER_RPC_HOST: "taskbroker:50051" + LAUNCHPAD_WORKER_CONCURRENCY: "${LAUNCHPAD_TASKWORKER_CONCURRENCY:-4}" + LAUNCHPAD_WORKER_HEALTH_CHECK_FILE_PATH: "/tmp/health.txt" + KAFKA_BOOTSTRAP_SERVERS: "kafka:9092" + SENTRY_BASE_URL: "http://web:9000" + LAUNCHPAD_RPC_SHARED_SECRET: "" + LAUNCHPAD_ENV: "self-hosted" + healthcheck: + <<: *file_healthcheck_defaults + depends_on: + kafka: + <<: *depends_on-healthy + taskbroker: + <<: *depends_on-default + web: + <<: *depends_on-healthy vroom: <<: [*restart_policy, *pull_policy] image: "$VROOM_IMAGE" From 8c9f1b19ade681689c0952c9d06f81f581605d75 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Fri, 10 Apr 2026 11:08:43 +0700 Subject: [PATCH 2/8] fix: make launchpad working on self-hosted --- .env | 1 + docker-compose.yml | 5 +++-- install/setup-custom-ca-certificate.sh | 13 ++++++++++++- install/update-docker-images.sh | 1 + 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.env b/.env index 537c66688b0..483f3748751 100644 --- a/.env +++ b/.env @@ -4,6 +4,7 @@ COMPOSE_PROJECT_NAME=sentry-self-hosted # See https://develop.sentry.dev/self-hosted/optional-features/errors-only/ COMPOSE_PROFILES=feature-complete SENTRY_EVENT_RETENTION_DAYS=90 +LAUNCHPAD_RPC_SHARED_SECRET=supersecret # You can either use a port number or an IP:PORT combo for SENTRY_BIND # See https://docs.docker.com/compose/compose-file/#ports for more SENTRY_BIND=9000 diff --git a/docker-compose.yml b/docker-compose.yml index d9fc2edd242..3801f5a1766 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -70,6 +70,7 @@ x-sentry-defaults: &sentry_defaults SENTRY_MAIL_HOST: SENTRY_MAX_EXTERNAL_SOURCEMAP_SIZE: SENTRY_SYSTEM_SECRET_KEY: + LAUNCHPAD_RPC_SHARED_SECRET: SENTRY_STATSD_ADDR: "${STATSD_ADDR:-}" volumes: - "sentry-data:/data" @@ -776,14 +777,14 @@ services: launchpad-taskworker: <<: [*restart_policy, *pull_policy] image: "$LAUNCHPAD_IMAGE" - command: worker --verbose + command: "worker --verbose" environment: LAUNCHPAD_WORKER_RPC_HOST: "taskbroker:50051" LAUNCHPAD_WORKER_CONCURRENCY: "${LAUNCHPAD_TASKWORKER_CONCURRENCY:-4}" LAUNCHPAD_WORKER_HEALTH_CHECK_FILE_PATH: "/tmp/health.txt" KAFKA_BOOTSTRAP_SERVERS: "kafka:9092" SENTRY_BASE_URL: "http://web:9000" - LAUNCHPAD_RPC_SHARED_SECRET: "" + LAUNCHPAD_RPC_SHARED_SECRET: LAUNCHPAD_ENV: "self-hosted" healthcheck: <<: *file_healthcheck_defaults diff --git a/install/setup-custom-ca-certificate.sh b/install/setup-custom-ca-certificate.sh index 581fee37b14..e5a458d46dd 100644 --- a/install/setup-custom-ca-certificate.sh +++ b/install/setup-custom-ca-certificate.sh @@ -59,7 +59,7 @@ if [[ "${SETUP_CUSTOM_CA_CERTIFICATE:-}" == "1" ]]; then # Pairs of service nickname and the env var that holds the image reference. # All of these are loaded from .env (via install/_lib.sh) before this script runs. - image_nicknames=(relay symbolicator snuba vroom taskbroker uptime-checker) + image_nicknames=(relay symbolicator snuba vroom taskbroker uptime-checker launchpad) image_names=( "${RELAY_IMAGE:-}" "${SYMBOLICATOR_IMAGE:-}" @@ -67,6 +67,7 @@ if [[ "${SETUP_CUSTOM_CA_CERTIFICATE:-}" == "1" ]]; then "${VROOM_IMAGE:-}" "${TASKBROKER_IMAGE:-}" "${UPTIME_CHECKER_IMAGE:-}" + "${LAUNCHPAD_IMAGE:-}" ) custom_ca_debug "Target services: ${image_nicknames[*]}" @@ -182,6 +183,14 @@ x-custom-ca-uptime-checker: &ca_uptime_checker source: ./certificates/.generated/uptime-checker/etc/ssl/certs target: /etc/ssl/certs +x-custom-ca-launchpad: &ca_launchpad + volumes: + - type: bind + read_only: true + source: ./certificates/.generated/launchpad/etc/ssl/certs + target: /etc/ssl/certs + + services: relay: <<: *ca_relay @@ -247,6 +256,8 @@ services: <<: *ca_taskbroker uptime-checker: <<: *ca_uptime_checker + launchpad-taskworker: + <<: *ca_launchpad YAML fi diff --git a/install/update-docker-images.sh b/install/update-docker-images.sh index 9e6cc230a33..34ddf484b6b 100644 --- a/install/update-docker-images.sh +++ b/install/update-docker-images.sh @@ -22,5 +22,6 @@ $CONTAINER_ENGINE pull ${RELAY_IMAGE} || true $CONTAINER_ENGINE pull ${TASKBROKER_IMAGE} || true $CONTAINER_ENGINE pull ${VROOM_IMAGE} || true $CONTAINER_ENGINE pull ${UPTIME_CHECKER_IMAGE} || true +$CONTAINER_ENGINE pull ${LAUNCHPAD_IMAGE} || true echo "${_endgroup}" From ef9ca4547555ccfb3b633aa91e77f49639c79748 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Fri, 10 Apr 2026 13:25:32 +0700 Subject: [PATCH 3/8] feat: add launchpad to feature-complete profile --- .github/ISSUE_TEMPLATE/release.yml | 1 + docker-compose.yml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/release.yml b/.github/ISSUE_TEMPLATE/release.yml index 36f872763a5..fabd3056042 100644 --- a/.github/ISSUE_TEMPLATE/release.yml +++ b/.github/ISSUE_TEMPLATE/release.yml @@ -17,6 +17,7 @@ body: - [ ] [`vroom`](https://github.com/getsentry/vroom/actions/workflows/release.yaml) - [ ] [`uptime-checker`](https://github.com/getsentry/uptime-checker/actions/workflows/release.yml) - [ ] [`taskbroker`](https://github.com/getsentry/taskbroker/actions/workflows/release.yml) + - [ ] [`launchpad`](https://github.com/getsentry/launchpad/actions/workflows/release.yml) - [ ] Release self-hosted. - [ ] [Prepare the `self-hosted` release](https://github.com/getsentry/self-hosted/actions/workflows/release.yml) (_replace with publish issue repo link_). - [ ] Check to make sure the new release branch in self-hosted includes the appropriate CalVer images. diff --git a/docker-compose.yml b/docker-compose.yml index 3801f5a1766..530505ff80b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -795,6 +795,8 @@ services: <<: *depends_on-default web: <<: *depends_on-healthy + profiles: + - feature-complete vroom: <<: [*restart_policy, *pull_policy] image: "$VROOM_IMAGE" From 5c19cfdf85f80e2ac4abb82dbfc17f337f92aab8 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Sat, 11 Apr 2026 12:10:22 +0700 Subject: [PATCH 4/8] feat: add required feature flag --- sentry/sentry.conf.example.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index 3bdc3e1a19d..4e5b23af2c2 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -433,6 +433,10 @@ def get_internal_network(): "organizations:ourlogs-stats", "organizations:ourlogs-replay-ui", ) + # Emerge Tools (Size Analysis, Build Distribution, etc) related flags + + ( + "organizations:preprod-frontend-routes", + ) } ) From ff95930ac3136c307dc645d231485d633d7ea4fe Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Sat, 11 Apr 2026 15:56:07 +0700 Subject: [PATCH 5/8] feat: integration test for mobile build --- _integration-test/test_01_basics.py | 36 ++++++++++++++++++++++++++++- action.yaml | 14 +++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/_integration-test/test_01_basics.py b/_integration-test/test_01_basics.py index a2073e2df11..0b36d63f85b 100644 --- a/_integration-test/test_01_basics.py +++ b/_integration-test/test_01_basics.py @@ -55,6 +55,16 @@ def get_sentry_dsn(client: httpx.Client) -> str: sentry_dsn = json.loads(response.text)[0]["dsn"]["public"] return sentry_dsn +@lru_cache +def get_organization_token(client: httpx.Client, name: str) -> str: + response = client.post( + f"{SENTRY_TEST_HOST}/api/0/organizations/pans/org-auth-tokens/", + follow_redirects=True, + data={"name": name}, + headers={"Referer": f"{SENTRY_TEST_HOST}/settings/pans/auth-tokens/new-token/"}, + ) + token = json.loads(response.text)["token"] + return token @pytest.fixture() def client_login(): @@ -76,7 +86,6 @@ def client_login(): assert login_response.status_code == 200 yield (client, login_response) - def test_initial_redirect(): initial_auth_redirect = httpx.get(SENTRY_TEST_HOST, follow_redirects=True) assert initial_auth_redirect.url == f"{SENTRY_TEST_HOST}/auth/login/sentry/" @@ -490,6 +499,31 @@ def test_receive_logs_events(client_login): lambda x: len(json.loads(x)["data"]) > 0, ) +@pytest.mark.skipif(os.environ.get("COMPOSE_PROFILES") != "feature-complete", reason="Only run if feature-complete") +def test_upload_mobile_builds(client_login): + client, _ = client_login + sentry_dsn = get_sentry_dsn(client) + + organization_auth_token = get_organization_token(client, "preprod") + env = os.environ.copy() + env["SENTRY_DSN"] = sentry_dsn + subprocess.run( + ["sentry-cli", "--log-level", "DEBUG", "--url", SENTRY_TEST_HOST, "--auth-token", organization_auth_token, "build", "upload", "hn.aab", "--org", "sentry", "--project", "internal"], + check=True, + shell=False, + env=env, + cwd="_integration-test/emerge-tools", + stdout=sys.stdout, + stderr=sys.stderr, + timeout=60, + ) + + poll_for_response( + f"{SENTRY_TEST_HOST}/api/0/organizations/sentry/builds/?display=size&per_page=25&project=-1&query=%21size_state%3Anot_ran&statsPeriod=24h&tab=mobile-builds", + client, + lambda x: len(json.loads(x)) > 0, + ) + def test_customizations(): commands = [ [ diff --git a/action.yaml b/action.yaml index 532fabf509e..9533c53f386 100644 --- a/action.yaml +++ b/action.yaml @@ -194,12 +194,26 @@ runs: with: node-version: "22.x" + - name: Setup Sentry CLI + shell: bash + run: curl -sL https://sentry.io/get-cli/ | SENTRY_CLI_VERSION="3.3.5" bash + - name: Install Nodejs dependencies shell: bash run: | cd ${{ github.action_path }}/_integration-test/nodejs npm ci + - name: Setup required Emerge Tools files + if: inputs.compose_profiles == 'feature-complete' + shell: bash + run: | + set -euo pipefail + cd ${{ github.action_path }} + mkdir -p _integration-test/emerge-tools + cd _integration-test/emerge-tools + curl -LO https://github.com/getsentry/launchpad/raw/893ad23dcfd81d70edbb26ea217d9d18e2ba81da/tests/_fixtures/android/hn.aab + - name: Integration Test shell: bash env: From 3e24e26a42112a68e25f42a2e662f034ba630f8f Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Sat, 11 Apr 2026 15:57:27 +0700 Subject: [PATCH 6/8] fix(test): wrong organization name --- _integration-test/test_01_basics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_integration-test/test_01_basics.py b/_integration-test/test_01_basics.py index 0b36d63f85b..9db046ced2e 100644 --- a/_integration-test/test_01_basics.py +++ b/_integration-test/test_01_basics.py @@ -58,10 +58,10 @@ def get_sentry_dsn(client: httpx.Client) -> str: @lru_cache def get_organization_token(client: httpx.Client, name: str) -> str: response = client.post( - f"{SENTRY_TEST_HOST}/api/0/organizations/pans/org-auth-tokens/", + f"{SENTRY_TEST_HOST}/api/0/organizations/sentry/org-auth-tokens/", follow_redirects=True, data={"name": name}, - headers={"Referer": f"{SENTRY_TEST_HOST}/settings/pans/auth-tokens/new-token/"}, + headers={"Referer": f"{SENTRY_TEST_HOST}/settings/sentry/auth-tokens/new-token/"}, ) token = json.loads(response.text)["token"] return token From 1afbcd5dc494c592cf3b01ff8f5559ed179081ed Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Sat, 11 Apr 2026 16:17:09 +0700 Subject: [PATCH 7/8] feat(test): debug organization token --- _integration-test/test_01_basics.py | 1 + 1 file changed, 1 insertion(+) diff --git a/_integration-test/test_01_basics.py b/_integration-test/test_01_basics.py index 9db046ced2e..bcafa3831b8 100644 --- a/_integration-test/test_01_basics.py +++ b/_integration-test/test_01_basics.py @@ -63,6 +63,7 @@ def get_organization_token(client: httpx.Client, name: str) -> str: data={"name": name}, headers={"Referer": f"{SENTRY_TEST_HOST}/settings/sentry/auth-tokens/new-token/"}, ) + print(response.text) token = json.loads(response.text)["token"] return token From 967e57fc3c9b57c6762691277718284e2e97c7e6 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Tue, 28 Apr 2026 20:12:35 +0700 Subject: [PATCH 8/8] test: acquire csrf token from login response Investigated and resolved CSRF token issue in integration tests for organization token creation. The organization auth tokens endpoint requires CSRF protection via the X-CSRFToken header, which should be populated with the value from the sc cookie obtained during the login response. This was discovered by examining the sentry codebase's OrganizationAuthTokensEndpoint which uses Django REST Framework's SessionAuthentication that enforces CSRF by default. The solution involves extracting the CSRF token from the client's cookies and passing it to POST requests as a request header. --- _integration-test/test_01_basics.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/_integration-test/test_01_basics.py b/_integration-test/test_01_basics.py index bcafa3831b8..f07ad82a43d 100644 --- a/_integration-test/test_01_basics.py +++ b/_integration-test/test_01_basics.py @@ -56,14 +56,16 @@ def get_sentry_dsn(client: httpx.Client) -> str: return sentry_dsn @lru_cache -def get_organization_token(client: httpx.Client, name: str) -> str: +def get_organization_token(client: httpx.Client, csrf_token: str, name: str) -> str: response = client.post( f"{SENTRY_TEST_HOST}/api/0/organizations/sentry/org-auth-tokens/", follow_redirects=True, data={"name": name}, - headers={"Referer": f"{SENTRY_TEST_HOST}/settings/sentry/auth-tokens/new-token/"}, + headers={ + "Referer": f"{SENTRY_TEST_HOST}/settings/sentry/auth-tokens/new-token/", + "X-CSRFToken": csrf_token, + }, ) - print(response.text) token = json.loads(response.text)["token"] return token @@ -502,10 +504,10 @@ def test_receive_logs_events(client_login): @pytest.mark.skipif(os.environ.get("COMPOSE_PROFILES") != "feature-complete", reason="Only run if feature-complete") def test_upload_mobile_builds(client_login): - client, _ = client_login + client, login_response = client_login sentry_dsn = get_sentry_dsn(client) - organization_auth_token = get_organization_token(client, "preprod") + organization_auth_token = get_organization_token(client, login_response.cookies["sc"], "preprod") env = os.environ.copy() env["SENTRY_DSN"] = sentry_dsn subprocess.run(