diff --git a/checkout-ssh/action.yml b/checkout-ssh/action.yml index cedc09f..0f1aa79 100644 --- a/checkout-ssh/action.yml +++ b/checkout-ssh/action.yml @@ -10,6 +10,10 @@ inputs: git-submodules: description: "Checkout the project with git submodules" required: false + fetch-depth: + description: "Number of commits to fetch. 0 fetches the full history, which tools like sentry-cli need to see the commits of a release." + required: false + default: '1' runs: using: "composite" steps: @@ -17,6 +21,7 @@ runs: with: lfs: ${{ inputs.git-lfs }} submodules: ${{ inputs.git-submodules }} + fetch-depth: ${{ inputs.fetch-depth }} ssh-key: ${{ inputs.git-submodules != 'false' && inputs.ssh-private-key || '' }} - uses: webfactory/ssh-agent@v0.6.0 with: diff --git a/delete-debug-symbols/action.yml b/delete-debug-symbols/action.yml new file mode 100644 index 0000000..a42e22d --- /dev/null +++ b/delete-debug-symbols/action.yml @@ -0,0 +1,89 @@ +name: 'Delete debug symbols' +description: 'Delete the debug symbols a build stored in QB Spaces, once Sentry has them' +inputs: + access-key: + description: 'Digital Ocean Access Key' + required: true + secret-key: + description: 'Digital Ocean Secret Key' + required: true + space-name: + description: 'Name of the DO Space the symbols were stored in' + required: false + default: 'quickbird-artifacts' + space-region: + description: 'Region of the DO Space' + required: false + default: 'fra1' + build-number: + description: 'Build number the symbols were stored for. Must be the same one the build jobs passed to store-debug-symbols. Falls back to the workflow run id.' + required: false + default: '' + platforms: + description: 'Space separated platform labels to delete. Platforms without stored symbols are skipped.' + required: false + default: 'ios android-apk android-aab' +runs: + using: "composite" + steps: + - name: Delete debug symbols from QB Spaces + shell: bash + env: + SPACES_ACCESS_KEY: ${{ inputs.access-key }} + SPACES_SECRET_KEY: ${{ inputs.secret-key }} + run: | + build_number="${{ inputs.build-number }}" + build_number="${build_number:-$GITHUB_RUN_ID}" + + if [[ ! "$build_number" =~ ^[A-Za-z0-9._-]+$ ]]; then + echo "::error::Invalid build number '$build_number' (allowed: letters, digits, '.', '_', '-')" + exit 1 + fi + + if ! curl --help all 2>/dev/null | grep -q -- '--aws-sigv4'; then + echo "::error::curl on this runner cannot sign S3 requests (needs curl 7.75+), found: $(curl --version | head -1)" + exit 1 + fi + + # Passing the keys as --user would expose them in the process list, which + # matters on shared self-hosted runners. A 0600 config file does not. + credentials="$RUNNER_TEMP/qb-spaces-curl.conf" + (umask 077 && printf 'user = "%s:%s"\n' "$SPACES_ACCESS_KEY" "$SPACES_SECRET_KEY" > "$credentials") + trap 'rm -f "$credentials"' EXIT + + endpoint="https://${{ inputs.space-name }}.${{ inputs.space-region }}.digitaloceanspaces.com" + prefix="${GITHUB_REPOSITORY##*/}/debug-symbols/$build_number" + deleted=0 + requested=0 + + echo "Deleting debug symbols under '$prefix/'" + + for platform in ${{ inputs.platforms }}; do + requested=$((requested + 1)) + archive="debug-symbols-$platform.tar.gz" + + curl_status=0 + status="$(curl --silent --show-error --config "$credentials" \ + --request DELETE \ + --aws-sigv4 "aws:amz:${{ inputs.space-region }}:s3" \ + --output /dev/null --write-out '%{http_code}' \ + "$endpoint/$prefix/$archive")" || curl_status=$? + + if [[ "$curl_status" -ne 0 ]]; then + echo "::warning::Could not reach $endpoint to delete '$archive' (curl exit $curl_status)" + continue + fi + + # S3 deletes are idempotent, so a missing key answers 204 just like a hit. + case "$status" in + 200|204|404) + deleted=$((deleted + 1)) + echo "Deleted $archive" + ;; + *) + echo "::warning::Deleting '$archive' answered HTTP $status - it will stay in the Space" + ;; + esac + done + + echo "Deleted $deleted of $requested key(s)" diff --git a/restore-debug-symbols/action.yml b/restore-debug-symbols/action.yml new file mode 100644 index 0000000..10e73a8 --- /dev/null +++ b/restore-debug-symbols/action.yml @@ -0,0 +1,167 @@ +name: 'Restore debug symbols' +description: 'Download the debug symbols that store-debug-symbols put into QB Spaces for a build and extract them for the Sentry upload' +inputs: + access-key: + description: 'Digital Ocean Access Key' + required: true + secret-key: + description: 'Digital Ocean Secret Key' + required: true + space-name: + description: 'Name of the DO Space the symbols were stored in' + required: false + default: 'quickbird-artifacts' + space-region: + description: 'Region of the DO Space' + required: false + default: 'fra1' + build-number: + description: 'Build number the symbols were stored for. Must be the same one the build jobs passed to store-debug-symbols. Falls back to the workflow run id.' + required: false + default: '' + platforms: + description: 'Space separated platform labels to look for. Platforms without stored symbols are skipped.' + required: false + default: 'ios android-apk android-aab' + destination: + description: 'Directory to extract the symbols into' + required: false + default: 'debug-symbols' + fail-if-empty: + description: 'Fail if no symbols were found for the build' + required: false + default: 'true' +outputs: + symbols-dir: + description: 'Directory the symbols were extracted into (one folder per platform)' + value: ${{ steps.extract.outputs.symbols-dir }} + release: + description: 'The Sentry release name that was stored with the symbols (empty if none)' + value: ${{ steps.extract.outputs.release }} +runs: + using: "composite" + steps: + - name: Download debug symbols from QB Spaces + shell: bash + env: + SPACES_ACCESS_KEY: ${{ inputs.access-key }} + SPACES_SECRET_KEY: ${{ inputs.secret-key }} + run: | + build_number="${{ inputs.build-number }}" + build_number="${build_number:-$GITHUB_RUN_ID}" + + if [[ ! "$build_number" =~ ^[A-Za-z0-9._-]+$ ]]; then + echo "::error::Invalid build number '$build_number' (allowed: letters, digits, '.', '_', '-')" + exit 1 + fi + + # curl signs the S3 requests itself since 7.75. Fail with a clear message + # instead of a confusing 403 when the runner ships something older. + if ! curl --help all 2>/dev/null | grep -q -- '--aws-sigv4'; then + echo "::error::curl on this runner cannot sign S3 requests (needs curl 7.75+), found: $(curl --version | head -1)" + exit 1 + fi + + # Passing the keys as --user would expose them in the process list, which + # matters on shared self-hosted runners. A 0600 config file does not. + credentials="$RUNNER_TEMP/qb-spaces-curl.conf" + (umask 077 && printf 'user = "%s:%s"\n' "$SPACES_ACCESS_KEY" "$SPACES_SECRET_KEY" > "$credentials") + trap 'rm -f "$credentials"' EXIT + + download_dir="$RUNNER_TEMP/qb-debug-symbols-download" + rm -rf "$download_dir" + mkdir -p "$download_dir" + + endpoint="https://${{ inputs.space-name }}.${{ inputs.space-region }}.digitaloceanspaces.com" + prefix="${GITHUB_REPOSITORY##*/}/debug-symbols/$build_number" + downloaded=0 + + echo "Looking for debug symbols under '$prefix/'" + + for platform in ${{ inputs.platforms }}; do + archive="debug-symbols-$platform.tar.gz" + target="$download_dir/$archive" + + curl_status=0 + status="$(curl --silent --show-error --config "$credentials" \ + --aws-sigv4 "aws:amz:${{ inputs.space-region }}:s3" \ + --output "$target" --write-out '%{http_code}' \ + "$endpoint/$prefix/$archive")" || curl_status=$? + + if [[ "$curl_status" -ne 0 ]]; then + echo "::error::Downloading '$archive' failed, could not reach $endpoint (curl exit $curl_status)" + exit 1 + fi + + case "$status" in + 200) + downloaded=$((downloaded + 1)) + echo "Downloaded $archive" + ;; + 404) + rm -f "$target" + echo "No symbols stored for '$platform'" + ;; + *) + echo "::error::Downloading '$archive' failed with HTTP $status" + cat "$target" || true + exit 1 + ;; + esac + done + + echo "Downloaded $downloaded archive(s)" + - name: Extract debug symbols + id: extract + shell: bash + run: | + # The destination is wiped before extracting, so refuse an empty value. + if [[ -z "${{ inputs.destination }}" ]]; then + echo "::error::destination must not be empty" + exit 1 + fi + + download_dir="$RUNNER_TEMP/qb-debug-symbols-download" + symbols_dir="${{ inputs.destination }}" + + rm -rf "$symbols_dir" + mkdir -p "$symbols_dir" + + platforms="" + release="" + + for archive in "$download_dir"/debug-symbols-*.tar.gz; do + [[ -f "$archive" ]] || continue + + name="$(basename "$archive" .tar.gz)" + platform="${name#debug-symbols-}" + target="$symbols_dir/$platform" + + mkdir -p "$target" + tar -xzf "$archive" -C "$target" + platforms="${platforms:+$platforms }$platform" + echo "Restored '$platform' from $(basename "$archive")" + + if [[ -z "$release" && -f "$target/metadata.env" ]]; then + release="$(sed -n 's/^release=//p' "$target/metadata.env" | head -1)" + fi + done + + if [[ -z "$platforms" ]]; then + echo "release=" >> $GITHUB_OUTPUT + echo "symbols-dir=$symbols_dir" >> $GITHUB_OUTPUT + + if [[ "${{ inputs.fail-if-empty }}" == "true" ]]; then + echo "::error::No debug symbols found for this build. Either the build jobs stored none (check their 'Store debug symbols for Sentry' step), or a previous run of this job already uploaded them to Sentry and deleted them." + exit 1 + fi + + echo "::warning::No debug symbols found for this build - nothing to upload" + exit 0 + fi + + echo "release=$release" >> $GITHUB_OUTPUT + echo "symbols-dir=$symbols_dir" >> $GITHUB_OUTPUT + + echo "Restored platforms: $platforms" + echo "Sentry release: ${release:-}" diff --git a/store-debug-symbols/action.yml b/store-debug-symbols/action.yml new file mode 100644 index 0000000..8b9b00b --- /dev/null +++ b/store-debug-symbols/action.yml @@ -0,0 +1,157 @@ +name: 'Store debug symbols' +description: 'Collect debug symbols (dSYMs, ProGuard mapping, Dart symbols) into one archive and upload it to QB Spaces, so a separate job can push them to Sentry' +inputs: + platform: + description: 'Label of the build these symbols belong to (e.g. ios, android-apk, android-aab). Becomes the folder name the Sentry job restores them into.' + required: true + build-number: + description: 'Build number of this build. Groups the symbols of all platforms of one build together, so the Sentry job can pick them up again. Falls back to the workflow run id.' + required: false + default: '' + dsyms-path: + description: 'The path to the dSYMs folder' + required: false + default: '' + proguard-mapping-file-path: + description: 'The path to the proguard mapping file' + required: false + default: '' + dart-symbols-file-path: + description: 'The path to the dart symbols folder' + required: false + default: '' + dart-obfuscation-map-file-path: + description: 'The path to the dart obfuscation map JSON file' + required: false + default: '' + release: + description: 'Sentry release name (e.g. 1.0.0+12345). Stored with the symbols and used by the Sentry job to create and finalize the release.' + required: false + default: '' + access-key: + description: 'Digital Ocean Access Key' + required: true + secret-key: + description: 'Digital Ocean Secret Key' + required: true + space-name: + description: 'Name of the DO Space to store the symbols in' + required: false + default: 'quickbird-artifacts' + space-region: + description: 'Region of the DO Space' + required: false + default: 'fra1' +runs: + using: "composite" + steps: + - name: Collect debug symbols + id: collect + shell: bash + run: | + platform="${{ inputs.platform }}" + build_number="${{ inputs.build-number }}" + build_number="${build_number:-$GITHUB_RUN_ID}" + + if [[ ! "$platform" =~ ^[A-Za-z0-9._-]+$ ]]; then + echo "::error::Invalid platform label '$platform' (allowed: letters, digits, '.', '_', '-')" + exit 1 + fi + + if [[ ! "$build_number" =~ ^[A-Za-z0-9._-]+$ ]]; then + echo "::error::Invalid build number '$build_number' (allowed: letters, digits, '.', '_', '-')" + exit 1 + fi + + staging="$RUNNER_TEMP/qb-debug-symbols-staging/$platform" + rm -rf "$staging" + mkdir -p "$staging" + + requested=0 + collected=0 + + stage_dir() { + local source="$1" target="$2" label="$3" + requested=$((requested + 1)) + if [[ -d "$source" ]] && [[ -n "$(ls -A "$source")" ]]; then + mkdir -p "$staging/$target" + cp -R "$source"/. "$staging/$target"/ + collected=$((collected + 1)) + echo "Collected $label from $source" + else + echo "::warning::No $label found at '$source' - skipping" + fi + } + + stage_file() { + local source="$1" target="$2" label="$3" + requested=$((requested + 1)) + if [[ -f "$source" ]]; then + mkdir -p "$staging/$(dirname "$target")" + cp "$source" "$staging/$target" + collected=$((collected + 1)) + echo "Collected $label from $source" + else + echo "::warning::No $label found at '$source' - skipping" + fi + } + + if [[ -n "${{ inputs.dsyms-path }}" ]]; then + stage_dir "${{ inputs.dsyms-path }}" "dsyms" "iOS dSYMs" + fi + + if [[ -n "${{ inputs.dart-symbols-file-path }}" ]]; then + stage_dir "${{ inputs.dart-symbols-file-path }}" "dart-symbols" "Dart symbols" + fi + + if [[ -n "${{ inputs.dart-obfuscation-map-file-path }}" ]]; then + stage_file "${{ inputs.dart-obfuscation-map-file-path }}" "dart-symbols/obfuscation.map.json" "Dart obfuscation map" + fi + + if [[ -n "${{ inputs.proguard-mapping-file-path }}" ]]; then + stage_file "${{ inputs.proguard-mapping-file-path }}" "proguard/mapping.txt" "ProGuard mapping" + fi + + if [[ "$collected" -eq 0 ]]; then + echo "stored=false" >> $GITHUB_OUTPUT + if [[ "$requested" -gt 0 ]]; then + echo "::error::None of the $requested requested debug symbol paths exist - nothing to store for '$platform'" + exit 1 + fi + echo "::warning::No debug symbol paths given for '$platform' - nothing to store" + exit 0 + fi + + { + echo "platform=$platform" + echo "release=${{ inputs.release }}" + echo "build_number=$build_number" + } > "$staging/metadata.env" + + # The upload action resolves its source relative to the workspace, so keep + # the archive in the workspace and hand out a workspace-relative name. + archive_name="debug-symbols-$platform.tar.gz" + archive="$GITHUB_WORKSPACE/$archive_name" + rm -f "$archive" + tar -C "$staging" -czf "$archive" . + + echo "stored=true" >> $GITHUB_OUTPUT + echo "archive=$archive_name" >> $GITHUB_OUTPUT + echo "key-prefix=${GITHUB_REPOSITORY##*/}/debug-symbols/$build_number" >> $GITHUB_OUTPUT + + echo "Archive: $(du -h "$archive" | cut -f1) at $archive" + - name: Upload debug symbols to QB Spaces + if: ${{ steps.collect.outputs.stored == 'true' }} + uses: BetaHuhn/do-spaces-action@latest + with: + access_key: ${{ inputs.access-key }} + secret_key: ${{ inputs.secret-key }} + space_name: ${{ inputs.space-name }} + space_region: ${{ inputs.space-region }} + source: ${{ steps.collect.outputs.archive }} + out_dir: ${{ steps.collect.outputs.key-prefix }} + permission: private + - name: Remove the archive from the workspace + if: ${{ always() && steps.collect.outputs.stored == 'true' }} + shell: bash + run: rm -f "$GITHUB_WORKSPACE/${{ steps.collect.outputs.archive }}" diff --git a/upload-debug-symbols-to-sentry/action.yml b/upload-debug-symbols-to-sentry/action.yml index cfd8ad3..2c89126 100644 --- a/upload-debug-symbols-to-sentry/action.yml +++ b/upload-debug-symbols-to-sentry/action.yml @@ -14,6 +14,10 @@ inputs: description: 'Organization name' required: false default: '' + symbols-dir: + description: 'Directory holding one folder per platform as created by restore-debug-symbols (/dsyms, /dart-symbols, /proguard/mapping.txt). Alternative to passing the paths below individually.' + required: false + default: '' dsyms-path: description: 'The path to the dsYMs folder' required: false @@ -46,35 +50,118 @@ runs: - name: Sentry CLI Version shell: bash run: sentry-cli --version + - name: Upload Debug Symbols from Directory + if: ${{ inputs.symbols-dir != '' }} + shell: bash + env: + SENTRY_AUTH_TOKEN: ${{ inputs.auth-token }} + run: | + if [ ! -d "${{ inputs.symbols-dir }}" ]; then + echo "::error::symbols-dir '${{ inputs.symbols-dir }}' does not exist" + exit 1 + fi + + uploaded=0 + + for platform_dir in "${{ inputs.symbols-dir }}"/*/; do + platform_dir="${platform_dir%/}" + [ -d "$platform_dir" ] || continue + echo "::group::Sentry upload for $(basename "$platform_dir")" + + if [ -d "$platform_dir/dsyms" ]; then + echo "Uploading dSYMs from $platform_dir/dsyms" + sentry-cli --url ${{ inputs.url || 'https://sentry.io' }} debug-files upload --org ${{ inputs.organization }} --project ${{ inputs.project }} "$platform_dir/dsyms" + uploaded=$((uploaded + 1)) + fi + + if [ -d "$platform_dir/dart-symbols" ]; then + echo "Uploading dart symbols from $platform_dir/dart-symbols" + sentry-cli --url ${{ inputs.url || 'https://sentry.io' }} debug-files upload --org ${{ inputs.organization }} --project ${{ inputs.project }} "$platform_dir/dart-symbols" + uploaded=$((uploaded + 1)) + + for debug_file in "$platform_dir"/dart-symbols/*.symbols; do + if [ -f "$debug_file" ] && [ -f "$platform_dir/dart-symbols/obfuscation.map.json" ]; then + echo "Uploading obfuscation map for $debug_file" + sentry-cli --url ${{ inputs.url || 'https://sentry.io' }} dart-symbol-map upload --org ${{ inputs.organization }} --project ${{ inputs.project }} "$platform_dir/dart-symbols/obfuscation.map.json" "$debug_file" + fi + done + fi + + if [ -f "$platform_dir/proguard/mapping.txt" ]; then + echo "Uploading proguard mapping $platform_dir/proguard/mapping.txt" + sentry-cli --url ${{ inputs.url || 'https://sentry.io' }} upload-proguard --org ${{ inputs.organization }} --project ${{ inputs.project }} "$platform_dir/proguard/mapping.txt" + uploaded=$((uploaded + 1)) + fi + + echo "::endgroup::" + done + + if [ "$uploaded" -eq 0 ]; then + echo "::error::No debug symbols found under '${{ inputs.symbols-dir }}'" + exit 1 + fi - name: Upload Dart Symbols if: ${{ inputs.dart-symbols-file-path != '' }} shell: bash + env: + SENTRY_AUTH_TOKEN: ${{ inputs.auth-token }} run: | - sentry-cli --url ${{inputs.url}} debug-files upload ${{ inputs.dart-symbols-file-path }} --auth-token ${{inputs.auth-token}} --org ${{inputs.organization}} --project ${{inputs.project}} || true + if [ ! -d "${{ inputs.dart-symbols-file-path }}" ]; then + echo "::warning::No dart symbols found at '${{ inputs.dart-symbols-file-path }}' - skipping" + exit 0 + fi + sentry-cli --url ${{ inputs.url || 'https://sentry.io' }} debug-files upload --org ${{ inputs.organization }} --project ${{ inputs.project }} ${{ inputs.dart-symbols-file-path }} - name: Upload Dart Obfuscation Map if: ${{ inputs.dart-obfuscation-map-file-path != '' && inputs.dart-symbols-file-path != '' }} shell: bash + env: + SENTRY_AUTH_TOKEN: ${{ inputs.auth-token }} run: | for debug_file in ${{ inputs.dart-symbols-file-path }}/*.symbols; do - if [ -f "$debug_file" ]; then + if [ -f "$debug_file" ] && [ -f "${{ inputs.dart-obfuscation-map-file-path }}" ]; then echo "Uploading obfuscation map for $debug_file" - sentry-cli --url ${{inputs.url}} dart-symbol-map upload --auth-token ${{inputs.auth-token}} --org ${{inputs.organization}} --project ${{inputs.project}} ${{ inputs.dart-obfuscation-map-file-path }} "$debug_file" || true + sentry-cli --url ${{ inputs.url || 'https://sentry.io' }} dart-symbol-map upload --org ${{ inputs.organization }} --project ${{ inputs.project }} ${{ inputs.dart-obfuscation-map-file-path }} "$debug_file" fi done - name: Upload Debug Symbols (iOS) if: ${{ inputs.dsyms-path != '' }} shell: bash + env: + SENTRY_AUTH_TOKEN: ${{ inputs.auth-token }} run: | - sentry-cli --url ${{inputs.url}} debug-files upload ${{inputs.dsyms-path}} --auth-token ${{inputs.auth-token}} --org ${{inputs.organization}} --project ${{inputs.project}} || true + if [ ! -d "${{ inputs.dsyms-path }}" ]; then + echo "::warning::No dSYMs found at '${{ inputs.dsyms-path }}' - skipping" + exit 0 + fi + sentry-cli --url ${{ inputs.url || 'https://sentry.io' }} debug-files upload --org ${{ inputs.organization }} --project ${{ inputs.project }} ${{ inputs.dsyms-path }} - name: Upload Proguard Mapping (Android) if: ${{ inputs.proguard-mapping-file-path != '' }} shell: bash + env: + SENTRY_AUTH_TOKEN: ${{ inputs.auth-token }} run: | - sentry-cli --url ${{inputs.url}} upload-proguard ${{inputs.proguard-mapping-file-path}} --auth-token ${{inputs.auth-token}} --org ${{inputs.organization}} --project ${{inputs.project}} || true + if [ ! -f "${{ inputs.proguard-mapping-file-path }}" ]; then + echo "::warning::No proguard mapping found at '${{ inputs.proguard-mapping-file-path }}' - skipping" + exit 0 + fi + sentry-cli --url ${{ inputs.url || 'https://sentry.io' }} upload-proguard --org ${{ inputs.organization }} --project ${{ inputs.project }} ${{ inputs.proguard-mapping-file-path }} - name: Create & Finalize Sentry Release if: ${{ inputs.release != '' }} shell: bash + env: + SENTRY_AUTH_TOKEN: ${{ inputs.auth-token }} run: | - sentry-cli --url ${{ inputs.url }} releases --org ${{ inputs.organization }} new "${{ inputs.release }}" --auth-token ${{ inputs.auth-token }} || true - sentry-cli --url ${{ inputs.url }} releases --org ${{ inputs.organization }} set-commits "${{ inputs.release }}" --auto --auth-token ${{ inputs.auth-token }} || true - sentry-cli --url ${{ inputs.url }} releases --org ${{ inputs.organization }} finalize "${{ inputs.release }}" --auth-token ${{ inputs.auth-token }} || true + sentry-cli --url ${{ inputs.url || 'https://sentry.io' }} releases --org ${{ inputs.organization }} new "${{ inputs.release }}" + + # --auto reads the local git tree, so the job has to check out the full + # history (fetch-depth: 0) for the commits to be found. + commits_status=0 + sentry-cli --url ${{ inputs.url || 'https://sentry.io' }} releases --org ${{ inputs.organization }} set-commits "${{ inputs.release }}" --auto || commits_status=$? + + # Finalize either way, so a failed association never leaves a half-created release. + sentry-cli --url ${{ inputs.url || 'https://sentry.io' }} releases --org ${{ inputs.organization }} finalize "${{ inputs.release }}" + + if [ "$commits_status" -ne 0 ]; then + echo "::error::Could not associate commits with release '${{ inputs.release }}'. The checkout needs the full git history (fetch-depth: 0) for sentry-cli to see them." + exit 1 + fi