diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml index c22d46e..ccc27a7 100644 --- a/.github/workflows/bump-version.yml +++ b/.github/workflows/bump-version.yml @@ -4,9 +4,9 @@ on: workflow_dispatch: inputs: bump: - description: "Release type: fix (0.1.0 -> 0.1.1), feature (-> 0.2.0), major (-> 1.0.0), or auto from CHANGELOG.md" + description: "Release type: fix (1.1.0 -> 1.1.1), feature (-> 1.2.0), major (-> 2.0.0), alpha / beta / rc (-> 1.2.0-alpha01 ...), stable (1.2.0-rc01 -> 1.2.0), or auto from CHANGELOG.md" type: choice - options: [auto, fix, feature, major] + options: [auto, fix, feature, major, alpha, beta, rc, stable] default: auto permissions: diff --git a/.github/workflows/publish-maven-central.yml b/.github/workflows/publish-maven-central.yml new file mode 100644 index 0000000..247a727 --- /dev/null +++ b/.github/workflows/publish-maven-central.yml @@ -0,0 +1,56 @@ +name: Publish to Maven Central + +# Publishes an already released version that isn't on Maven Central yet, from a branch made from its tag +# with the Maven Central setup added (e.g. central/v1.0.0). New versions are published by the Release workflow. +on: + workflow_dispatch: + inputs: + ref: + description: "Branch to publish, e.g. central/v1.0.0" + type: string + required: true + +permissions: + contents: read + +jobs: + publish: + name: Publish + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + ref: ${{ inputs.ref }} + fetch-depth: 0 + persist-credentials: false + + - name: Check version + run: | + version=$(grep '^VERSION_NAME=' gradle.properties | cut -d= -f2) + if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta|rc)[0-9]{2})?$ ]]; then + echo "::error::VERSION_NAME=${version} isn't X.Y.Z or X.Y.Z-(alpha|beta|rc)NN" + exit 1 + fi + # Only versions that were already released and tagged; the Release workflow handles new ones. + if ! git rev-parse -q --verify "refs/tags/v${version}" >/dev/null; then + echo "::error::v${version} isn't tagged; release new versions with Bump version instead" + exit 1 + fi + echo "Publishing ${version} from ${GITHUB_REF_NAME:-$(git rev-parse --short HEAD)}" + + - uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad + with: + distribution: temurin + java-version: 17 + - uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 + + - name: Build and test + run: ./gradlew assembleRelease testDebugUnitTest + + - name: Publish to Maven Central + env: + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }} + run: ./gradlew publishAndReleaseToMavenCentral --no-configuration-cache diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bc46498..f45e871 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,7 +26,13 @@ jobs: id: version run: | version=$(grep '^VERSION_NAME=' gradle.properties | cut -d= -f2) + # AndroidX-style versions only: 1.2.0, or 1.2.0-alpha01 / -beta01 / -rc01 for pre-releases. + if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta|rc)[0-9]{2})?$ ]]; then + echo "::error::VERSION_NAME=${version} isn't X.Y.Z or X.Y.Z-(alpha|beta|rc)NN" + exit 1 + fi echo "version=${version}" >> "$GITHUB_OUTPUT" + if [[ "$version" == *-* ]]; then echo "prerelease=true" >> "$GITHUB_OUTPUT"; else echo "prerelease=false" >> "$GITHUB_OUTPUT"; fi if git rev-parse -q --verify "refs/tags/v${version}" >/dev/null; then echo "exists=true" >> "$GITHUB_OUTPUT" echo "v${version} is already released" @@ -58,13 +64,26 @@ jobs: ' CHANGELOG.md > notes.md if [ ! -s notes.md ]; then echo "See CHANGELOG.md" > notes.md; fi + # Published before tagging, so a failed upload can be retried without a dangling tag. + - name: Publish to Maven Central + if: steps.version.outputs.exists == 'false' + env: + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }} + run: ./gradlew publishAndReleaseToMavenCentral --no-configuration-cache + - name: Create tag and GitHub release if: steps.version.outputs.exists == 'false' env: GH_TOKEN: ${{ github.token }} VERSION: ${{ steps.version.outputs.version }} + PRERELEASE: ${{ steps.version.outputs.prerelease }} run: | - gh release create "v${VERSION}" --target "${GITHUB_SHA}" --title "v${VERSION}" --notes-file notes.md + flags=() + if [ "$PRERELEASE" = "true" ]; then flags+=(--prerelease); fi + gh release create "v${VERSION}" --target "${GITHUB_SHA}" --title "v${VERSION}" --notes-file notes.md "${flags[@]}" - name: Close milestone if: steps.version.outputs.exists == 'false' @@ -72,15 +91,10 @@ jobs: GH_TOKEN: ${{ github.token }} VERSION: ${{ steps.version.outputs.version }} run: | - number=$(gh api "repos/${GITHUB_REPOSITORY}/milestones?state=open" --jq '.[] | select(.title == "Next release") | .number') + # Close the vX.Y.Z milestone, or rename and close a "Next release" one. Pre-releases leave it open. + if [[ "$VERSION" == *-* ]]; then exit 0; fi + number=$(gh api "repos/${GITHUB_REPOSITORY}/milestones?state=open" \ + --jq "[.[] | select(.title == \"v${VERSION}\" or .title == \"Next release\")][0].number // empty") if [ -n "$number" ]; then gh api -X PATCH "repos/${GITHUB_REPOSITORY}/milestones/${number}" -f title="v${VERSION}" -f state=closed fi - - - name: Trigger JitPack build - if: steps.version.outputs.exists == 'false' - env: - VERSION: ${{ steps.version.outputs.version }} - run: | - curl -s -o /dev/null -w "JitPack responded %{http_code}\n" --max-time 600 \ - "https://jitpack.io/com/github/${GITHUB_REPOSITORY_OWNER}/${GITHUB_REPOSITORY#*/}/v${VERSION}/" || true diff --git a/CHANGELOG.md b/CHANGELOG.md index e522d4b..d508f30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ Add entries under **Unreleased** in each pull request. The *Bump version* workfl ## [Unreleased] ### Added -- MIT license, code of conduct, and a demo GIF in the README showing each feature's effect in the sample app. +- MIT license and code of conduct. - `NetworkLogger.maxTotalBodyChars` (default 12M characters) caps the bodies kept across all calls; the oldest bodies are dropped first, their calls stay listed. - `AppActions.clearCacheInBackground()` clears the cache off the main thread. - `TextInputItem`, `NumberInputItem` and `JsonInputItem` take an optional `subtitle`. @@ -23,7 +23,9 @@ Add entries under **Unreleased** in each pull request. The *Bump version* workfl ### Changed - The sample's fake update feed offers 1.1.0 now that the sample is 1.0.0. -- README install lines use a `LATEST_VERSION` placeholder that matches the JitPack badge, plus a version catalog example. +- **Published to Maven Central** as `io.github.appshipkit:devdeck` (and `devdeck-okhttp`, `devdeck-timber`, `devdeck-appdistribution`) instead of JitPack. Change your dependency lines from `com.github.appshipkit.devdeck::v1.0.0` to `io.github.appshipkit::`; the JitPack repository is no longer needed. The library's own code and packages are unchanged. +- Versions follow AndroidX style: stable `1.2.0`, pre-releases `1.2.0-alpha01`, `-beta01`, `-rc01`. +- The README is reorganised with a table of contents, a quick start and shorter feature sections, and the demo GIF is gone. - Drawer pages slide in and out instead of switching instantly. - The network inspector's clear button sits in the count row, like the other lists. - The device & app snapshot shows share, save and copy as small icons on the card; they are not part of the shared image. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3a023ee..bbcaf9a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,7 +37,7 @@ Review `.api` diffs like code: every line is a promise to library users. ## Versioning and releases -DevDeck uses [Semantic Versioning](https://semver.org/). The version lives in one place: `VERSION_NAME` in `gradle.properties`. +DevDeck uses [Semantic Versioning](https://semver.org/) with AndroidX-style pre-releases: `1.2.0` for stable releases, and `1.2.0-alpha01`, `1.2.0-beta01`, `1.2.0-rc01` before that (two-digit counters, alpha → beta → rc → stable). The version lives in one place: `VERSION_NAME` in `gradle.properties`, and the release workflow rejects any other format. 1. Run the **Bump version** workflow (Actions tab → *Bump version*) and pick the release type: @@ -46,16 +46,22 @@ DevDeck uses [Semantic Versioning](https://semver.org/). The version lives in on | `fix` | 1.0.0 → **1.0.1** | Bug fixes only | | `feature` | 1.0.0 → **1.1.0** | New features or behaviour changes | | `major` | 0.x → **1.0.0**, then 1.x → **2.0.0** | The first stable release, and later breaking changes: removed or renamed public API, changed defaults | + | `alpha` | 1.1.0 → **1.2.0-alpha01**, then → **1.2.0-alpha02** | Early previews; APIs may still change | + | `beta` | 1.2.0-alpha03 → **1.2.0-beta01** | Feature complete; fixes only from here | + | `rc` | 1.2.0-beta02 → **1.2.0-rc01** | Release candidate; ships unchanged unless a blocker turns up | + | `stable` | 1.2.0-rc01 → **1.2.0** | Drops the pre-release suffix | | `auto` (default) | — | Picks one from the `[Unreleased]` entries in `CHANGELOG.md`: `### Removed` or the word `BREAKING` → major; `### Added` / `### Changed` → feature; only `### Fixed` → fix. While on 0.x, breaking changes stay a feature release, so 1.0.0 only happens when you choose `major` | It updates `VERSION_NAME`, moves the *Unreleased* changelog entries under the new version, and opens a **Release vX.Y.Z** pull request. 2. Merge that pull request. The **Release** workflow then: - builds and tests, - - creates the `vX.Y.Z` tag and a GitHub release with the changelog notes, - - renames the **Next release** milestone to `vX.Y.Z` and closes it, - - asks JitPack to build the tag. + - signs the four libraries and publishes them to Maven Central as `io.github.appshipkit::` (they appear on [central.sonatype.com](https://central.sonatype.com/namespace/io.github.appshipkit) within about 30 minutes), + - creates the `vX.Y.Z` tag and a GitHub release with the changelog notes, marked as a pre-release for alpha, beta and rc versions, + - closes the `vX.Y.Z` milestone (stable releases only). -You can also bump locally with `scripts/bump-version.sh ` and open the PR yourself. +**Publishing an older release:** to put an already tagged version on Maven Central (for example one released before DevDeck moved there), create a branch from its tag, add the Maven Central setup, and run the **Publish to Maven Central** workflow (Actions tab) with that branch. The tag itself stays as it was. + +You can also bump locally with `scripts/bump-version.sh ` and open the PR yourself. `./gradlew publishToMavenLocal` installs the libraries in `~/.m2` to try them in another project. ## One-time repository setup @@ -68,3 +74,8 @@ Do this once after creating the GitHub repository: 2. **Settings → Actions → General → Workflow permissions:** select *Read and write permissions* and tick *Allow GitHub Actions to create and approve pull requests* (needed by *Bump version*). 3. **Optional: `RELEASE_TOKEN` secret.** Pull requests opened with the default Actions token don't trigger CI, so the release PR's required checks won't run. Add a fine-grained personal access token with *Contents* and *Pull requests* write access as a repository secret named `RELEASE_TOKEN`, and *Bump version* will use it. Without it, re-run the checks by pushing an empty commit to the release branch, or merge as an admin. 4. **Labels and milestone:** run the **Set up repository** workflow once (Actions tab). It creates the `bug`, `enhancement`, `dependencies` and `skip-changelog` labels used by the templates, Dependabot and CI, plus the **Next release** milestone. +5. **Maven Central publishing.** The release workflow needs these, set up once by an admin of the appshipkit organization: + 1. Sign in to the [Central Portal](https://central.sonatype.com) with GitHub, then *Namespaces → Add namespace* `io.github.appshipkit`. It shows a verification key; create a public repository with that exact name in the appshipkit organization, press *Verify*, then delete the repository. + 2. *Account → Generate user token*. Save the token's username and password. + 3. Create a signing key: `gpg --full-generate-key` (RSA 4096), publish the public key with `gpg --keyserver keyserver.ubuntu.com --send-keys `, and export the private key with `gpg --armor --export-secret-keys `. + 4. Add four repository secrets (*Settings → Secrets and variables → Actions*): `MAVEN_CENTRAL_USERNAME` and `MAVEN_CENTRAL_PASSWORD` (the token), `SIGNING_KEY` (the exported private key, including the BEGIN/END lines) and `SIGNING_KEY_PASSWORD`. diff --git a/README.md b/README.md index bc3699c..71e2138 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,52 @@ # DevDeck [![CI](https://github.com/appshipkit/devdeck/actions/workflows/ci.yml/badge.svg?branch=main&event=push)](https://github.com/appshipkit/devdeck/actions/workflows/ci.yml?query=branch%3Amain) -[![JitPack](https://jitpack.io/v/appshipkit/devdeck.svg)](https://jitpack.io/#appshipkit/devdeck) +[![Maven Central](https://img.shields.io/maven-central/v/io.github.appshipkit/devdeck)](https://central.sonatype.com/artifact/io.github.appshipkit/devdeck) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) A debug drawer for Android apps, built with Jetpack Compose. Open it with a two-finger swipe, a floating button, a keyboard shortcut or code, and get your team's debug tools in one place: -- switch endpoints and override feature flags -- inspect network calls (REST and GraphQL) -- simulate a slow, flaky or offline network and low memory -- tell testers when a newer test build is available (Firebase App Distribution or your own feed) -- read your own logs, logcat and Timber logs, plus crash reports -- take screenshots and screen recordings, share bug reports and device info -- browse storage, watch frame rate and memory, and more +- **Configure:** switch endpoints and override feature flags. +- **Inspect:** network calls (REST and GraphQL), your logs, logcat, Timber and crashes. +- **Simulate:** a slow, flaky or offline network, and low memory. +- **Capture:** screenshots, long screenshots, screen recordings and bug reports. +- **Ship:** tell testers when a newer test build is available. -Everything is a plugin: pick the ones you need or write your own in a few lines. It works on Compose and classic View screens, and it stays out of release builds when you add it with `debugImplementation` (see [Keep it out of release builds](#keep-it-out-of-release-builds)). +Everything is a plugin: pick the ones you need or write your own in a few lines. DevDeck works on Compose and View screens, and stays out of release builds when added with `debugImplementation`. -

- DevDeck sample app: feature flag, slow and failing network, network inspector, frame rate and keyline overlays, dark mode, breadcrumbs, test build update and permissions -

+## Contents + +- [Quick start](#quick-start) +- [Opening the drawer](#opening-the-drawer) +- [Plugins](#plugins) +- [Features](#features) +- [Extending DevDeck](#extending-devdeck) +- [Reference](#reference) +- [Security](#security) +- [Sample app, contributing and license](#sample-app) --- -## Setup +## Quick start ### 1. Add the dependencies -```kotlin -// settings.gradle.kts -dependencyResolutionManagement { - repositories { maven("https://jitpack.io") } -} +DevDeck is on Maven Central, so `mavenCentral()` (in every new Android project) is all you need. +```kotlin // app/build.gradle.kts dependencies { - debugImplementation("com.github.appshipkit.devdeck:devdeck:LATEST_VERSION") - debugImplementation("com.github.appshipkit.devdeck:devdeck-okhttp:LATEST_VERSION") // optional: network inspector (OkHttp / Apollo) - debugImplementation("com.github.appshipkit.devdeck:devdeck-timber:LATEST_VERSION") // optional: Timber logs - debugImplementation("com.github.appshipkit.devdeck:devdeck-appdistribution:LATEST_VERSION") // optional: Firebase test build updates + debugImplementation("io.github.appshipkit:devdeck:LATEST_VERSION") + debugImplementation("io.github.appshipkit:devdeck-okhttp:LATEST_VERSION") // optional: network inspector (OkHttp, Apollo) + debugImplementation("io.github.appshipkit:devdeck-timber:LATEST_VERSION") // optional: Timber logs + debugImplementation("io.github.appshipkit:devdeck-appdistribution:LATEST_VERSION") // optional: Firebase test build updates } ``` -Replace `LATEST_VERSION` with the version on the [JitPack badge](https://jitpack.io/#appshipkit/devdeck) above, for example `1.0.0`. JitPack accepts the version with or without the `v` of the git tag, so `1.0.0` and `v1.0.0` give the same build. +Replace `LATEST_VERSION` with the version on the Maven Central badge above, for example `1.1.0`. -Using a version catalog: +
+With a version catalog ```toml # gradle/libs.versions.toml @@ -51,9 +54,10 @@ Using a version catalog: devdeck = "LATEST_VERSION" [libraries] -devdeck = { module = "com.github.appshipkit.devdeck:devdeck", version.ref = "devdeck" } -devdeck-okhttp = { module = "com.github.appshipkit.devdeck:devdeck-okhttp", version.ref = "devdeck" } +devdeck = { module = "io.github.appshipkit:devdeck", version.ref = "devdeck" } +devdeck-okhttp = { module = "io.github.appshipkit:devdeck-okhttp", version.ref = "devdeck" } ``` +
### 2. Install it @@ -66,9 +70,9 @@ class App : Application() { } ``` -That's it: every activity now has a drawer with the default plugins. +Every activity now has a drawer with the default plugins. -### 3. (Optional) Capture network calls +### 3. Capture network calls (optional) ```kotlin OkHttpClient.Builder() @@ -76,14 +80,14 @@ OkHttpClient.Builder() .build() ``` -For Timber, add `TimberLogsPlugin()` to your plugins (see [Choosing plugins](#choosing-plugins)); it plants its tree for you. +For Timber, add `TimberLogsPlugin()` to your plugins; it plants its own tree. -### Keep it out of release builds +### 4. Keep it out of release builds > [!WARNING] -> DevDeck doesn't check the build type at runtime. Added with `implementation`, it ships in your release build, with its network inspector, logs, file browser and app controls. Add it with `debugImplementation` (or `qaImplementation` and the like for internal build types), never to the build you publish. +> DevDeck doesn't check the build type at runtime. Added with `implementation`, it ships in your release build with its network inspector, logs, file browser and app controls. Use `debugImplementation` (or `qaImplementation` for internal build types), never the configuration of the build you publish. -With `debugImplementation`, release code can't reference DevDeck. Put all DevDeck code in the **debug** source set and an empty version in the **release** source set: +With `debugImplementation`, release code can't reference DevDeck. Keep DevDeck code in the **debug** source set and an empty version in **release**: ```kotlin // src/debug/kotlin/com/myapp/DebugTools.kt @@ -110,19 +114,21 @@ OkHttpClient.Builder().apply { DebugTools.interceptors().forEach(::addIntercepto | How | Default | Change it | |---|---|---| | Two-finger swipe toward the drawer, anywhere on screen | On | `gestures = DeckGestures(twoFingerSwipe = false)` | -| **Ctrl + Shift + D** (emulator or hardware keyboard) | On | `DeckGestures(keyboardShortcut = DeckShortcut(keyCode = KeyEvent.KEYCODE_F12, ctrl = false, shift = false))`, or `null` to turn off | -| Floating bug button (draggable; it stays where you leave it) | On | `trigger = DeckTrigger.None` | +| **Ctrl + Shift + D** (emulator or hardware keyboard) | On | `DeckGestures(keyboardShortcut = DeckShortcut(...))`, or `null` to turn off | +| Floating bug button (drag it anywhere; it stays there) | On | `trigger = DeckTrigger.None` | | Code | — | `DevDeck.open()`, `DevDeck.close()`, `DevDeck.toggle()` | -- DevDeck never covers your controls. The swipe is detected by watching touches without taking them, so taps, scrolls and system back keep working. -- **On an emulator,** a mouse can't do a two-finger swipe, so use Ctrl + Shift + D or the floating button. +- The swipe is detected without taking touches, so taps, scrolls and system back keep working. +- On an emulator, a mouse can't do a two-finger swipe: use Ctrl + Shift + D or the floating button. - If your app uses two-finger horizontal pans (maps, canvases), turn the swipe off. +The drawer header shows the app version, plus chips for a new test build, an active recording (with a *Stop* button) and any network or memory simulation. + --- -## Choosing plugins +## Plugins -`DevDeck.install(this)` uses `DeckPlugins.defaults()`. To choose your own, pass a list; plugins render top to bottom, grouped by section. Each section is a card with its emoji as a badge; tap a card's header to expand or collapse it. Sections start collapsed, so the drawer opens as a short list. The header shows the app version and any active network or memory simulation. The defaults follow a test session: test conditions first, then logs, everyday actions and tools, with reference info at the bottom. +`DevDeck.install(this)` uses `DeckPlugins.defaults()`. To choose your own, pass a list. Plugins render top to bottom, grouped into collapsible section cards. ```kotlin val flags = SharedPreferencesFlagStore(this) @@ -130,7 +136,7 @@ val flags = SharedPreferencesFlagStore(this) DevDeck.install( application = this, plugins = listOf( - EndpointPlugin(listOf(Endpoint("Production", PROD_URL), Endpoint("Staging", STAGING_URL)), restartOnChange = true), + EndpointPlugin(listOf(Endpoint("Production", PROD_URL), Endpoint("Staging", STAGING_URL))), FeatureFlagsPlugin(Flags.all, flags), NetworkSimulatorPlugin(), NetworkLogsPlugin(), @@ -145,36 +151,30 @@ DevDeck.install( ) ``` -| Plugin | Section | What you get | In defaults | +| Plugin | Section | What you get | Default | |---|---|---|---| -| `EndpointPlugin` | Configuration | Endpoint picker | — needs endpoints | -| `FeatureFlagsPlugin` | Configuration | Toggle, text, number and JSON flags with reset and import/export | — needs flags | +| `EndpointPlugin` | Configuration | Endpoint picker, with *Restart app* after a switch | needs endpoints | +| `FeatureFlagsPlugin` | Configuration | Toggle, text, number and JSON flags; restart buttons; reset all, import and export | needs flags | | `NetworkSimulatorPlugin` | Simulate | Slow or failing network for OkHttp calls | ✓ | | `MemorySimulatorPlugin` | Simulate | Memory warnings, heap pressure, kill in background | ✓ | | `NetworkLogsPlugin` | Logs | Network inspector | ✓ | | `LogcatPlugin` | Logs | Live logcat for your app's process | ✓ | -| `TimberLogsPlugin` | Logs | Every Timber call (module `devdeck-timber`) | — | -| `LogsPlugin` | Logs | Your own `DeckLogger.log(...)` entries, one row per tag; optional lifecycle logs | ✓ | -| `BreadcrumbsPlugin` | Logs | Timeline of screens, taps, requests and app events; saved with crashes and bug reports | ✓ | +| `TimberLogsPlugin` | Logs | Every Timber call (`devdeck-timber`) | — | +| `LogsPlugin` | Logs | Your `DeckLogger.log(...)` entries, one row per tag | ✓ | +| `BreadcrumbsPlugin` | Logs | Screens, taps, requests and app events | ✓ | | `CrashPlugin` | Logs | Saved crashes with stack traces | ✓ | -| `AppActionsPlugin` | App | Deep-link launcher, WebView debugging, restart, clear cache/data, kill | ✓ | -| `UiToolsPlugin` | User Interface | Slow animations, keyline overlay, screenshot, screen recording, gallery | ✓ | -| `AppearancePlugin` | User Interface | Dark mode (Android 12+) and app language (Android 13+) override | ✓ | -| `PerformancePlugin` | System | FPS / jank / memory overlay and memory details | ✓ | -| `StoragePlugin` | System | SharedPreferences browser/editor and storage usage | ✓ | -| `DevicePlugin` | System | Permissions (request, revoke, open settings), developer options | ✓ | -| `UpdatesPlugin` | App | New test build check, release notes and install (module `devdeck-appdistribution` for Firebase) | — needs a source | -| `InfoPlugin` | Info | App info (version, build type, R8, SDK levels), device info, device & app snapshot image, bug report, version | ✓ | - -Every plugin takes a `section` (or `null` for no header) and switches to hide individual rows, e.g. `UiToolsPlugin(screenCapture = false)`. +| `AppActionsPlugin` | App | Deep links, WebView debugging, restart, clear cache or data, kill | ✓ | +| `UpdatesPlugin` | App | New test builds, release notes and install (`devdeck-appdistribution` for Firebase) | needs a source | +| `UiToolsPlugin` | User Interface | Slow animations, keylines, screenshots, long screenshots, recording, gallery | ✓ | +| `AppearancePlugin` | User Interface | Dark mode and app language overrides, system language and display settings | ✓ | +| `PerformancePlugin` | System | Frame rate, jank and memory overlay, memory details | ✓ | +| `StoragePlugin` | System | SharedPreferences browser and editor, storage usage | ✓ | +| `DevicePlugin` | System | Permissions, developer options | ✓ | +| `InfoPlugin` | Info | App info, device info, shareable snapshot, bug report, version | ✓ | -**Only one Compose screen?** Wrap it instead of installing app-wide: - -```kotlin -DevDeckHost(plugins = myPlugins) { AppContent() } -``` +Every plugin takes a `section` (or `null` for no header) and switches to hide rows, e.g. `UiToolsPlugin(screenCapture = false)`. -You can use both: a screen with `DevDeckHost` takes over its activity, and the app-wide drawer steps aside there. +**Only one Compose screen?** Wrap it instead of installing app-wide: `DevDeckHost(plugins = myPlugins) { AppContent() }`. You can use both; the app-wide drawer steps aside on screens with `DevDeckHost`. --- @@ -196,19 +196,16 @@ object Flags { val enabled: Boolean = flags[Flags.newCheckout] ``` -- Need two pickers (say API and CDN)? Give each a `key`: `EndpointPlugin(cdnEndpoints, title = "CDN", key = "cdn")`. -- Flag `section` becomes a header on the flags page and `category` a sub-page. -- JSON flags get a highlighted preview and a full-page editor with Format, Minify and validation. -- To use your own flag system (LaunchDarkly, Firebase, an in-house repository), implement `FeatureFlagStore`: `get`, `set`, `isOverridden`, `reset`. +- **Endpoints:** after a switch, *Restart app* applies it (or pass `restartOnChange = true`). For two pickers, give each a `key`: `EndpointPlugin(cdnEndpoints, title = "CDN", key = "cdn")`. +- **Flags:** `section` becomes a header and `category` a sub-page. JSON flags get a preview and a full-page editor. *Restart screen* and *Restart app* apply flags your app reads once. +- **Your own flag system** (LaunchDarkly, Firebase, in-house): implement `FeatureFlagStore` (`get`, `set`, `isOverridden`, `reset`). ### Network inspector -- *Timeline* or *By endpoint* view: calls are grouped by GraphQL operation, or by method + path, with call counts, failures and timings. -- Each call shows its overview, query params, headers and pretty-printed bodies, with Copy URL, Copy cURL and Share. -- GraphQL operation names are detected automatically. `Authorization`, cookie, API-key and auth/CSRF token headers are redacted, and so are passwords, tokens, secrets and API keys in URLs and JSON or form bodies (`NetworkLogger.redactedHeaders` / `redactedFields`). Field names match regardless of case, `_` or `-`, so `access_token`, `accessToken` and `access-token` are all hidden. -- Streaming responses (server-sent events, chunked plain text) and bodies without a content type are passed through without being captured. -- Memory stays bounded: the last `NetworkLogger.maxCalls` calls (300), up to `maxBodyChars` per body and `maxTotalBodyChars` across all bodies. The oldest bodies are dropped first; their calls stay listed. -- Highlight what matters: +- **Views:** *Timeline*, or *By endpoint* grouped by GraphQL operation or method and path, with counts, failures and timings. +- **Details:** overview, query params, headers and pretty-printed bodies, with Copy URL, Copy cURL and Share. +- **Redaction:** auth, cookie, API-key and CSRF headers, and passwords, tokens, secrets and API keys in URLs and bodies (`NetworkLogger.redactedHeaders` / `redactedFields`). Field names match regardless of case, `_` or `-`. +- **Limits:** the last 300 calls (`maxCalls`), with per-body and total body caps (`maxBodyChars`, `maxTotalBodyChars`). Streaming and untyped bodies pass through uncaptured. ```kotlin NetworkLogsPlugin(highlights = listOf( @@ -218,106 +215,62 @@ NetworkLogsPlugin(highlights = listOf( )) ``` -Not using OkHttp? Report calls yourself with `NetworkLogger.start(...)` and `complete(...)` / `fail(...)`. +Not using OkHttp? Report calls with `NetworkLogger.start(...)` and `complete(...)` / `fail(...)`. ### Bad network and low memory -**Simulate 🧪 → Network** changes how requests behave, so testers can check loading, error and retry states without touching the device's connection: - -| Condition | What happens | +| Network condition | What happens | |---|---| | Normal | Requests go through untouched | -| Slow | Each request waits an extra delay (default 3 s) and downloads at a limited speed (default 50 KB/s), then succeeds | -| Failing | Requests fail as *No connection* (`UnknownHostException`), *Timeout* (`SocketTimeoutException` after the client's read timeout) or *Server error 503*, without reaching the server | - -- *Failure rate* below 100% makes a flaky network; *Only URLs containing* limits it to matching calls, e.g. `/checkout`. -- Switch back to *Normal* to check that the app recovers. -- The setting survives restarts, and a badge stays on screen while it's on. Affected calls are tagged *Simulated* in the network inspector. -- It works through `DevDeckOkHttpInterceptor` (Apollo/GraphQL included). Add it as an application interceptor with `addInterceptor`, before other interceptors. Turn it off for a client with `DevDeckOkHttpInterceptor(simulateNetwork = false)`, or drive it from code with `NetworkSimulator.simulation = NetworkSimulation(...)`. +| Slow | An extra delay (default 3 s) and limited download speed (default 50 KB/s) | +| Failing | *No connection*, *Timeout* or *Server error 503*, without reaching the server | -The memory rows under **Simulate 🧪** reproduce what the app sees on a low-memory device (an app can't lower the device's real memory): - -- *Send memory warning* calls `onTrimMemory(level)` or `onLowMemory()` on the application, its registered `ComponentCallbacks` and open activities, as Android does. -- *Fill memory* holds extra Java heap (16 MB, 64 MB, or up to 90% of the limit) until you release it. -- *Kill app when backgrounded* ends the process the next time the app goes to the background. Reopen it from recents to test restoring state after process death. +- Set a *Failure rate* below 100% for a flaky network, and *Only URLs containing* to target calls like `/checkout`. +- The setting survives restarts, and a badge stays on screen while it's on. It works through `DevDeckOkHttpInterceptor` (Apollo included), or from code with `NetworkSimulator.simulation = NetworkSimulation(...)`. +- **Memory:** *Send memory warning* triggers `onTrimMemory` / `onLowMemory`, *Fill memory* holds extra heap, and *Kill app when backgrounded* lets you test restoring state after process death. ### Test build updates -`UpdatesPlugin` tells testers when a newer test build is available. It shows a green *New version · v1.4.0 (412)* chip in the drawer header, which jumps to the plugin row, and a dot on the floating button. The plugin row shows the release notes, an *Install latest* button with download progress, a refresh icon to check again, and tester sign-in. - -With Firebase App Distribution: +`UpdatesPlugin` shows a *New version* chip in the header (tap it to jump to the details), a dot on the floating button, release notes and an *Install latest* button. ```kotlin -// app/build.gradle.kts, in the variants testers install (never in the Play Store build) -debugImplementation("com.github.appshipkit.devdeck:devdeck-appdistribution:LATEST_VERSION") +// app/build.gradle.kts, only in the builds testers install +debugImplementation("io.github.appshipkit:devdeck-appdistribution:LATEST_VERSION") debugImplementation("com.google.firebase:firebase-appdistribution:16.0.0-beta21") // Application.onCreate DevDeck.install(this, plugins = DeckPlugins.defaults() + UpdatesPlugin(FirebaseAppDistributionSource(), notify = true)) ``` -- Your app needs Firebase set up (`google-services.json` and the Google Services plugin), and testers must be invited in App Distribution. The first check asks the tester to sign in with their Google account in the browser. -- Checks run on app start and when the drawer opens, at most every 4 hours (`checkIntervalMillis`), plus the refresh icon. `notify = true` also posts one notification per new build (Android 13+ needs the notification permission). -- **Older or specific builds:** an `UpdateSource` that overrides `availableBuilds()` gets a *Builds* dropdown to install any listed version. Firebase's SDK only exposes the latest build, so with Firebase an *All builds* pill opens the tester web page instead. Android can't install an older version over a newer one, so downgrading means uninstalling first, and the dropdown warns about that. -- Not using Firebase? Implement `UpdateSource` (`checkForUpdate()` and `install()`) for GitHub Releases, your own endpoint or any other feed. +- Needs Firebase set up and testers invited in App Distribution; the first check asks the tester to sign in. +- Checks run on app start and when the drawer opens, at most every 4 hours (`checkIntervalMillis`). +- Not using Firebase? Implement `UpdateSource` (`checkForUpdate()`, `install()`), and `availableBuilds()` for a *Builds* picker. -### Logs +### Logs and breadcrumbs | Source | How logs get in | |---|---| | `LogsPlugin` | You call `DeckLogger.log("Order placed", tag = "Analytics", payload = json)` | -| `LogcatPlugin` | Automatic: everything your app process writes to logcat, no permission needed | -| `TimberLogsPlugin` | Automatic: every `Timber` call, with exact tags and levels | +| `LogcatPlugin` | Everything your app's process writes to logcat; no permission needed | +| `TimberLogsPlugin` | Every `Timber` call, with its tag and level | -Logcat and Timber pages have level filters, search, an *All / By tag* view with counts, pause, share and clear. +Log pages have level filters, search, an *All / By tag* view, pause, share and clear. Common platform noise is hidden; adjust it with `LogcatPlugin(ignoredTags = …, ignoredTagPrefixes = …, ignoredMessagePrefixes = …)`. -
-Logcat noise filtering - -Android logs some lines inside your process that aren't about your app. These are hidden by default: +**Breadcrumbs** record what led up to a problem: screens, app events, taps (`Modifier.breadcrumb("Place order")` labels Compose controls), requests, and your own steps (`Breadcrumbs.add(...)`). The last 50 are saved with every crash and the last 100 go into bug reports. -- tags: `perf_hint`, `ashmem`, `EGL_emulation`, `eglCodecCommon`, `HostConnection`, `goldfish-address-space`, `Gralloc4`, `libEGL`, `nativeloader`, `ziparchive`, `ProfileInstaller`, `CompatibilityChangeReporter`, `ImeTracker`, `InsetsController` -- tags starting with `VRI[`, and messages starting with `setRequestedFrameRate` (Android 15+ frame-rate logs from Compose) +### Screen capture, bug reports and info -`Choreographer` and `OpenGLRenderer` stay visible because their warnings point at real jank. Android Studio runs also add a few `hiddenapi` / `Redefining intrinsic method` warnings from Studio's own agent; those are harmless. - -```kotlin -LogcatPlugin( - ignoredTags = LogcatNoise.PLATFORM_TAGS + "MyNoisySdk", - ignoredTagPrefixes = LogcatNoise.PLATFORM_TAG_PREFIXES, - ignoredMessagePrefixes = LogcatNoise.PLATFORM_MESSAGE_PREFIXES, -) -``` - -Pass `emptySet()` to show everything. The filters apply to new lines. -
- -### Breadcrumbs - -*Logs → Breadcrumbs* shows what happened before a problem, newest first: - -- **Screens:** each activity created, started, resumed, paused, stopped and destroyed. -- **App events:** the app moving to the background or foreground, configuration changes and system memory warnings. -- **Taps:** on View screens the tapped button by text or id, e.g. `CheckoutActivity · Button "Place order"`. Compose doesn't expose its views, so Compose taps show the screen and position. Label important Compose controls with `Modifier.breadcrumb("Place order")`. -- **Requests:** every call through the network inspector, e.g. `POST /orders → 500 · 830 ms`. -- **Your own steps:** `Breadcrumbs.add("Payment sheet shown")` or `Breadcrumbs.screen("Checkout")`, e.g. from your navigation listener. - -The last 50 steps are saved with every crash and shown as *Steps before the crash*. Bug reports include the last 100. Filter by type, share the list or clear it from the page. Turn parts off with `BreadcrumbsPlugin(taps = false, network = false)`. - -### Screen capture, bug reports and device info - -- **Screenshot / screen recording / gallery:** under User Interface. Recording asks for Android's screen-capture permission. While it runs, a small floating bar shows the time with pause/resume (Android 7+) and stop, the drawer header shows a *Recording* chip and a red *Stop* button, and the notification has the same controls. The floating bar is drawn in your app's window, so it appears in the video. -- **Screenshots leave DevDeck out:** the floating button, simulation badge and recording bar are hidden while a screenshot is taken. The keyline and frame rate overlays stay, since you turn those on to capture them. -- **Long screenshot (Android 12+):** *Take a long screenshot* captures the screen's main scrolling list or page in one tall image, using the same scroll capture support as Android's *Capture more* (ScrollView, RecyclerView, ListView and Compose scrolling layouts). It starts at what's on screen and goes down, stopping at the end of the content, after 5 screens (or about 15 megapixels), after 8 seconds in total, or when scrolling shows nothing new, so endless feeds stay bounded; they may still load their next page while it scrolls. Screens with nothing to scroll show a message instead. -- **App info and device info:** under Info. App info shows the version, package, build type and flavor (read from your app's `BuildConfig` when it exists and R8 hasn't removed it), whether the build is debuggable, R8 state and SDK levels. -- **Device & app snapshot:** under Info. One image with app version, build type, SDKs, device, Android version, screen and locale, plus your `buildInfo`. Share it, save it, or copy it as text from the icons on the card; the icons are left out of the image. -- **App settings:** the ⚙ button next to the drawer's close button opens the app's system settings page. -- **Bug report:** a description, recent steps, your logs, recent logcat, device info and chosen captures, sent through the share sheet. -- **Permissions:** under System, on their own page. Each permission shows Granted or Denied with what you can do about it. *Request* shows the system dialog, or opens the app's settings once Android stops asking. *Revoke* (Android 13+) removes a runtime permission when the app closes; until then the row shows *Revoked when the app restarts*. The list refreshes when you come back from a permission dialog or settings screen. *Open settings* handles special permissions such as drawing over other apps. Install-time permissions can't change, so they are only listed. To grant or revoke silently, use `adb shell pm grant` / `pm revoke`. +- **Screenshots and recordings:** under User Interface. While recording, a small floating bar shows the time with pause and stop, so the drawer can stay closed. DevDeck's own floating button and badges are left out of screenshots. +- **Long screenshot (Android 12+):** captures the main scrolling list or page in one image, from what's on screen downwards. It stops at the end, after 5 screens or 8 seconds, or when nothing new appears, so endless feeds stay bounded. +- **Bug report:** a description, recent steps, logs, logcat, device info and chosen captures, sent through the share sheet. +- **App info and device info:** version, package, build type and flavor (from your `BuildConfig`), debuggable, R8 state, SDK levels and device details. The *Device & app snapshot* shares them as one image. +- **Permissions:** each permission's state, with *Request*, *Revoke* (Android 13+) or *Open settings*. --- -## Your own plugins +## Extending DevDeck + +### Your own plugins ```kotlin val cachePlugin = deckPlugin(id = "cache", section = "System 📱") { @@ -326,22 +279,18 @@ val cachePlugin = deckPlugin(id = "cache", section = "System 📱") { } ``` -Use the same building blocks as the built-in plugins: - | Building blocks | Use for | |---|---| -| `SwitchItem`, `CheckboxItem`, `DropdownItem`, `SingleSelectItem`, `TextInputItem`, `NumberInputItem`, `JsonInputItem` | Editing values (`CompactToggle`, `DeckCheckbox` and `DeckRadio` are the bare controls) | +| `SwitchItem`, `CheckboxItem`, `DropdownItem`, `SingleSelectItem`, `TextInputItem`, `NumberInputItem`, `JsonInputItem` | Editing values | | `ButtonItem`, `ConfirmButtonItem` | Actions (the confirm version needs a second tap) | -| `PageLinkItem`, `ExpandableItem`, `Section`, `CollapsibleSection`, `SectionDivider`, `DeckCard` | Structure and sub-pages; `DeckCard` gives custom `drawerContent` the same card look | -| `SegmentedPills`, `FilterPill`, `FilterPillRow`, `ActionPill`, `ActionPillRow`, `SearchField` | View switchers, filters and search, as in the log viewers and network inspector | -| `BulletItem`, `InfoGroup`, `KeyValueItem`, `CodeBlock`, `TextItem` | Showing information; wrap long lists in `InfoGroup` to add dividers between rows | - -- Close or navigate the drawer from your plugin with `LocalDeckState.current?.close()` or `?.navigateTo(title) { … }`. -- For setup work, implement `DeckPlugin` and override `onAttach(context)`; it runs once per plugin `id` with the application context. +| `PageLinkItem`, `ExpandableItem`, `Section`, `CollapsibleSection`, `SectionDivider`, `DeckCard` | Structure and sub-pages | +| `SegmentedPills`, `FilterPill`, `FilterPillRow`, `ActionPill`, `ActionPillRow`, `SearchField` | View switchers, filters and search | +| `BulletItem`, `InfoGroup`, `KeyValueItem`, `CodeBlock`, `TextItem` | Showing information | ---- +- Close or navigate from a plugin with `LocalDeckState.current?.close()` or `?.navigateTo(title) { … }`. +- For setup work, implement `DeckPlugin` and override `onAttach(context)`. -## Control and customize +### Customizing ```kotlin DevDeck.install( @@ -356,18 +305,9 @@ DevDeck.isEnabled = false // hide everywhere, e.g. DevDeck.updateConfig { it.copy(plugins = it.plugins + cachePlugin) } ``` -`DeckStyle` also sets the panel width, colors, the panel background (`containerColor`) and the section cards (`cardColor`). Pass `collapsibleSections = false` to show every section open, with plain headers, or `oneSectionAtATime = true` so opening a section closes the others. +`DeckStyle` also sets the panel width and colors; `collapsibleSections = false` shows every section open, and `oneSectionAtATime = true` closes the others when one opens. -### Payment and 3DS screens - -DevDeck skips payment SDK activities automatically, so it never sits on top of card entry or 3DS challenges: Adyen, Primer, Netcetera 3DS, Cardinal Commerce, Stripe, Braintree, Checkout.com and PayPal (`DeckExclusions.PAYMENT_SDKS`). - -```kotlin -DevDeck.install(this, plugins, excludedActivityPrefixes = DeckExclusions.PAYMENT_SDKS + "com.mybank.sdk.") // add your own -DevDeck.install(this, plugins, excludedActivityPrefixes = emptySet()) // turn off -``` - -3DS in the bank's app or a browser runs in another process, so DevDeck is never there. For 3DS inside your own screen, set `DevDeck.isEnabled = false` during that step. +**Payment screens:** DevDeck skips Adyen, Primer, Netcetera 3DS, Cardinal Commerce, Stripe, Braintree, Checkout.com and PayPal activities automatically (`DeckExclusions.PAYMENT_SDKS`). Add your own with `excludedActivityPrefixes`, and set `DevDeck.isEnabled = false` during 3DS inside your own screens. --- @@ -375,31 +315,32 @@ DevDeck.install(this, plugins, excludedActivityPrefixes = emptySet()) | Module | Package | Contents | |---|---|---| -| `devdeck` | `io.devdeck` | Drawer, plugins and building blocks. Depends on Compose, AndroidX Activity and Core. | -| `devdeck-okhttp` | `io.devdeck.okhttp` | `DevDeckOkHttpInterceptor`. OkHttp is `compileOnly`, so your version is used. | -| `devdeck-timber` | `io.devdeck.timber` | `DevDeckTimberTree`, `TimberLogsPlugin`. Timber is `compileOnly` (4.7.1+, works with 5.x). | -| `devdeck-appdistribution` | `io.devdeck.appdistribution` | `FirebaseAppDistributionSource` for `UpdatesPlugin`. The Firebase App Distribution API is `compileOnly`; add the full SDK to your tester builds. | - -- **Requirements:** minSdk 23, compileSdk 36. Built with AGP 9.4, Kotlin 2.2 and Compose BOM 2026.05. If your app uses an older Compose, debug builds resolve to this BOM's versions. -- **Stored data:** DevDeck keeps its own settings in one SharedPreferences file, `devdeck_settings`. Feature flags from `SharedPreferencesFlagStore` go to `devdeck_flags` (or the name you pass). Crash logs live in `files/devdeck/crashes/`, screenshots and recordings in `devdeck/` under the app's external files directory, and exported bug reports and logs in `cache/devdeck-reports/` (the newest 20 are kept). -- **Manifest additions:** `FOREGROUND_SERVICE`, `FOREGROUND_SERVICE_MEDIA_PROJECTION` and `POST_NOTIFICATIONS` for screen recording, a `FileProvider` (`${applicationId}.devdeck.files`) and the recording service. Debug builds only, if you use `debugImplementation`. -- **R8:** each module ships consumer rules, so minified internal builds (say, a `qa` build type with `isMinifyEnabled = true`) need no extra keep rules. OkHttp, Timber and Firebase are `compileOnly`, and the rules stop R8 from failing when your app doesn't include them. -- **Limits:** *Slow animations* affects View animations only, because Compose follows the system animation scale. Logcat shows your app's process only. Some OEM builds hide debug-level logs. +| `devdeck` | `io.devdeck` | Drawer, plugins and building blocks | +| `devdeck-okhttp` | `io.devdeck.okhttp` | `DevDeckOkHttpInterceptor` (OkHttp is `compileOnly`) | +| `devdeck-timber` | `io.devdeck.timber` | `DevDeckTimberTree`, `TimberLogsPlugin` (Timber 4.7.1+ or 5.x, `compileOnly`) | +| `devdeck-appdistribution` | `io.devdeck.appdistribution` | `FirebaseAppDistributionSource` (Firebase App Distribution API is `compileOnly`) | + +- **Requirements:** minSdk 23, compileSdk 36. Built with AGP 9.4, Kotlin 2.2 and Compose BOM 2026.05; debug builds of apps on an older Compose resolve to this BOM's versions. +- **Versions:** `MAJOR.MINOR.PATCH`, with pre-releases as `-alphaNN`, `-betaNN` and `-rcNN` (e.g. `1.2.0-beta01`), like AndroidX. Minor and patch releases don't break your code. +- **Stored data:** settings in the `devdeck_settings` SharedPreferences file, flags in `devdeck_flags`, crash logs in `files/devdeck/crashes/`, captures in `devdeck/` under the external files directory, and exported reports in `cache/devdeck-reports/` (newest 20 kept). +- **Manifest additions:** foreground service and notification permissions for screen recording, a `FileProvider` (`${applicationId}.devdeck.files`) and the recording service. +- **R8:** consumer rules are included, so minified internal builds need no extra rules. +- **Limits:** *Slow animations* affects View animations only. Logcat shows your app's process only. ## Security -- **Keep DevDeck out of store builds.** Add it with `debugImplementation` (or your QA flavours) only. It shows network traffic, logs, preferences and files, and can change app state, which is fine for testers but never for end users. -- **Secrets are masked** in the network inspector, but your own log messages, logcat lines and preference values are shown as they are. Avoid logging secrets. -- **Nothing leaves the device on its own.** Crash logs, captures and settings stay in the app's private storage. Data is only shared when a tester taps *Share* or sends a bug report. +- **Keep DevDeck out of store builds.** It shows network traffic, logs, preferences and files, and can change app state. +- **Secrets are masked** in the network inspector, but your own logs, logcat lines and preference values are shown as they are. +- **Nothing leaves the device on its own.** Data is only shared when a tester taps *Share* or sends a bug report. - **Found a vulnerability?** Report it privately, as described in [SECURITY.md](SECURITY.md). ## Sample app -Run the `sample` configuration in Android Studio. [sample/README.md](sample/README.md) walks through each file and shows what to try. +Run the `sample` configuration in Android Studio. [sample/README.md](sample/README.md) walks through each screen and what to try. ## Contributing -See [CONTRIBUTING.md](CONTRIBUTING.md) for local checks, pull requests and the release process, and [CHANGELOG.md](CHANGELOG.md) for what changed in each version. Please follow the [code of conduct](CODE_OF_CONDUCT.md). +See [CONTRIBUTING.md](CONTRIBUTING.md) for local checks, pull requests and releases, and [CHANGELOG.md](CHANGELOG.md) for what changed in each version. Please follow the [code of conduct](CODE_OF_CONDUCT.md). ## License diff --git a/build.gradle.kts b/build.gradle.kts index d8a53d1..c85d836 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,6 +8,7 @@ plugins { alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.compose) apply false alias(libs.plugins.binary.compatibility.validator) + alias(libs.plugins.maven.publish) apply false } val ktlint: Configuration = configurations.create("ktlint") @@ -102,3 +103,14 @@ subprojects { tasks.named("check") { dependsOn(apiCheck) } } } + +// Maven Central: every published module signs its artifacts when a signing key is available (CI release), and +// coordinates and POM metadata come from gradle.properties (shared) and each module's gradle.properties. +subprojects { + plugins.withId("com.vanniktech.maven.publish") { + extensions.configure { + publishToMavenCentral(automaticRelease = true) + if (providers.gradleProperty("signingInMemoryKey").isPresent) signAllPublications() + } + } +} diff --git a/devdeck-appdistribution/build.gradle.kts b/devdeck-appdistribution/build.gradle.kts index 1e9700f..33b1fad 100644 --- a/devdeck-appdistribution/build.gradle.kts +++ b/devdeck-appdistribution/build.gradle.kts @@ -2,12 +2,9 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.android.library) - `maven-publish` + alias(libs.plugins.maven.publish) } -group = providers.gradleProperty("GROUP").get() -version = providers.gradleProperty("VERSION_NAME").get() - android { namespace = "io.devdeck.appdistribution" compileSdk = 36 @@ -32,12 +29,6 @@ android { testOptions { unitTests.isReturnDefaultValues = true } - - publishing { - singleVariant("release") { - withSourcesJar() - } - } } kotlin { @@ -53,16 +44,3 @@ dependencies { testImplementation(libs.firebase.appdistribution.api) testImplementation(libs.junit) } - -afterEvaluate { - publishing { - publications { - create("release") { - from(components["release"]) - groupId = project.group.toString() - artifactId = "devdeck-appdistribution" - version = project.version.toString() - } - } - } -} diff --git a/devdeck-appdistribution/gradle.properties b/devdeck-appdistribution/gradle.properties new file mode 100644 index 0000000..ea45e2d --- /dev/null +++ b/devdeck-appdistribution/gradle.properties @@ -0,0 +1,3 @@ +POM_ARTIFACT_ID=devdeck-appdistribution +POM_NAME=DevDeck App Distribution +POM_DESCRIPTION=Firebase App Distribution source for DevDeck's test build updates. diff --git a/devdeck-okhttp/build.gradle.kts b/devdeck-okhttp/build.gradle.kts index 350ef50..4e4fe8e 100644 --- a/devdeck-okhttp/build.gradle.kts +++ b/devdeck-okhttp/build.gradle.kts @@ -2,12 +2,9 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.android.library) - `maven-publish` + alias(libs.plugins.maven.publish) } -group = providers.gradleProperty("GROUP").get() -version = providers.gradleProperty("VERSION_NAME").get() - android { namespace = "io.devdeck.okhttp" compileSdk = 36 @@ -32,12 +29,6 @@ android { testOptions { unitTests.isReturnDefaultValues = true } - - publishing { - singleVariant("release") { - withSourcesJar() - } - } } kotlin { @@ -54,16 +45,3 @@ dependencies { testImplementation(libs.mockwebserver) testImplementation(libs.junit) } - -afterEvaluate { - publishing { - publications { - create("release") { - from(components["release"]) - groupId = project.group.toString() - artifactId = "devdeck-okhttp" - version = project.version.toString() - } - } - } -} diff --git a/devdeck-okhttp/gradle.properties b/devdeck-okhttp/gradle.properties new file mode 100644 index 0000000..4d23cfa --- /dev/null +++ b/devdeck-okhttp/gradle.properties @@ -0,0 +1,3 @@ +POM_ARTIFACT_ID=devdeck-okhttp +POM_NAME=DevDeck OkHttp +POM_DESCRIPTION=OkHttp interceptor that feeds DevDeck's network inspector and network simulator. diff --git a/devdeck-timber/build.gradle.kts b/devdeck-timber/build.gradle.kts index 3edfdb9..a1b3b29 100644 --- a/devdeck-timber/build.gradle.kts +++ b/devdeck-timber/build.gradle.kts @@ -3,12 +3,9 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.compose) - `maven-publish` + alias(libs.plugins.maven.publish) } -group = providers.gradleProperty("GROUP").get() -version = providers.gradleProperty("VERSION_NAME").get() - android { namespace = "io.devdeck.timber" compileSdk = 36 @@ -37,12 +34,6 @@ android { testOptions { unitTests.isReturnDefaultValues = true } - - publishing { - singleVariant("release") { - withSourcesJar() - } - } } kotlin { @@ -58,16 +49,3 @@ dependencies { testImplementation(libs.timber) testImplementation(libs.junit) } - -afterEvaluate { - publishing { - publications { - create("release") { - from(components["release"]) - groupId = project.group.toString() - artifactId = "devdeck-timber" - version = project.version.toString() - } - } - } -} diff --git a/devdeck-timber/gradle.properties b/devdeck-timber/gradle.properties new file mode 100644 index 0000000..7ccedbb --- /dev/null +++ b/devdeck-timber/gradle.properties @@ -0,0 +1,3 @@ +POM_ARTIFACT_ID=devdeck-timber +POM_NAME=DevDeck Timber +POM_DESCRIPTION=Timber tree and plugin that show Timber logs in DevDeck. diff --git a/devdeck/build.gradle.kts b/devdeck/build.gradle.kts index b38a896..d734352 100644 --- a/devdeck/build.gradle.kts +++ b/devdeck/build.gradle.kts @@ -3,12 +3,9 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.compose) - `maven-publish` + alias(libs.plugins.maven.publish) } -group = providers.gradleProperty("GROUP").get() -version = providers.gradleProperty("VERSION_NAME").get() - android { namespace = "io.devdeck" compileSdk = 36 @@ -37,12 +34,6 @@ android { buildFeatures { compose = true } - - publishing { - singleVariant("release") { - withSourcesJar() - } - } } kotlin { @@ -63,16 +54,3 @@ dependencies { testImplementation(libs.junit) testImplementation(libs.json) } - -afterEvaluate { - publishing { - publications { - create("release") { - from(components["release"]) - groupId = project.group.toString() - artifactId = "devdeck" - version = project.version.toString() - } - } - } -} diff --git a/devdeck/gradle.properties b/devdeck/gradle.properties new file mode 100644 index 0000000..fbc5c5b --- /dev/null +++ b/devdeck/gradle.properties @@ -0,0 +1,3 @@ +POM_ARTIFACT_ID=devdeck +POM_NAME=DevDeck +POM_DESCRIPTION=A debug drawer for Android apps, built with Jetpack Compose: endpoints, feature flags, network inspector, logs, screen capture and more. diff --git a/docs/devdeck-demo.gif b/docs/devdeck-demo.gif deleted file mode 100644 index 61bc5f6..0000000 Binary files a/docs/devdeck-demo.gif and /dev/null differ diff --git a/gradle.properties b/gradle.properties index 56d3fda..6b78b82 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,5 +9,20 @@ kotlin.code.style=official # Enabled parallel sync for Gradle 9.4+ org.gradle.tooling.parallel=true -GROUP=com.github.appshipkit +# Maven coordinates: io.github.appshipkit::. Versions follow AndroidX style: +# 1.2.0 for stable releases, 1.2.0-alpha01 / -beta01 / -rc01 for pre-releases (see scripts/bump-version.sh). +GROUP=io.github.appshipkit VERSION_NAME=1.0.0 + +# POM metadata shared by every published module (each module sets POM_ARTIFACT_ID, POM_NAME, POM_DESCRIPTION) +POM_INCEPTION_YEAR=2026 +POM_URL=https://github.com/appshipkit/devdeck +POM_LICENSE_NAME=MIT License +POM_LICENSE_URL=https://github.com/appshipkit/devdeck/blob/main/LICENSE +POM_LICENSE_DIST=repo +POM_SCM_URL=https://github.com/appshipkit/devdeck +POM_SCM_CONNECTION=scm:git:git://github.com/appshipkit/devdeck.git +POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/appshipkit/devdeck.git +POM_DEVELOPER_ID=appshipkit +POM_DEVELOPER_NAME=AppShipKit +POM_DEVELOPER_URL=https://github.com/appshipkit diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ce5dca8..90e37d6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,6 +11,7 @@ ktlint = "1.8.0" junit = "4.13.2" json = "20260814" binaryCompatibilityValidator = "0.18.2" +mavenPublish = "0.37.0" asm = "9.11.1" [libraries] @@ -46,3 +47,4 @@ android-library = { id = "com.android.library", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binaryCompatibilityValidator" } +maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" } diff --git a/jitpack.yml b/jitpack.yml deleted file mode 100644 index ef6972f..0000000 --- a/jitpack.yml +++ /dev/null @@ -1,4 +0,0 @@ -jdk: - - openjdk17 -install: - - ./gradlew :devdeck:publishToMavenLocal :devdeck-okhttp:publishToMavenLocal :devdeck-timber:publishToMavenLocal :devdeck-appdistribution:publishToMavenLocal diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh index 43cb35d..9228bc5 100755 --- a/scripts/bump-version.sh +++ b/scripts/bump-version.sh @@ -1,20 +1,55 @@ #!/usr/bin/env bash -# Usage: scripts/bump-version.sh -# fix 0.1.0 -> 0.1.1 (alias: patch) -# feature 0.1.0 -> 0.2.0 (alias: minor) -# major 0.1.0 -> 1.0.0 -# auto picks one from the [Unreleased] section of CHANGELOG.md; never leaves 0.x +# Usage: scripts/bump-version.sh +# +# Versions follow AndroidX style: MAJOR.MINOR.PATCH for stable releases, and a two-digit +# pre-release suffix otherwise: 1.2.0-alpha01, 1.2.0-beta02, 1.2.0-rc01. +# +# fix 1.1.0 -> 1.1.1 (alias: patch) +# feature 1.1.0 -> 1.2.0 (alias: minor) +# major 1.1.0 -> 2.0.0 +# alpha 1.1.0 -> 1.2.0-alpha01, 1.2.0-alpha01 -> 1.2.0-alpha02 +# beta 1.2.0-alpha03 -> 1.2.0-beta01, 1.2.0-beta01 -> 1.2.0-beta02 (1.1.0 -> 1.2.0-beta01) +# rc 1.2.0-beta02 -> 1.2.0-rc01, 1.2.0-rc01 -> 1.2.0-rc02 (1.1.0 -> 1.2.0-rc01) +# stable 1.2.0-rc02 -> 1.2.0 +# auto picks fix, feature or major from the [Unreleased] section of CHANGELOG.md (stable versions only) +# version an exact version such as 2.0.0-alpha01 set -euo pipefail cd "$(dirname "$0")/.." +VERSION_PATTERN='^[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta|rc)[0-9]{2})?$' + current=$(grep '^VERSION_NAME=' gradle.properties | cut -d= -f2) -IFS=. read -r major minor patch <<< "${current%%-*}" +if ! [[ "$current" =~ $VERSION_PATTERN ]]; then + echo "VERSION_NAME=${current} in gradle.properties isn't X.Y.Z or X.Y.Z-(alpha|beta|rc)NN" >&2 + exit 1 +fi +base="${current%%-*}" +IFS=. read -r major minor patch <<< "$base" +stage="" +stage_number=0 +if [[ "$current" == *-* ]]; then + suffix="${current#*-}" + stage="${suffix%%[0-9]*}" + stage_number=$((10#${suffix#"$stage"})) +fi + +stage_rank() { + case "$1" in + alpha) echo 1 ;; + beta) echo 2 ;; + rc) echo 3 ;; + esac +} unreleased=$(awk '/^## \[Unreleased\]/ { found = 1; next } found && /^## \[/ { exit } found { print }' CHANGELOG.md) kind="${1:-}" if [ "$kind" = "auto" ]; then + if [ -n "$stage" ]; then + echo "auto: ${current} is a pre-release; choose alpha, beta, rc or stable" >&2 + exit 1 + fi if grep -qiE '^### Removed|BREAKING' <<< "$unreleased"; then kind=major elif grep -qE '^### (Added|Changed)' <<< "$unreleased"; then @@ -36,10 +71,33 @@ case "$kind" in fix | patch) next="${major}.${minor}.$((patch + 1))" ;; feature | minor) next="${major}.$((minor + 1)).0" ;; major) next="$((major + 1)).0.0" ;; - [0-9]*.[0-9]*.[0-9]*) next="$kind" ;; + alpha | beta | rc) + if [ -z "$stage" ]; then + # A new pre-release line starts from the next feature version. + next="${major}.$((minor + 1)).0-${kind}01" + elif [ "$kind" = "$stage" ]; then + next=$(printf '%s-%s%02d' "$base" "$kind" $((stage_number + 1))) + elif [ "$(stage_rank "$kind")" -gt "$(stage_rank "$stage")" ]; then + next="${base}-${kind}01" + else + echo "${current} is already past ${kind}; choose $([ "$stage" = rc ] && echo "rc or stable" || echo "a later stage or stable")" >&2 + exit 1 + fi + ;; + stable) + if [ -z "$stage" ]; then + echo "${current} is already stable; choose fix, feature or major" >&2 + exit 1 + fi + next="$base" + ;; *) - echo "Usage: $0 " >&2 - exit 1 + if [[ "$kind" =~ $VERSION_PATTERN ]]; then + next="$kind" + else + echo "Usage: $0 " >&2 + exit 1 + fi ;; esac