diff --git a/.gitignore b/.gitignore index 9d9349d..c309513 100644 --- a/.gitignore +++ b/.gitignore @@ -170,4 +170,3 @@ docs/api docs/sample docs/changelog.md docs/markdown.md -publishing-progress.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 076663a..dad0109 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,20 @@ All notable changes to this project are documented in this file. ## Unreleased +### Changed +- Downgraded Kotlin from 2.4.10 to 2.3.21 for broader consumer compatibility. +- Removed `compose-stability-analyzer` plugin (KMP incompatible). + +### Documentation +- Updated minimum Kotlin version requirement to 2.3.21 in installation guide. +- Added acknowledgment for chrisbanes/haze inspiration. +- Added GitHub issue templates for bugs and feature requests. + +### Quality +- Added Metalava API tracking to all published modules. +- Improved CI infrastructure: split workflows to Linux/macOS, added snapshot deploy, replaced `release.sh` with platform-agnostic `release.py`. +- Enabled automatic Maven Central publishing. + ## [0.1.0] - 2026-07-15 ### Added diff --git a/TODO-endpoint-preview.md b/TODO-endpoint-preview.md deleted file mode 100644 index b54d4ed..0000000 --- a/TODO-endpoint-preview.md +++ /dev/null @@ -1,243 +0,0 @@ -# NetworkMock — Endpoint Detail Screen & Mock Preview - -Migration of `NetworkMockEndpointBottomSheet` to a proper full-screen navigation -destination, enabling a `ModalBottomSheet` for previewing individual mock response -content without any sheet-nesting concerns. - ---- - -## Current State (as of 2026-03-13) - -| File | Status | -|---|---| -| `NetworkMock.kt` | ✅ `Endpoint` destination declared, serializer registered, `entryDestination` set, `entry` fully wired with inline VM factory | -| `NetworkMockScreen.kt` | Old bottom-sheet block **commented out**; `selectedDescriptor` / `selectedEndpointState` observations and related dead params still present — pending Step 6 | -| `NetworkMockEndpointScreen.kt` | ✅ Rewritten as a proper full screen with endpoint header, VM-driven state, and `previewingResponse` stub | -| `NetworkMockViewModel.kt` | Selection state (`selectedEndpointKey`, `selectedEndpointDescriptor`, `selectedEndpointState`, `selectEndpoint()`, `clearSelectedEndpoint()`) still present — pending Step 7 | -| `NetworkMockEndpointViewModel.kt` | ✅ Created | -| `MockItem.kt` | ✅ `onLongClick`, `isInPreviewMode`, `combinedClickable`, vertical-axis flip animation implemented | -| `NetworkMockEndpointPreviewBottomSheet.kt` | ✅ Complete — Single and Compare modes implemented, smart diff wired | 3c | -| `components/MockResponseDiffContent.kt` | ✅ Created — `DiffLine`, LCS algorithm, `InlineDiffContent`, `SplitDiffContent` | 3c | - ---- - -## Implementation Steps - -### Step 1 — `NetworkMockEndpointViewModel` ✅ Done - -Created `viewmodel/NetworkMockEndpointViewModel.kt` with: -- `uiState: StateFlow` (`Loading` / `Error` / `Content`) -- `setMockState(responseFileName: String?)` delegating to `stateRepository` -- `EndpointLoadingState` private sealed interface (renamed from `LoadingState` to avoid - package-level redeclaration conflict with `NetworkMockViewModel.kt`) - -**Note:** `EndpointDescriptor` was subsequently made `@Serializable` (along with `MockResponse` -and `EndpointConfig`). `NetworkMockDestination.Endpoint` still carries `EndpointKey` rather than -`EndpointDescriptor` — embedding the full descriptor (including all `MockResponse.content` bodies) -in a `NavKey` would bloat serialised navigation state proportionally to mock file count and size. -`EndpointKey` is the correct minimal identifier; the VM reconstructs the descriptor from the -cached repositories on the other side. - ---- - -### Step 2 — `NetworkMockEndpointScreen` ✅ Done - -Rewrote `NetworkMockEndpointScreen.kt` as a proper full screen: -- Removed `ModalBottomSheet`, `rememberModalBottomSheetState`, `rememberCoroutineScope`, - close `IconButton`, `onDismissRequest`, and the wrapping `Box` header -- Final signature: `viewModel: NetworkMockEndpointViewModel`, `modifier`, `bottomPadding` — - repositories are not passed through the screen; the VM is constructed by the caller - (`NetworkMock.registerContent`) and injected directly -- Collects `uiState` from `NetworkMockEndpointViewModel`; delegates to existing `LoadingState` - and `ErrorState` components for those variants -- Extracted the list body into a private `NetworkMockEndpointScreenContent` composable; - wraps the `LazyColumn` in a `Column` with a sticky header `Surface` showing: - - `config.name` at `titleLarge` (was the old bottom sheet title) - - `method` + `path` in monospaced style, consistent with `EndpointCard` - - `EndpointStateChip` for the current active state - - `HorizontalDivider` separating the header from the list -- `bottomPadding` applied via `contentPadding` on the `LazyColumn` -- `previewingResponse: MockResponse?` local state added as a stub — wired to `onPreviewClick` - on each `MockItem` but not yet consumed (intentional — Step 3 will add the sheet) -- `@Preview` calls `NetworkMockEndpointScreenContent` directly with fake data - ---- - -### Step 3 — `MockResponsePreviewSheet` / `NetworkMockEndpointPreviewBottomSheet` ✅ Done - -> **Interaction model:** long-press on a `MockItem` opens/toggles the preview sheet. -> Tap continues to mean "select this mock for the endpoint" — the two actions are fully -> orthogonal. A hint card in the endpoint header tells the user that long-press -> is available, making the gesture discoverable without cluttering the rows. - -#### 3a — `PreviewSheetState` ✅ Done - -Implemented as a `sealed interface` in `NetworkMockEndpointScreen.kt`: - -```kotlin -sealed interface PreviewSheetState { - sealed interface HasResponse : PreviewSheetState - data object Hidden : PreviewSheetState - data class Single(val response: MockResponse) : HasResponse - data class Compare(val left: MockResponse, val right: MockResponse) : HasResponse - - fun transition(response: MockResponse): PreviewSheetState - fun isInPreviewMode(response: MockResponse): Boolean -} -``` - -Transition behaviour (deviates slightly from original plan — better UX): - -| Current state | Long-pressed item | Next state | -|---|---|---| -| `Hidden` | any `r` | `Single(r)` | -| `Single(r)` | same `r` | `Hidden` (toggle off / close) | -| `Single(a)` | different `b` | `Compare(a, b)` | -| `Compare(a, b)` | `a` | `Single(b)` (de-selects `a`, keeps `b`) | -| `Compare(a, b)` | `b` | `Single(a)` (de-selects `b`, keeps `a`) | -| `Compare(a, b)` | new `c` | `Compare(a, b)` unchanged (cap enforced — no-op) | - -#### 3b — `NetworkMockEndpointScreenContent` wiring ✅ Done - -- `previewSheetState: PreviewSheetState` owned in `NetworkMockEndpointScreenContent` -- `showPreviewBottomSheet: Boolean` separate boolean guards the actual sheet composition -- Centered FAB appears with `slideInVertically + fadeIn + scaleIn` when `previewSheetState != Hidden`; tapping it sets `showPreviewBottomSheet = true` -- Each `MockItem` receives `onLongClick = { previewSheetState = previewSheetState.transition(response) }` and `isInPreviewMode = previewSheetState.isInPreviewMode(response)` -- Hint card (`ElevatedCard` + `bodySmall` text) shown above the `HorizontalDivider` -- Sheet composed conditionally: `if (showPreviewBottomSheet && previewSheetState is HasResponse)` - -#### 3c — `NetworkMockEndpointPreviewBottomSheet` body ✅ Done - -`NetworkMockEndpointPreviewBottomSheet.kt` exists with the shell in place: -- `ModalBottomSheet(skipPartiallyExpanded = true)` ✅ -- Animated close via `sheetState.hide()` + `invokeOnCompletion` ✅ -- `HasResponse` typed parameter ✅ -- **Body content not yet implemented** — needs the two modes below. - -##### Single mode (`PreviewSheetState.Single`) - -- Header row: status-code coloured icon chip + `displayName` as title + existing close button -- Body: `Text` with `FontFamily.Monospace` inside `verticalScroll` + `horizontalScroll` - so wide JSON lines do not wrap - -##### Compare mode (`PreviewSheetState.Compare`) - -- Header: two response chips side-by-side + close button at trailing end -- Body: **smart diff** — choose display mode based on content similarity: - -**Diff algorithm (pure Kotlin, no library):** - -Create `components/MockResponseDiffContent.kt` containing: - -```kotlin -internal sealed interface DiffLine { - data class Unchanged(val text: String) : DiffLine - data class Different(val textLeft: String?, val textRight: String?) : DiffLine -} -``` - -1. Split both `content` strings by `\n` into `linesA` and `linesB` -2. Compute LCS (Longest Common Subsequence) of the two line lists — standard O(n²) DP -3. Derive similarity ratio: `lcsLength / max(linesA.size, linesB.size)` -4. If `ratio >= 0.4` → **inline diff** (Mode A); otherwise → **split view** (Mode B) - - Threshold is hardcoded at `0.4` for now - -**Mode A — Inline diff** (similarity ≥ 0.4): -- Single scrollable column, full width -- Each `DiffLine.Unchanged` → plain `Text` in `FontFamily.Monospace` -- Each `DiffLine.Different` → two sub-rows: - - Left line: `primary` colour background tint + `onPrimary` text - - Right line: `secondary` colour background tint + `onSecondary` text -- Labels above the header chips ("left" / "right") clarify which colour belongs to which response -- Avoids red/green semantics — `primary`/`secondary` are neutral "A vs B" colours - -**Mode B — Split view** (similarity < 0.4): -- Two `Column`s stacked **vertically** via `weight(1f)` each in a parent `Column` -- A `HorizontalDivider` separates the two halves -- Each half has its own independent `verticalScroll` + `horizontalScroll` -- Each half has a small chip header identifying which response it shows -- Content rendered in `FontFamily.Monospace` -- No side-by-side splitting — safe for portrait phone screens - -Both `computeLineDiff()` and `shouldUseInlineDiff()` computed via `remember(left.content, right.content)` in the composable so they only rerun when content changes. - -`@Preview` variants needed: -- `Single` with a fake response -- `Compare` — inline diff case (similar content) -- `Compare` — split view case (dissimilar content) - ---- - -### Step 4 — `MockItem` — long-press + animated leading slot swap 🚧 In Progress - -#### 4a — `onLongClick` + `isInPreviewMode` ✅ Done - -`MockItem` updated with: -- `onLongClick: () -> Unit` parameter -- `isInPreviewMode: Boolean` parameter -- `combinedClickable(onClick = onClick, onLongClick = onLongClick)` on the row - -#### 4b — Animated leading slot swap ✅ Done - -Gmail-style vertical-axis flip on the leading icon chip: -- `updateTransition(isInPreviewMode)` animates `flipProgress: Float` from `-1f` → `1f` -- `abs(flipProgress)` applied as `scaleX` via `graphicsLayer` — collapses to 0 at midpoint -- Icon swaps at midpoint via `derivedStateOf { if (flipProgress >= 0f) CheckBox else statusIcon }` -- `NetworkItem` is unaffected (no long-press, no flip) - -#### 4c — Hint card ✅ Done - -`ElevatedCard` with `bodySmall` text above the list divider in `NetworkMockEndpointScreenContent`: -> *"Long press a mock response to be able to preview its content"* - ---- - -### Step 5 — Wire `NetworkMock.registerContent` ✅ Done - -`entry` is fully wired in `NetworkMock.kt`: -- VM constructed inline via `viewModel { NetworkMockEndpointViewModel(...) }` using - `it.endpointKey` from the typed destination -- `modifier = Modifier.fillMaxSize()` and `bottomPadding` forwarded correctly - ---- - -### Step 6 — Clean up `NetworkMockScreen` ✅ TODO - -The bottom-sheet call block is already commented out. Finish the cleanup: - -- Delete the commented-out `selectedDescriptor?.let { ... }` block -- Remove `val selectedDescriptor` and `val selectedEndpointState` observations from - `NetworkMockScreen` -- Remove `setEndpointMockState`, `clearSelectedEndpoint`, `selectedDescriptor`, - `selectedEndpointState` parameters from `NetworkMockScreen`, `NetworkMockScreenContent`, - `ContentState`, and the `@Preview` -- Remove now-unused imports: `EndpointDescriptor`, `EndpointMockState` - ---- - -### Step 7 — Clean up `NetworkMockViewModel` ✅ TODO - -Remove the endpoint selection concern entirely: - -- Delete `selectedEndpointKey: MutableStateFlow` -- Delete `selectedEndpointDescriptor: StateFlow` and its KDoc -- Delete `selectedEndpointState: StateFlow` and its KDoc -- Delete `selectEndpoint(key: EndpointKey)` and its KDoc -- Delete `clearSelectedEndpoint()` and its KDoc -- Remove `EndpointDescriptor` import if it becomes unused after the deletions - ---- - -## Files Changed Summary - -| File | Action | Step | -|---|---|---| -| `viewmodel/NetworkMockEndpointViewModel.kt` | ✅ Created | 1 | -| `NetworkMockEndpointScreen.kt` | ✅ Rewritten — real screen, VM-driven, header added, preview stub in place | 2 | -| `components/MockResponsePreviewSheet.kt` *(new)* | Create preview sheet — single & compare modes; `PreviewSheetState` sealed interface | 3 | -| `components/MockResponseDiffContent.kt` *(new)* | `DiffLine` sealed interface, LCS algorithm, `computeLineDiff()`, `shouldUseInlineDiff()`, `InlineDiffContent`, `SplitDiffContent` composables | 3c | -| `components/MockItem.kt` | Add `MockItemPreviewState` enum + `onLongPress` + `inPreviewMode` boolean + Gmail-style animated leading slot swap (status chip ↔ Checkbox) | 4a | -| `NetworkMockEndpointScreen.kt` | Add hint card to endpoint header; compute `previewState` per item from `previewSheetState` | 4b | -| `NetworkMock.kt` | ✅ `entry` wired with inline VM factory | 5 | -| `NetworkMockScreen.kt` | Remove dead selected-endpoint wiring | 6 | -| `NetworkMockViewModel.kt` | Remove selection state, flows, and methods | 7 | diff --git a/devview-networkmock-core/src/commonMain/kotlin/com/worldline/devview/networkmock/core/repository/MockConfigRepository.kt b/devview-networkmock-core/src/commonMain/kotlin/com/worldline/devview/networkmock/core/repository/MockConfigRepository.kt index 9e34058..80cfdfd 100644 --- a/devview-networkmock-core/src/commonMain/kotlin/com/worldline/devview/networkmock/core/repository/MockConfigRepository.kt +++ b/devview-networkmock-core/src/commonMain/kotlin/com/worldline/devview/networkmock/core/repository/MockConfigRepository.kt @@ -6,6 +6,7 @@ import com.worldline.devview.networkmock.core.model.MockMatch import com.worldline.devview.networkmock.core.model.MockResponse import com.worldline.devview.networkmock.core.model.effectiveEndpoints import com.worldline.devview.networkmock.core.repository.MockConfigRepository.Companion.DEFAULT_STATUS_CODES +import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json /** @@ -181,50 +182,59 @@ public class MockConfigRepository( * * @return A [Result] containing the [MockConfiguration] on success, or an error on failure */ - public suspend fun loadConfiguration(): Result = runCatching { - cachedConfig?.let { - println( - message = "[NetworkMock][Config] Using cached configuration with ${it.apiGroups.size} group(s)" - ) - return@runCatching it - } - - println(message = "[NetworkMock][Config] Loading configuration from: $configPath") + public suspend fun loadConfiguration(): Result { + // ponytail: explicit try-catch instead of runCatching — K/N's inline expansion of runCatching + // does not reliably catch exceptions thrown by suspend calls in the generated state machine. + return try { + cachedConfig?.let { + println( + message = "[NetworkMock][Config] Using cached configuration with ${it.apiGroups.size} group(s)" + ) + return Result.success(value = it) + } - val configBytes = resourceLoader(configPath) - val configJson = configBytes.decodeToString() + println(message = "[NetworkMock][Config] Loading configuration from: $configPath") - val config = json - .decodeFromString( - string = configJson - ) - cachedConfig = config + val configBytes = resourceLoader(configPath) + val configJson = configBytes.decodeToString() - println(message = "[NetworkMock][Config] Successfully loaded configuration:") - config.apiGroups.forEach { group -> - println( - message = "[NetworkMock][Config] Group: ${group.id} " + - "with ${group.endpoints.size} shared endpoint(s) " + - "and ${group.environments.size} environment(s)" - ) - group.environments.forEach { env -> - println( - message = "[NetworkMock][Config] Environment: ${env.id} (${env.url})" + val config = json + .decodeFromString( + string = configJson ) - } - group.endpoints.forEach { endpoint -> + cachedConfig = config + + println(message = "[NetworkMock][Config] Successfully loaded configuration:") + config.apiGroups.forEach { group -> println( - message = "[NetworkMock][Config] - ${endpoint.method} ${endpoint.path} (${endpoint.id})" + message = "[NetworkMock][Config] Group: ${group.id} " + + "with ${group.endpoints.size} shared endpoint(s) " + + "and ${group.environments.size} environment(s)" ) + group.environments.forEach { env -> + println( + message = "[NetworkMock][Config] Environment: ${env.id} (${env.url})" + ) + } + group.endpoints.forEach { endpoint -> + println( + message = "[NetworkMock][Config] - ${endpoint.method} ${endpoint.path} (${endpoint.id})" + ) + } } - } - config - }.onFailure { error -> - println( - message = "[NetworkMock][Config] ERROR: Failed to load configuration - ${error.message}" - ) - error.printStackTrace() + Result.success(value = config) + } catch (e: IllegalStateException) { + println( + message = "[NetworkMock][Config] ERROR: Failed to load configuration - ${e.message}" + ) + Result.failure(exception = e) + } catch (e: SerializationException) { + println( + message = "[NetworkMock][Config] ERROR: Failed to load configuration - ${e.message}" + ) + Result.failure(exception = e) + } } /** @@ -549,7 +559,7 @@ public class MockConfigRepository( private suspend fun loadMockResponseFromPath( filePath: String, fileName: String - ): MockResponse? = runCatching { + ): MockResponse? = try { val responseBytes = resourceLoader(filePath) val content = responseBytes.decodeToString() MockResponse.Companion @@ -557,7 +567,9 @@ public class MockConfigRepository( fileName = fileName, content = content ) - }.getOrNull() + } catch (@Suppress("SwallowedException") e: IllegalStateException) { + null + } /** * Extracts the hostname from a URL string. diff --git a/devview-networkmock-core/src/commonTest/kotlin/com/worldline/devview/networkmock/core/repository/MockConfigRepositoryTest.kt b/devview-networkmock-core/src/commonTest/kotlin/com/worldline/devview/networkmock/core/repository/MockConfigRepositoryTest.kt index e81883e..2d23354 100644 --- a/devview-networkmock-core/src/commonTest/kotlin/com/worldline/devview/networkmock/core/repository/MockConfigRepositoryTest.kt +++ b/devview-networkmock-core/src/commonTest/kotlin/com/worldline/devview/networkmock/core/repository/MockConfigRepositoryTest.kt @@ -329,7 +329,7 @@ class MockConfigRepositoryTest { ) { private val calls = mutableMapOf() - fun load(path: String): ByteArray { + suspend fun load(path: String): ByteArray { calls[path] = (calls[path] ?: 0) + 1 return resources[path]?.encodeToByteArray() ?: error("Resource not found: $path") diff --git a/docs/contributing/publishing.md b/docs/contributing/publishing.md index 00ecc4f..ceb8768 100644 --- a/docs/contributing/publishing.md +++ b/docs/contributing/publishing.md @@ -6,7 +6,7 @@ This guide walks through publishing DevView to Maven Central from scratch — no The publish pipeline is already wired: -- **Trigger**: pushing a tag matching `v*` (e.g. `v0.1.0`) fires `.github/workflows/publish.yml` +- **Trigger**: pushing a tag matching `[0-9]*` (e.g. `0.1.1`) fires `.github/workflows/publish.yml` - **What it does**: runs build + detekt + konsist checks, then calls `./gradlew publish` using the [Vanniktech Maven Publish plugin](https://github.com/vanniktech/gradle-maven-publish-plugin) - **Signing**: all artifacts are GPG-signed before upload (`RELEASE_SIGNING_ENABLED=true` in `gradle.properties`) - **Target**: the new [Maven Central Portal](https://central.sonatype.com) (`SONATYPE_HOST=CENTRAL_PORTAL`) @@ -134,19 +134,21 @@ The `publish.yml` workflow passes these to Gradle as `ORG_GRADLE_PROJECT_*` envi Ensure you are on `main` with a clean working tree, then run: ```bash -./scripts/release.sh 0.1.0 0.2.0-SNAPSHOT +python3 scripts/release.py 0.1.1 0.2.0-SNAPSHOT ``` -On Windows, run this from Git Bash (not PowerShell — the script uses `sed` and bash features). +Works on any platform (Windows, macOS, Linux) — no bash required. The script does the following automatically: -1. Replaces `VERSION_NAME` in `gradle.properties` with `0.1.0` -2. Commits: `Prepare for release 0.1.0` -3. Creates tag `v0.1.0` — **this tag push triggers the publish workflow** -4. Replaces `VERSION_NAME` with `0.2.0-SNAPSHOT` -5. Commits: `Prepare next development version` -6. Pushes both commits and the tag (`git push && git push --tags`) +1. Replaces `VERSION_NAME` in `gradle.properties` with `0.1.1` +2. Snapshots each published module's `api/api.txt` to `api/0.1.1.txt` +3. Inserts `## [0.1.1] - ` in `CHANGELOG.md` under `## Unreleased` +4. Commits: `Prepare for release 0.1.1` +5. Creates tag `0.1.1` — **this tag push triggers the publish workflow** +6. Replaces `VERSION_NAME` with `0.2.0-SNAPSHOT` +7. Commits: `Prepare next development version` +8. Pushes both commits and the tag --- diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 3730ca1..02fa0d5 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -45,7 +45,7 @@ Add DevView dependencies to your `gradle/libs.versions.toml`: ```toml [versions] -devview = "0.1.0" +devview = "0.1.1" [libraries] devview = { module = "com.worldline.devview:devview", version.ref = "devview" } @@ -85,13 +85,13 @@ kotlin { sourceSets { commonMain.dependencies { // Core DevView module (required) - implementation("com.worldline.devview:devview:0.1.0") + implementation("com.worldline.devview:devview:0.1.1") // Optional: FeatureFlip module - implementation("com.worldline.devview:devview-featureflip:0.1.0") + implementation("com.worldline.devview:devview-featureflip:0.1.1") // Optional: Analytics module - implementation("com.worldline.devview:devview-analytics:0.1.0") + implementation("com.worldline.devview:devview-analytics:0.1.1") } } } diff --git a/docs/index.md b/docs/index.md index 52cb23c..759ddfe 100644 --- a/docs/index.md +++ b/docs/index.md @@ -17,12 +17,11 @@ ## What's New -> **v0.1.0 — First Release** -> - Core DevView framework with module registry DSL, section-based home screen, and type-safe Navigation3 integration -> - FeatureFlip module: feature flags with type badge and filter, tri-state remote override, DataStore-backed persistence -> - Analytics module: real-time in-app event capture with typed log categories and clear-log action -> - NetworkMock modules: JSON-driven mock engine, Ktor client plugin, and UI for toggling mocks per endpoint -> - devview-utils: shared DataStore utilities and `RequiresDataStore` initialization contract +> **v0.1.1 — Compatibility Fix** +> - Downgraded Kotlin from 2.4.10 to 2.3.21 for broader consumer compatibility +> - Removed `compose-stability-analyzer` plugin (KMP incompatible) +> - Added Metalava API tracking to all published modules +> - CI improvements: split Linux/macOS workflows, snapshot deploys, platform-agnostic release script --- diff --git a/gradle.properties b/gradle.properties index 6a850a1..aaa08f9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -34,7 +34,7 @@ RELEASE_SIGNING_ENABLED=true mavenCentralAutomaticPublishing=true GROUP=com.worldline.devview -VERSION_NAME=0.2.0-SNAPSHOT +VERSION_NAME=0.1.1-SNAPSHOT POM_DESCRIPTION=DevView POM_URL=https://github.com/worldline/devview/