-
Notifications
You must be signed in to change notification settings - Fork 4k
Release Shark Explorer separately from LeakCanary #2926
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
edb1341
Release Shark Explorer separately from LeakCanary
pyricau 6412e14
Give Shark Explorer its own change log
pyricau c598311
Don't restore a Gradle cache in the Shark Explorer release workflow
pyricau 77d55c9
Fail the release when the signed app was not notarized
pyricau 6701535
Name the two signing-service bugs in the release doc
pyricau 23e5776
Say that Apple refused this app, not that the service never notarizes
pyricau b8598e9
Name the package SharkExplorer until the signing service takes a space
pyricau 722fe7f
Record what the retry after the notarization fix showed
pyricau 094bce0
Strip the dylib Apple refuses the packaged app over
pyricau a71f492
Put the JDK modules a packaged build needs in the runtime image
pyricau d14d799
Record that the space fix has merged but not shipped
pyricau b172370
Call the app Shark Explorer
pyricau 42cbb6a
Warn that a locked screen looks like the notarization hang
pyricau File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| # Tells every running Shark Explorer that a release exists, which is a separate act from publishing it. | ||
| # | ||
| # The app reads one file — the `latest.properties` asset of the rolling `shark-explorer-latest` release — | ||
| # and this workflow is the only thing that writes it. So a release can be published, installed, and tried | ||
| # before anybody else is told about it, and a release that turns out to be bad is never announced rather | ||
| # than announced and withdrawn. | ||
| # | ||
| # Run it by hand: `gh workflow run promote-shark-explorer.yml -f version=1.0.0`. | ||
| # | ||
| # Why a file on the release download CDN rather than the GitHub API, which would need no promotion step at | ||
| # all: `releases/latest` answers with the newest release of *either* line, and this repository releases | ||
| # LeakCanary on `v*` tags too, so it is usually the wrong one. The unauthenticated API also allows 60 | ||
| # requests an hour per IP, which a shared corporate egress can exhaust, while a release asset is an | ||
| # ordinary unmetered download. See UpdateCheck.kt. | ||
| name: Promote Shark Explorer | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| inputs: | ||
| version: | ||
| description: 'The released version to start offering, e.g. 1.0.0' | ||
| required: true | ||
| type: string | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| promote: | ||
| runs-on: ubuntu-latest | ||
| if: github.repository == 'square/leakcanary' | ||
| permissions: | ||
| contents: write | ||
| steps: | ||
| - uses: actions/checkout@v7 | ||
|
|
||
| # A release nobody can download is not one to point at, so this is checked before the tag moves. | ||
| - name: Check that the release exists and has its macOS builds | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| VERSION: ${{ inputs.version }} | ||
| run: | | ||
| set -euo pipefail | ||
| assets="$(gh release view "shark-explorer-$VERSION" --json assets --jq '.assets[].name')" | ||
| echo "$assets" | ||
| for arch in arm64 x64; do | ||
| echo "$assets" | grep -qx "Shark-Explorer-$VERSION-macos-$arch.dmg" || { | ||
| echo "::error::shark-explorer-$VERSION has no macOS $arch DMG, so there is nothing to promote." | ||
| exit 1 | ||
| } | ||
| done | ||
|
|
||
| - name: Write the manifest and move the rolling release onto it | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| VERSION: ${{ inputs.version }} | ||
| ROLLING_TAG: shark-explorer-latest | ||
| run: | | ||
| set -euo pipefail | ||
| # Read by shark.explorer.app.parseReleaseManifest. Properties rather than JSON so that the app | ||
| # parses it with java.util.Properties and needs no JSON dependency for this one file. | ||
| cat > latest.properties <<EOF | ||
| # Which Shark Explorer release is currently being offered to running copies of the app. | ||
| # Written by .github/workflows/promote-shark-explorer.yml. Read by the app on startup. | ||
| version=$VERSION | ||
| releaseUrl=https://github.com/${GITHUB_REPOSITORY}/releases/tag/shark-explorer-$VERSION | ||
| EOF | ||
| cat latest.properties | ||
|
|
||
| # One rolling release rather than a tag per promotion. `--clobber` because the asset is replaced | ||
| # every time, and the release is created only the first time this ever runs. | ||
| if ! gh release view "$ROLLING_TAG" >/dev/null 2>&1; then | ||
| gh release create "$ROLLING_TAG" \ | ||
| --title "Shark Explorer update manifest" \ | ||
| --notes "Not a release. Holds the one file running copies of Shark Explorer read to find out whether a newer release exists. Written by promote-shark-explorer.yml." \ | ||
| --prerelease | ||
| fi | ||
| gh release upload "$ROLLING_TAG" latest.properties --clobber | ||
|
|
||
| # The app fetches this exact URL, so fetching it here is the check that promotion worked. The CDN | ||
| # serves the previous asset for a moment after an upload, hence retrying rather than asserting once. | ||
| - name: Check that the app's URL now serves the new version | ||
| env: | ||
| VERSION: ${{ inputs.version }} | ||
| run: | | ||
| set -euo pipefail | ||
| url="https://github.com/${GITHUB_REPOSITORY}/releases/download/shark-explorer-latest/latest.properties" | ||
| for attempt in $(seq 1 10); do | ||
| served="$(curl -fsSL "$url" | sed -n 's/^version=//p' || true)" | ||
| if [[ "$served" == "$VERSION" ]]; then | ||
| echo "$url serves version=$VERSION" | ||
| exit 0 | ||
| fi | ||
| echo "Attempt $attempt: $url serves '${served:-nothing}', waiting" | ||
| sleep 15 | ||
| done | ||
| echo "::error::$url did not serve version=$VERSION" | ||
| exit 1 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,264 @@ | ||
| # Releases Shark Explorer, which is released separately from LeakCanary itself. | ||
| # | ||
| # LeakCanary's libraries go out on `v*` tags through publish-release.yml, to Maven Central. This app goes | ||
| # out on `shark-explorer-*` tags, to a GitHub release, on its own version line and its own schedule. The | ||
| # two share nothing but the repository. See docs/releasing-shark-explorer.md. | ||
| # | ||
| # Publishing a release does NOT tell anyone about it. The in-app update check reads the manifest that | ||
| # promote-shark-explorer.yml writes, which is a separate, deliberate step. | ||
| name: Release Shark Explorer | ||
|
|
||
| on: | ||
| push: | ||
| tags: | ||
| - 'shark-explorer-*' | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| env: | ||
| EXPLORER_MODULE: ':shark:shark-explorer:shark-explorer-app' | ||
|
|
||
| jobs: | ||
| # The tag names the version, gradle.properties holds it, and nothing keeps the two the same. Checked | ||
| # first and on its own so that a mismatch costs one quick job rather than four packaging jobs and a | ||
| # release built out of the wrong version. | ||
| version: | ||
| runs-on: ubuntu-latest | ||
| if: github.repository == 'square/leakcanary' | ||
| outputs: | ||
| version: ${{ steps.version.outputs.version }} | ||
| steps: | ||
| - uses: actions/checkout@v7 | ||
| - name: Check the tag against SHARK_EXPLORER_VERSION | ||
| id: version | ||
| run: | | ||
| set -euo pipefail | ||
| tag_version="${GITHUB_REF_NAME#shark-explorer-}" | ||
| built_version="$(sed -n 's/^SHARK_EXPLORER_VERSION=//p' gradle.properties)" | ||
| if [[ "$tag_version" != "$built_version" ]]; then | ||
| echo "::error::Tag $GITHUB_REF_NAME would build $built_version. Set SHARK_EXPLORER_VERSION=$tag_version, or tag shark-explorer-$built_version." | ||
| exit 1 | ||
| fi | ||
| echo "version=$tag_version" >> "$GITHUB_OUTPUT" | ||
|
|
||
| # macOS is the platform this app is for, and the only one whose artifact is signed. Two jobs rather than | ||
| # one universal binary: jpackage builds a thin binary for the architecture it runs on and has no | ||
| # universal option, so an Apple Silicon DMG and an Intel DMG are two builds on two runners. | ||
| # | ||
| # macos-15-intel is the last x86_64 macOS image GitHub will offer, and it goes away in August 2027. When | ||
| # it does, either this matrix entry goes or Intel users stay on whatever the last release was. | ||
| macos: | ||
| needs: version | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| include: | ||
| - runner: macos-15 | ||
| arch: arm64 | ||
| - runner: macos-15-intel | ||
| arch: x64 | ||
| runs-on: ${{ matrix.runner }} | ||
| permissions: | ||
| contents: read | ||
| # The signing service authenticates the workflow through OIDC rather than through a secret, which is | ||
| # what makes signing from a public repository safe: there is no key here to leak. | ||
| id-token: write | ||
| steps: | ||
| - uses: actions/checkout@v7 | ||
| - uses: actions/setup-java@v5 | ||
| with: | ||
| java-version: 17 | ||
| distribution: 'zulu' | ||
| - uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5 | ||
| with: | ||
| # An Actions cache is writable from any branch of the repository and restorable by a tag build, | ||
| # so on a workflow whose output people download — and, on macOS, download signed as Block — a | ||
| # cache entry is an untrusted input. A release runs a few times a year, so caching buys nothing | ||
| # worth that. https://docs.zizmor.sh/audits/#cache-poisoning | ||
| cache-disabled: true | ||
|
|
||
| # Not packageReleaseDmg, which runs the artifact through R8. Shark's object inspectors read fields | ||
| # by name, so minification is a change that needs testing on its own rather than one to take on | ||
| # along with a first release. | ||
| - name: Build the DMG | ||
| run: ./gradlew ${{ env.EXPLORER_MODULE }}:packageDmg --stacktrace | ||
|
|
||
| - name: Find the unsigned DMG | ||
| id: unsigned | ||
| run: | | ||
| set -euo pipefail | ||
| # Found rather than named: jpackage names it after packageName and the version, and the DMG | ||
| # this uploads is named below instead, after the architecture the runner built it for. | ||
| dmg="$(find shark/shark-explorer/shark-explorer-app/build/compose/binaries/main/dmg -name '*.dmg' -print -quit)" | ||
| test -n "$dmg" | ||
| echo "path=$dmg" >> "$GITHUB_OUTPUT" | ||
|
|
||
| # Signs and notarizes with `Developer ID Application: Block, Inc.` through Block's internal signing | ||
| # service. Ask #mdx-ios to provision OSX_CODESIGN_ROLE and CODESIGN_S3_BUCKET for the repository. | ||
| # | ||
| # It signs the .app inside the DMG and rebuilds the DMG around it, so the app is notarized and the | ||
| # DMG container itself is not signed. Gatekeeper checks the app, which is what has to pass. | ||
| - name: Codesign and notarize | ||
| id: codesign | ||
| uses: block/apple-codesign-action@679535d1ab7c5a7c18e6f9afcba3464512cc3dde # v1.1.0 | ||
| with: | ||
| osx-codesign-role: ${{ secrets.OSX_CODESIGN_ROLE }} | ||
| codesign-s3-bucket: ${{ secrets.CODESIGN_S3_BUCKET }} | ||
| unsigned-artifact-path: ${{ steps.unsigned.outputs.path }} | ||
| entitlements-plist-path: shark/shark-explorer/shark-explorer-app/entitlements.plist | ||
| artifact-name: shark-explorer-${{ needs.version.outputs.version }}-${{ matrix.arch }}-${{ github.run_id }} | ||
|
|
||
| # Says what was actually produced rather than trusting that it was signed. | ||
| # | ||
| # `stapler validate` failing is fatal, because an app the signing service signed but Apple did not | ||
| # notarize does not launch: measured on macOS 26.5, it hangs in dyld with no output and no log file | ||
| # rather than being refused. `stapler` is also the only one of these that fails on it — `codesign` | ||
| # is happy, and `spctl --assess` still answers "accepted" for a build Apple has no notarization | ||
| # record of, because nothing here carries the quarantine attribute that makes Gatekeeper insist on | ||
| # a ticket. Its `source=` line is the tell, though: `Notarized Developer ID` against a plain | ||
| # `Developer ID`, which is why this prints it. So this is the check standing between a green | ||
| # release and a DMG that opens into a hang. | ||
| - name: Verify the signature and the notarization | ||
| env: | ||
| SIGNED_DMG: ${{ steps.codesign.outputs.signed-dmg-path }} | ||
| run: | | ||
| set -euo pipefail | ||
| mount_point="$(hdiutil attach "$SIGNED_DMG" -nobrowse -readonly | grep -o '/Volumes/.*')" | ||
| app="$(find "$mount_point" -maxdepth 1 -name '*.app' -print -quit)" | ||
| codesign --verify --deep --strict --verbose=2 "$app" | ||
| codesign -dvv "$app" 2>&1 | grep -E 'Authority|TeamIdentifier|flags' | ||
| spctl --assess --type execute --verbose=4 "$app" || echo "::warning::Gatekeeper did not accept the app" | ||
| if ! xcrun stapler validate "$app"; then | ||
| echo "::error::No notarization ticket is stapled to the app, so it will not launch. It came back signed, and Apple has no notarization record of it." | ||
| hdiutil detach "$mount_point" | ||
| exit 1 | ||
| fi | ||
| hdiutil detach "$mount_point" | ||
|
|
||
| # Renamed here rather than in the build script: the architecture is a property of the runner that | ||
| # built it rather than of the project, so jpackage has no way to put it in the name. | ||
| - name: Name the DMG after its version and architecture | ||
| id: named | ||
| env: | ||
| SIGNED_DMG: ${{ steps.codesign.outputs.signed-dmg-path }} | ||
| VERSION: ${{ needs.version.outputs.version }} | ||
| run: | | ||
| set -euo pipefail | ||
| named="$RUNNER_TEMP/Shark-Explorer-$VERSION-macos-${{ matrix.arch }}.dmg" | ||
| mv "$SIGNED_DMG" "$named" | ||
| echo "path=$named" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - uses: actions/upload-artifact@v7 | ||
| with: | ||
| name: shark-explorer-macos-${{ matrix.arch }} | ||
| path: ${{ steps.named.outputs.path }} | ||
| if-no-files-found: error | ||
|
|
||
| # Unsigned, deliberately. Windows would need Azure Trusted Signing, which is a separate ask and a | ||
| # separate service from the Apple one above, and a .deb needs no signature at all. Both are built | ||
| # because the build script already targets them and an unsigned build beats no build. | ||
| other-platforms: | ||
| needs: version | ||
| # Windows runners default to pwsh, where `./gradlew` does nothing at all and the step still reports | ||
| # success — so the MSI never got built and the first sign of it was a later step not finding the file. | ||
| # Git bash is on the image, and it runs the same script the Linux runner does. | ||
| defaults: | ||
| run: | ||
| shell: bash | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| include: | ||
| - runner: windows-latest | ||
| task: packageMsi | ||
| artifact: windows-x64 | ||
| extension: msi | ||
| - runner: ubuntu-latest | ||
| task: packageDeb | ||
| artifact: linux-x64 | ||
| extension: deb | ||
| runs-on: ${{ matrix.runner }} | ||
| steps: | ||
| - uses: actions/checkout@v7 | ||
| - uses: actions/setup-java@v5 | ||
| with: | ||
| java-version: 17 | ||
| distribution: 'zulu' | ||
| - uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5 | ||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||
| with: | ||
| # An Actions cache is writable from any branch of the repository and restorable by a tag build, | ||
| # so on a workflow whose output people download — and, on macOS, download signed as Block — a | ||
| # cache entry is an untrusted input. A release runs a few times a year, so caching buys nothing | ||
| # worth that. https://docs.zizmor.sh/audits/#cache-poisoning | ||
| cache-disabled: true | ||
|
|
||
| - name: Build the installer | ||
| run: ./gradlew ${{ env.EXPLORER_MODULE }}:${{ matrix.task }} --stacktrace | ||
|
|
||
| - name: Name it after its version and platform | ||
| id: named | ||
| shell: bash | ||
| env: | ||
| VERSION: ${{ needs.version.outputs.version }} | ||
| run: | | ||
| set -euo pipefail | ||
| built="$(find shark/shark-explorer/shark-explorer-app/build/compose/binaries/main -name "*.${{ matrix.extension }}" -print -quit)" | ||
| test -n "$built" | ||
| named="$RUNNER_TEMP/Shark-Explorer-$VERSION-${{ matrix.artifact }}.${{ matrix.extension }}" | ||
| mv "$built" "$named" | ||
| echo "path=$named" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - uses: actions/upload-artifact@v7 | ||
| with: | ||
| name: shark-explorer-${{ matrix.artifact }} | ||
| path: ${{ steps.named.outputs.path }} | ||
| if-no-files-found: error | ||
|
|
||
| release: | ||
| needs: [ version, macos, other-platforms ] | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: write | ||
| steps: | ||
| - uses: actions/checkout@v7 | ||
| - uses: actions/download-artifact@v7 | ||
| with: | ||
| path: artifacts | ||
| merge-multiple: true | ||
|
|
||
| # A prerelease, and titled so. The version number cannot say "alpha" — every installer format | ||
| # validates it down to three integers with a non-zero major, see gradle.properties — so this is | ||
| # where that gets said instead. | ||
| # The notes are a YAML block scalar rather than a shell heredoc because YAML strips this | ||
| # indentation and a heredoc would keep it, and four spaces of kept indentation is a Markdown code | ||
| # block: the whole release page would render as one. | ||
| - name: Create the GitHub release | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| VERSION: ${{ needs.version.outputs.version }} | ||
| NOTES: | | ||
| Shark Explorer ${{ needs.version.outputs.version }}, an alpha release. | ||
|
|
||
| A desktop app that opens an Android heap dump and shows what is holding its memory, as a | ||
| navigable treemap. Download the build for your platform below, open it, and drag the app to | ||
| Applications. | ||
|
|
||
| | Platform | Download | Signed | | ||
| | --- | --- | --- | | ||
| | macOS, Apple Silicon | `Shark-Explorer-${{ needs.version.outputs.version }}-macos-arm64.dmg` | Yes | | ||
| | macOS, Intel | `Shark-Explorer-${{ needs.version.outputs.version }}-macos-x64.dmg` | Yes | | ||
| | Windows | `Shark-Explorer-${{ needs.version.outputs.version }}-windows-x64.msi` | No | | ||
| | Linux | `Shark-Explorer-${{ needs.version.outputs.version }}-linux-x64.deb` | No | | ||
|
|
||
| The macOS builds are signed and notarized by Block. The Windows and Linux builds are not | ||
| signed, so their installers will warn. | ||
|
|
||
| What changed: https://square.github.io/leakcanary/shark-explorer-changelog/ | ||
| run: | | ||
| set -euo pipefail | ||
| gh release create "$GITHUB_REF_NAME" \ | ||
| --title "Shark Explorer $VERSION (alpha)" \ | ||
| --notes "$NOTES" \ | ||
| --prerelease \ | ||
| artifacts/* | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.