From a4033d09460cbaa680ef45824defcd05ba05b1a6 Mon Sep 17 00:00:00 2001 From: Evie Gauthier Date: Thu, 9 Jul 2026 08:04:30 -0400 Subject: [PATCH 1/2] Verify Sentry release artifact workflow --- SENTRY.md | 37 ++++ .../sentryReleaseArtifacts.test.ts | 162 ++++++++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 src/observability/sentryReleaseArtifacts.test.ts diff --git a/SENTRY.md b/SENTRY.md index 970800d8..283820dd 100644 --- a/SENTRY.md +++ b/SENTRY.md @@ -108,6 +108,43 @@ bundle shape as shipped releases. Manual runs can override the Sentry release name and environment; tag runs default the release name to the tag, and manual runs without a release input default to the commit SHA. +Manual dispatch is safe only after the repository owner has configured the four +required secrets above. A dry repo-side check can validate workflow syntax, +release/env propagation, and build wiring, but it cannot prove Sentry accepted +the sourcemaps, debug files, Android mappings/native symbols, or Size Analysis +build without those secrets and a real workflow run. + +Suggested owner-side dispatch for a release candidate: + +```sh +gh workflow run sentry-release-artifacts.yml \ + --ref main \ + -f release=charm@2.0.0-rc.1 \ + -f environment=production +``` + +For a tag release, prefer running from the tag ref and omit `release` so the +workflow uses the tag name: + +```sh +gh workflow run sentry-release-artifacts.yml \ + --ref v2.0.0 \ + -f environment=production +``` + +Sentry-side verification after the workflow finishes: + +- Releases: the selected release exists and has frontend artifacts with debug + IDs/source maps for the built JavaScript chunks. +- Debug files: Linux, macOS, Windows, and iOS simulator debug files appear under + the `charm` project and are processed without unresolved upload errors. +- Android: ProGuard/R8 mapping and native symbols are associated with the + release build uploaded by the Gradle plugin. +- Size Analysis: the Android APK/AAB appears in Sentry Size Analysis with the + configured `Release` build configuration and base SHA. +- Workflow artifacts: GitHub contains the frontend, Linux, Apple, Windows, and + Android size report artifacts for the run. + Sentry Size Analysis currently receives Android builds only. The current iOS CI path builds an unsigned simulator debug app, while Sentry accepts XCArchive or IPA inputs for iOS size analysis; wire that upload once a signed device-release diff --git a/src/observability/sentryReleaseArtifacts.test.ts b/src/observability/sentryReleaseArtifacts.test.ts new file mode 100644 index 00000000..e4503073 --- /dev/null +++ b/src/observability/sentryReleaseArtifacts.test.ts @@ -0,0 +1,162 @@ +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { spawnSync } from "node:child_process"; +import { describe, expect, it } from "vitest"; + +const root = process.cwd(); + +function readRepoFile(path: string): string { + return readFileSync(join(root, path), "utf8"); +} + +function configureSentryReleaseEnv(env: Record = {}) { + const dir = mkdtempSync(join(tmpdir(), "charm-sentry-release-env-")); + const githubEnv = join(dir, "github-env"); + const result = spawnSync( + "bash", + [join(root, ".github/scripts/configure-sentry-release-env.sh")], + { + cwd: root, + encoding: "utf8", + env: { + ...process.env, + GITHUB_ENV: githubEnv, + SENTRY_AUTH_TOKEN: "token", + SENTRY_ORG: "cloudhubsocial", + SENTRY_PROJECT: "charm", + GITHUB_SHA: "local-test-sha", + ...env, + }, + }, + ); + const githubEnvContents = result.status === 0 ? readFileSync(githubEnv, "utf8") : ""; + rmSync(dir, { recursive: true, force: true }); + + return { ...result, githubEnvContents }; +} + +describe("Sentry release artifact workflow", () => { + it("keeps the frontend release build guarded and uploadable to Sentry", () => { + const workflow = readRepoFile(".github/workflows/sentry-release-artifacts.yml"); + const viteConfig = readRepoFile("vite.config.ts"); + + expect(workflow).toContain("workflow_dispatch:"); + expect(workflow).toContain('REQUIRE_VITE_SENTRY_DSN: "true"'); + expect(workflow).toContain('WRITE_FRONTEND_UPLOAD_ENV: "true"'); + expect(workflow).toContain("SENTRY_UPLOAD=true"); + expect(workflow).toContain("pnpm build"); + expect(workflow).toContain("Verify sourcemaps are not left in dist"); + expect(viteConfig).toContain("build: { sourcemap: sentryEnabled }"); + expect(viteConfig).toContain("sentryVitePlugin"); + expect(viteConfig).toContain( + "release: { name: procEnv.SENTRY_RELEASE || procEnv.npm_package_version }", + ); + expect(viteConfig).toContain('sourcemaps: { filesToDeleteAfterUpload: ["dist/**/*.map"] }'); + expect(viteConfig).toContain("reactComponentAnnotation: { enabled: true }"); + }); + + it("keeps native debug files and Android build analysis wired to blocking uploads", () => { + const workflow = readRepoFile(".github/workflows/sentry-release-artifacts.yml"); + const configureEnv = readRepoFile(".github/scripts/configure-sentry-release-env.sh"); + const androidGradle = readRepoFile("src-tauri/gen/android/app/build.gradle.kts"); + + expect(workflow.match(/WRITE_RUST_DEBUG_ENV: "true"/g)?.length).toBeGreaterThanOrEqual(4); + expect(configureEnv).toContain("CARGO_PROFILE_RELEASE_DEBUG=1"); + expect(workflow.match(/@sentry\/cli@3\.5\.1 debug-files upload/g)).toHaveLength(3); + expect(workflow.match(/--include-sources/g)?.length).toBeGreaterThanOrEqual(3); + expect(workflow.match(/--wait/g)?.length).toBeGreaterThanOrEqual(3); + expect(workflow).toContain('SENTRY_ANDROID_UPLOAD: "true"'); + expect(workflow).toContain("pnpm tauri android build --ci"); + expect(workflow).toContain("@sentry/cli@3.5.1 build upload"); + expect(workflow).toContain("--base-sha"); + expect(androidGradle).toContain("includeProguardMapping.set(true)"); + expect(androidGradle).toContain("autoUploadProguardMapping.set(true)"); + expect(androidGradle).toContain("uploadNativeSymbols.set(true)"); + expect(androidGradle).toContain("autoUploadNativeSymbols.set(true)"); + expect(androidGradle).toContain("includeNativeSources.set(true)"); + }); + + it("documents manual dispatch requirements and Sentry-side verification", () => { + const sentryDoc = readRepoFile("SENTRY.md"); + + expect(sentryDoc).toContain("gh workflow run sentry-release-artifacts.yml"); + expect(sentryDoc).toContain("SENTRY_AUTH_TOKEN"); + expect(sentryDoc).toContain("VITE_SENTRY_DSN"); + expect(sentryDoc).toContain("Sentry-side verification"); + expect(sentryDoc).toContain("debug files"); + expect(sentryDoc).toContain("Size Analysis"); + }); +}); + +describe("configure-sentry-release-env.sh", () => { + it("writes frontend upload environment from manual dispatch inputs", () => { + const result = configureSentryReleaseEnv({ + RELEASE_INPUT: "charm@2.0.0-test", + ENVIRONMENT_INPUT: "staging", + VITE_SENTRY_DSN: "https://public@example.invalid/1", + REQUIRE_VITE_SENTRY_DSN: "true", + WRITE_FRONTEND_UPLOAD_ENV: "true", + }); + + expect(result.status).toBe(0); + expect(result.githubEnvContents).toContain("SENTRY_RELEASE=charm@2.0.0-test"); + expect(result.githubEnvContents).toContain("SENTRY_UPLOAD=true"); + expect(result.githubEnvContents).toContain("VITE_SENTRY_RELEASE=charm@2.0.0-test"); + expect(result.githubEnvContents).toContain("SENTRY_ENVIRONMENT=staging"); + expect(result.githubEnvContents).toContain("VITE_SENTRY_ENVIRONMENT=staging"); + }); + + it("defaults tag releases to the tag and manual releases to the commit SHA", () => { + const tagResult = configureSentryReleaseEnv({ + GITHUB_REF_TYPE: "tag", + GITHUB_REF_NAME: "v2.0.0", + GITHUB_SHA: "abc123", + }); + const shaResult = configureSentryReleaseEnv({ + GITHUB_REF_TYPE: "branch", + GITHUB_SHA: "def456", + }); + + expect(tagResult.status).toBe(0); + expect(tagResult.githubEnvContents).toContain("SENTRY_RELEASE=v2.0.0"); + expect(shaResult.status).toBe(0); + expect(shaResult.githubEnvContents).toContain("SENTRY_RELEASE=def456"); + }); + + it("fails before upload when required Sentry secrets are missing", () => { + const result = configureSentryReleaseEnv({ + SENTRY_AUTH_TOKEN: "", + SENTRY_ORG: "", + SENTRY_PROJECT: "", + VITE_SENTRY_DSN: "", + REQUIRE_VITE_SENTRY_DSN: "true", + }); + + expect(result.status).toBe(1); + expect(result.stdout).toContain( + "::error::Missing required Sentry secret(s): SENTRY_AUTH_TOKEN SENTRY_ORG SENTRY_PROJECT VITE_SENTRY_DSN", + ); + }); + + it("rejects multiline release and environment values before writing GitHub env", () => { + const releaseResult = configureSentryReleaseEnv({ + RELEASE_INPUT: "release\ninjection", + }); + const environmentResult = configureSentryReleaseEnv({ + ENVIRONMENT_INPUT: "prod\ninjection", + WRITE_FRONTEND_UPLOAD_ENV: "true", + VITE_SENTRY_DSN: "https://public@example.invalid/1", + REQUIRE_VITE_SENTRY_DSN: "true", + }); + + expect(releaseResult.status).toBe(1); + expect(releaseResult.stdout).toContain( + "::error::Sentry release names must be single-line values", + ); + expect(environmentResult.status).toBe(1); + expect(environmentResult.stdout).toContain( + "::error::Sentry environment names must be single-line values", + ); + }); +}); From 7db906736cec2bc783eaf9b4d6ac655c28d5631f Mon Sep 17 00:00:00 2001 From: Evie Gauthier Date: Thu, 9 Jul 2026 08:17:21 -0400 Subject: [PATCH 2/2] Address Sentry artifact review feedback --- src/observability/sentryReleaseArtifacts.test.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/observability/sentryReleaseArtifacts.test.ts b/src/observability/sentryReleaseArtifacts.test.ts index e4503073..e124bcee 100644 --- a/src/observability/sentryReleaseArtifacts.test.ts +++ b/src/observability/sentryReleaseArtifacts.test.ts @@ -1,10 +1,12 @@ import { mkdtempSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; -import { join } from "node:path"; +import { dirname, join, resolve } from "node:path"; import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; -const root = process.cwd(); +const root = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); +const describeWithBash = process.platform === "win32" ? describe.skip : describe; function readRepoFile(path: string): string { return readFileSync(join(root, path), "utf8"); @@ -46,7 +48,8 @@ describe("Sentry release artifact workflow", () => { expect(workflow).toContain('WRITE_FRONTEND_UPLOAD_ENV: "true"'); expect(workflow).toContain("SENTRY_UPLOAD=true"); expect(workflow).toContain("pnpm build"); - expect(workflow).toContain("Verify sourcemaps are not left in dist"); + expect(workflow).toContain("find dist -name '*.map' -print -quit"); + expect(workflow).toContain("Sentry sourcemap upload left .map files in dist"); expect(viteConfig).toContain("build: { sourcemap: sentryEnabled }"); expect(viteConfig).toContain("sentryVitePlugin"); expect(viteConfig).toContain( @@ -89,7 +92,7 @@ describe("Sentry release artifact workflow", () => { }); }); -describe("configure-sentry-release-env.sh", () => { +describeWithBash("configure-sentry-release-env.sh", () => { it("writes frontend upload environment from manual dispatch inputs", () => { const result = configureSentryReleaseEnv({ RELEASE_INPUT: "charm@2.0.0-test",