From 9b9eff4581599b196fd0233b44d8f1627fcc5317 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Thu, 5 Nov 2026 15:44:32 +0100 Subject: [PATCH 1/5] feat: Add `debug_files` input to upload debug information files Co-Authored-By: Claude Opus 5.5 --- README.md | 10 ++++++++++ __tests__/main.test.ts | 16 ++++++++++++++++ action.yml | 10 +++++++++- dist/index.js | 25 ++++++++++++++++++++++++- src/main.ts | 13 +++++++++++++ src/options.ts | 13 +++++++++++++ 6 files changed, 85 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c9d003de..eaa9e866 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ Adding the following to your workflow will create a new Sentry release and tell | `environment` | Set the environment for this release. E.g. "production" or "staging". Omit to skip adding deploy to release. | - | | `sourcemaps` | Space-separated list of paths to JavaScript sourcemaps. Omit to skip uploading sourcemaps. | - | | `inject` | Injects Debug IDs into source files and source maps to ensure proper un-minifcation of your stacktraces. Does nothing if `sourcemaps` was not set. | `true` | +| `debug_files` | Space-separated list of paths to debug information files (e.g. dSYM, PDB, ELF, Breakpad). Omit to skip uploading debug files. | - | | `finalize` | When false, omit marking the release as finalized and released. | `true` | | `ignore_missing` | When the flag is set and the previous release commit was not found in the repository, will create a release with the default commits count instead of failing the command. | `false` | | `ignore_empty` | When the flag is set, command will not fail and just exit silently if no new commits for a given release have been found. | `false` | @@ -103,6 +104,15 @@ Adding the following to your workflow will create a new Sentry release and tell sourcemaps: './dist' ``` +- Create a new Sentry release and upload debug information files for a native app. + + ```yaml + - uses: getsentry/action-release@v3 + with: + environment: 'production' + debug_files: './build/app.pdb ./build/app.dSYM' + ``` + - Create a new Sentry release for the `production` environment of your project at version `v1.0.1`. ```yaml diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index a68f1507..36941c06 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -6,6 +6,7 @@ import { getBooleanOption, getDist, getSourcemaps, + getDebugFiles, getStartedAt, getRelease, getSetCommitsOption, @@ -68,6 +69,21 @@ describe('options', () => { }); }); + describe('getDebugFiles', () => { + afterEach(() => { + delete process.env['INPUT_DEBUG_FILES']; + }); + + test('should return empty list when debug_files is omitted', () => { + expect(getDebugFiles()).toEqual([]); + }); + + test('should split space-separated paths', () => { + process.env['INPUT_DEBUG_FILES'] = './build/app.pdb ./build/app.dSYM'; + expect(getDebugFiles()).toEqual(['./build/app.pdb', './build/app.dSYM']); + }); + }); + describe('getDist', () => { afterEach(() => { delete process.env['INPUT_DIST']; diff --git a/action.yml b/action.yml index f6621be7..ac2a8e9f 100644 --- a/action.yml +++ b/action.yml @@ -20,6 +20,12 @@ inputs: Does nothing if "sourcemaps" was not set. default: true required: false + debug_files: + description: |- + Space-separated list of paths to debug information files (e.g. dSYM, PDB, + ELF, Breakpad). Uploaded with "sentry-cli debug-files upload". + Omit to skip uploading debug files. + required: false dist: description: |- Unique identifier for the distribution, used to further segment your @@ -148,6 +154,7 @@ runs: INPUT_ENVIRONMENT: ${{ inputs.environment }} INPUT_INJECT: ${{ inputs.inject }} INPUT_SOURCEMAPS: ${{ inputs.sourcemaps }} + INPUT_DEBUG_FILES: ${{ inputs.debug_files }} INPUT_DIST: ${{ inputs.dist }} INPUT_FINALIZE: ${{ inputs.finalize }} INPUT_IGNORE_MISSING: ${{ inputs.ignore_missing }} @@ -167,7 +174,7 @@ runs: INPUT_WORKING_DIRECTORY: ${{ inputs.working_directory }} INPUT_DISABLE_TELEMETRY: ${{ inputs.disable_telemetry }} INPUT_DISABLE_SAFE_DIRECTORY: ${{ inputs.disable_safe_directory }} - uses: docker://ghcr.io/getsentry/action-release-image:ab-migrate-to-vitest + uses: docker://ghcr.io/getsentry/action-release-image:ab-upload-debug-files # For actions running on macos or windows runners, we use a composite # action approach which allows us to install the arch specific sentry-cli @@ -214,6 +221,7 @@ runs: INPUT_ENVIRONMENT: ${{ inputs.environment }} INPUT_INJECT: ${{ inputs.inject }} INPUT_SOURCEMAPS: ${{ inputs.sourcemaps }} + INPUT_DEBUG_FILES: ${{ inputs.debug_files }} INPUT_DIST: ${{ inputs.dist }} INPUT_FINALIZE: ${{ inputs.finalize }} INPUT_IGNORE_MISSING: ${{ inputs.ignore_missing }} diff --git a/dist/index.js b/dist/index.js index 9135bccc..f73f5445 100644 --- a/dist/index.js +++ b/dist/index.js @@ -99836,6 +99836,7 @@ const telemetry_1 = __nccwpck_require__(12417); const environment = options.getEnvironment(); const inject = options.getBooleanOption('inject', true); const sourcemaps = options.getSourcemaps(); + const debugFiles = options.getDebugFiles(); const dist = options.getDist(); const shouldFinalize = options.getBooleanOption('finalize', true); const ignoreMissing = options.getBooleanOption('ignore_missing', false); @@ -99904,6 +99905,16 @@ const telemetry_1 = __nccwpck_require__(12417); Sentry.setTag('sourcemaps-uploaded', true); })); } + Sentry.setTag('debug-files', debugFiles.length > 0); + if (debugFiles.length) { + yield (0, telemetry_1.traceStep)('upload-debug-files', () => __awaiter(void 0, void 0, void 0, function* () { + core.debug(`Uploading debug files`); + // Debug files belong to a project, not a release, so upload them for each project + for (const project of projects) { + yield (0, cli_1.getCLI)().execute(['debug-files', 'upload', '--project', project, ...debugFiles], true); + } + })); + } if (environment) { yield (0, telemetry_1.traceStep)('add-environment', () => __awaiter(void 0, void 0, void 0, function* () { core.debug(`Adding deploy to release`); @@ -99976,7 +99987,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.getWorkingDirectory = exports.getUrlPrefixOption = exports.getProjects = exports.checkEnvironmentVariables = exports.getSetCommitsManualOptions = exports.getSetCommitsOption = exports.getBooleanOption = exports.getDist = exports.getSourcemaps = exports.getStartedAt = exports.getEnvironment = exports.getRelease = void 0; +exports.getWorkingDirectory = exports.getUrlPrefixOption = exports.getProjects = exports.checkEnvironmentVariables = exports.getSetCommitsManualOptions = exports.getSetCommitsOption = exports.getBooleanOption = exports.getDist = exports.getDebugFiles = exports.getSourcemaps = exports.getStartedAt = exports.getEnvironment = exports.getRelease = void 0; const core = __importStar(__nccwpck_require__(42186)); const path_1 = __importDefault(__nccwpck_require__(71017)); const cli_1 = __nccwpck_require__(56733); @@ -100061,6 +100072,18 @@ const getSourcemaps = () => { return sourcemapsOption.split(' '); }; exports.getSourcemaps = getSourcemaps; +/** + * Debug files are optional, but there may be several as a space-separated list. + * @returns string[] + */ +const getDebugFiles = () => { + const debugFilesOption = core.getInput('debug_files'); + if (!debugFilesOption) { + return []; + } + return debugFilesOption.split(' '); +}; +exports.getDebugFiles = getDebugFiles; /** * Dist is optional, but should be a string when provided. * @returns string diff --git a/src/main.ts b/src/main.ts index 0bf8ac44..77d15f31 100644 --- a/src/main.ts +++ b/src/main.ts @@ -25,6 +25,7 @@ withTelemetry( const environment = options.getEnvironment(); const inject = options.getBooleanOption('inject', true); const sourcemaps = options.getSourcemaps(); + const debugFiles = options.getDebugFiles(); const dist = options.getDist(); const shouldFinalize = options.getBooleanOption('finalize', true); const ignoreMissing = options.getBooleanOption('ignore_missing', false); @@ -114,6 +115,18 @@ withTelemetry( }); } + Sentry.setTag('debug-files', debugFiles.length > 0); + + if (debugFiles.length) { + await traceStep('upload-debug-files', async () => { + core.debug(`Uploading debug files`); + // Debug files belong to a project, not a release, so upload them for each project + for (const project of projects) { + await getCLI().execute(['debug-files', 'upload', '--project', project, ...debugFiles], true); + } + }); + } + if (environment) { await traceStep('add-environment', async () => { core.debug(`Adding deploy to release`); diff --git a/src/options.ts b/src/options.ts index 77d23f36..77bf839c 100644 --- a/src/options.ts +++ b/src/options.ts @@ -88,6 +88,19 @@ export const getSourcemaps = (): string[] => { return sourcemapsOption.split(' '); }; +/** + * Debug files are optional, but there may be several as a space-separated list. + * @returns string[] + */ +export const getDebugFiles = (): string[] => { + const debugFilesOption: string = core.getInput('debug_files'); + if (!debugFilesOption) { + return []; + } + + return debugFilesOption.split(' '); +}; + /** * Dist is optional, but should be a string when provided. * @returns string From f6a167aa92ef3cc44a631dd3b605d8492bb3896a Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 6 Nov 2026 10:52:12 +0100 Subject: [PATCH 2/5] Add e2e test for debug files upload --- .github/workflows/build.yml | 54 +++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 70ee8de3..c4c224f0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -342,3 +342,57 @@ jobs: repo: getsentry/action-release commit: ${{ github.sha }} previous_commit: ${{ github.sha }} + + test-upload-debug-files: + needs: docker-build + strategy: + matrix: + os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + name: Test debug files upload + permissions: + contents: read + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + fetch-depth: 0 + + # Breakpad symbol files are plain text, so we can create one on every OS + # without a compiler. A random debug ID per run makes sure we find this + # upload and not one from an earlier run. + - name: Create a Breakpad debug file + id: dif + shell: bash + run: | + HEX=$(od -An -N16 -tx1 /dev/urandom | tr -d ' \n' | tr 'a-f' 'A-F') + mkdir -p e2e-debug-files + printf 'MODULE Linux x86_64 %s0 action-release-e2e\nFILE 0 main.c\nFUNC 1000 10 0 main\n1000 10 1 0\n' "$HEX" > e2e-debug-files/action-release-e2e.sym + DEBUG_ID=$(echo "$HEX" | tr 'A-F' 'a-f' | sed -E 's/(.{8})(.{4})(.{4})(.{4})(.{12})/\1-\2-\3-\4-\5/') + echo "debug_id=$DEBUG_ID" >> $GITHUB_OUTPUT + + - name: Create a release and upload debug files + uses: ./ + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_LOG_LEVEL: debug + with: + ignore_missing: true + debug_files: ./e2e-debug-files + + - name: Check that Sentry has the debug file + shell: bash + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + DEBUG_ID: ${{ steps.dif.outputs.debug_id }} + run: | + for i in 1 2 3 4 5; do + COUNT=$(curl -sf -H "Authorization: Bearer $SENTRY_AUTH_TOKEN" \ + "https://sentry.io/api/0/projects/$SENTRY_ORG/$SENTRY_PROJECT/files/dsyms/?query=$DEBUG_ID" | jq length) + if [ "$COUNT" -gt 0 ]; then + echo "SUCCESS: Found debug file $DEBUG_ID" + exit 0 + fi + sleep 5 + done + echo "ERROR: Debug file $DEBUG_ID not found in Sentry" + exit 1 From 88048563e51653ffb936ea70afe2915390c1b79f Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 6 Nov 2026 12:06:12 +0100 Subject: [PATCH 3/5] Fix debug file check on Windows --- .github/workflows/build.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c4c224f0..7d324cdb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -386,9 +386,10 @@ jobs: DEBUG_ID: ${{ steps.dif.outputs.debug_id }} run: | for i in 1 2 3 4 5; do - COUNT=$(curl -sf -H "Authorization: Bearer $SENTRY_AUTH_TOKEN" \ - "https://sentry.io/api/0/projects/$SENTRY_ORG/$SENTRY_PROJECT/files/dsyms/?query=$DEBUG_ID" | jq length) - if [ "$COUNT" -gt 0 ]; then + # Use the jq exit code, not its output: jq on Windows writes CRLF. + if curl -sf -H "Authorization: Bearer $SENTRY_AUTH_TOKEN" \ + "https://sentry.io/api/0/projects/$SENTRY_ORG/$SENTRY_PROJECT/files/dsyms/?query=$DEBUG_ID" \ + | jq -e 'length > 0' > /dev/null; then echo "SUCCESS: Found debug file $DEBUG_ID" exit 0 fi From 5b4be8396d9bc3e05f4368e22f2b2c0e8603d263 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 6 Nov 2026 12:12:21 +0100 Subject: [PATCH 4/5] Match exact debug ID in e2e check --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7d324cdb..15b5f81e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -389,7 +389,7 @@ jobs: # Use the jq exit code, not its output: jq on Windows writes CRLF. if curl -sf -H "Authorization: Bearer $SENTRY_AUTH_TOKEN" \ "https://sentry.io/api/0/projects/$SENTRY_ORG/$SENTRY_PROJECT/files/dsyms/?query=$DEBUG_ID" \ - | jq -e 'length > 0' > /dev/null; then + | jq -e --arg id "$DEBUG_ID" 'any(.[]; .debugId == $id)' > /dev/null; then echo "SUCCESS: Found debug file $DEBUG_ID" exit 0 fi From 8ff1a0ee7619091d318241641e2294bfc7a378e7 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 6 Nov 2026 12:28:24 +0100 Subject: [PATCH 5/5] Ignore empty entries in debug_files --- README.md | 2 +- __tests__/main.test.ts | 4 ++-- action.yml | 2 +- dist/index.js | 12 ++++++------ src/options.ts | 13 ++++++------- 5 files changed, 16 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index eaa9e866..5ad868ea 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ Adding the following to your workflow will create a new Sentry release and tell | `environment` | Set the environment for this release. E.g. "production" or "staging". Omit to skip adding deploy to release. | - | | `sourcemaps` | Space-separated list of paths to JavaScript sourcemaps. Omit to skip uploading sourcemaps. | - | | `inject` | Injects Debug IDs into source files and source maps to ensure proper un-minifcation of your stacktraces. Does nothing if `sourcemaps` was not set. | `true` | -| `debug_files` | Space-separated list of paths to debug information files (e.g. dSYM, PDB, ELF, Breakpad). Omit to skip uploading debug files. | - | +| `debug_files` | Space-separated list of paths to debug information files (e.g. dSYM, PDB, ELF, Breakpad). Paths cannot contain spaces. Omit to skip uploading debug files. | - | | `finalize` | When false, omit marking the release as finalized and released. | `true` | | `ignore_missing` | When the flag is set and the previous release commit was not found in the repository, will create a release with the default commits count instead of failing the command. | `false` | | `ignore_empty` | When the flag is set, command will not fail and just exit silently if no new commits for a given release have been found. | `false` | diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index 36941c06..96affd0e 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -78,8 +78,8 @@ describe('options', () => { expect(getDebugFiles()).toEqual([]); }); - test('should split space-separated paths', () => { - process.env['INPUT_DEBUG_FILES'] = './build/app.pdb ./build/app.dSYM'; + test('should split whitespace-separated paths and drop empty entries', () => { + process.env['INPUT_DEBUG_FILES'] = ' ./build/app.pdb ./build/app.dSYM\n'; expect(getDebugFiles()).toEqual(['./build/app.pdb', './build/app.dSYM']); }); }); diff --git a/action.yml b/action.yml index ac2a8e9f..aa839fb7 100644 --- a/action.yml +++ b/action.yml @@ -24,7 +24,7 @@ inputs: description: |- Space-separated list of paths to debug information files (e.g. dSYM, PDB, ELF, Breakpad). Uploaded with "sentry-cli debug-files upload". - Omit to skip uploading debug files. + Paths cannot contain spaces. Omit to skip uploading debug files. required: false dist: description: |- diff --git a/dist/index.js b/dist/index.js index f73f5445..9245ca88 100644 --- a/dist/index.js +++ b/dist/index.js @@ -100073,15 +100073,15 @@ const getSourcemaps = () => { }; exports.getSourcemaps = getSourcemaps; /** - * Debug files are optional, but there may be several as a space-separated list. + * Debug files are optional, but there may be several as a whitespace-separated list. + * Paths cannot contain spaces. * @returns string[] */ const getDebugFiles = () => { - const debugFilesOption = core.getInput('debug_files'); - if (!debugFilesOption) { - return []; - } - return debugFilesOption.split(' '); + return core + .getInput('debug_files') + .split(/\s+/) + .filter(file => file.length > 0); }; exports.getDebugFiles = getDebugFiles; /** diff --git a/src/options.ts b/src/options.ts index 77bf839c..8ddf88fb 100644 --- a/src/options.ts +++ b/src/options.ts @@ -89,16 +89,15 @@ export const getSourcemaps = (): string[] => { }; /** - * Debug files are optional, but there may be several as a space-separated list. + * Debug files are optional, but there may be several as a whitespace-separated list. + * Paths cannot contain spaces. * @returns string[] */ export const getDebugFiles = (): string[] => { - const debugFilesOption: string = core.getInput('debug_files'); - if (!debugFilesOption) { - return []; - } - - return debugFilesOption.split(' '); + return core + .getInput('debug_files') + .split(/\s+/) + .filter(file => file.length > 0); }; /**