diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 554e11126..78a517e81 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -215,7 +215,8 @@
android:name=".crashes.CrashListActivity"
android:exported="false" />
-
+
.filterNotExistsOnDisk(): List {
+ return this.filter {
+ File(it.filePath).exists()
+ }
+}
diff --git a/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadFragment.kt b/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadFragment.kt
index 82ed98e0a..95effa563 100644
--- a/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadFragment.kt
+++ b/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadFragment.kt
@@ -10,6 +10,7 @@ import android.view.View
import android.view.ViewGroup
import kotlinx.android.synthetic.main.fragment_downloads.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.browser.state.state.content.DownloadState
import mozilla.components.feature.downloads.AbstractFetchDownloadService
import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.support.base.feature.UserInteractionHandler
@@ -18,6 +19,7 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.components.metrics.Event
+import org.mozilla.fenix.ext.filterNotExistsOnDisk
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.library.LibraryPageFragment
@@ -37,13 +39,16 @@ class DownloadFragment : LibraryPageFragment(), UserInteractionHan
val items = requireComponents.core.store.state.downloads.map {
DownloadItem(
- it.value.id,
+ it.value.id.toString(),
it.value.fileName,
it.value.filePath,
it.value.contentLength.toString(),
- it.value.contentType
+ it.value.contentType,
+ it.value.status
)
- }
+ }.filter {
+ it.status == DownloadState.Status.COMPLETED
+ }.filterNotExistsOnDisk()
downloadStore = StoreProvider.get(this) {
DownloadFragmentStore(
diff --git a/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadFragmentStore.kt b/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadFragmentStore.kt
index c79d45027..8f4915e33 100644
--- a/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadFragmentStore.kt
+++ b/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadFragmentStore.kt
@@ -4,6 +4,7 @@
package org.mozilla.fenix.library.downloads
+import mozilla.components.browser.state.state.content.DownloadState
import mozilla.components.lib.state.Action
import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store
@@ -17,11 +18,12 @@ import mozilla.components.lib.state.Store
* @property contentType The type of file the download is
*/
data class DownloadItem(
- val id: Long,
+ val id: String,
val fileName: String?,
val filePath: String,
val size: String,
- val contentType: String?
+ val contentType: String?,
+ val status: DownloadState.Status
)
/**
diff --git a/app/src/main/java/org/mozilla/fenix/widget/VoiceSearchActivity.kt b/app/src/main/java/org/mozilla/fenix/widget/VoiceSearchActivity.kt
index c9232ba8b..d76d4864d 100644
--- a/app/src/main/java/org/mozilla/fenix/widget/VoiceSearchActivity.kt
+++ b/app/src/main/java/org/mozilla/fenix/widget/VoiceSearchActivity.kt
@@ -34,6 +34,11 @@ class VoiceSearchActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ if (Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).resolveActivity(packageManager) == null) {
+ finish()
+ return
+ }
+
// Retrieve the previous intent from the saved state
previousIntent = savedInstanceState?.get(PREVIOUS_INTENT) as Intent?
if (previousIntent.isForSpeechProcessing()) {
diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml
index a39a72233..c93fa1a72 100644
--- a/app/src/main/res/values-az/strings.xml
+++ b/app/src/main/res/values-az/strings.xml
@@ -1,9 +1,5 @@
-
- Məxfi Firefox Preview
-
- Firefox Preview (Məxfi)
Digər seçimlər
@@ -13,23 +9,17 @@
Məxfi səyahəti söndür
Ünvanı daxil et və ya axtar
-
- Açıq vərəq yoxdur
Açıq vərəqləriniz burada göstəriləcək.
Məxfi sessiyadasınız
-
- %1$s tətbiqdən çıxdığınızda və ya bütün məxfi səyahət vərəq və pəncərələrini qapatdığınızda axtarış və səyahət tarixçənizi təmizləyir. Bu sizi saytlar və internet provayderiniz üçün anonim etməsə də,
- onlayn məlumatlarınızı bu cihazı işlədən digər şəxslərdən saxlamağınızı asanlaşdırır.
Məxfi səyahət haqqında olan əfsanələr
Sessiyanı sil
-
+
Ana ekranınızdan məxfi vərəqləri açmaq üçün qısayol əlavə edin.
@@ -84,8 +74,6 @@
Yeni vərəq
Kolleksiyaya saxla
-
- Saytla bağlı problem xəbər et
Paylaş
@@ -99,7 +87,7 @@
%1$s Tərəfindən
-
+
Oxuyucu vörünüşü
Tətbiqdə aç
@@ -119,14 +107,8 @@
Skanla
-
- Qısayollar
Axtarış mühərriyi tənzimləmələri
-
- Bununla axtar:
-
- Bu dəfə bununla axtarın:
Buferdəki keçidi doldur
@@ -223,8 +205,6 @@
Tərtibatçı alətləri
USB ilə məsafəli sazlama
-
- Axtarış qısayollarını göstər
Axtarış təkliflərini göstər
@@ -273,10 +253,6 @@
Son uğurlu olan: %s
Son uğurlu olan: heç vaxt
-
- %s - %s %s
@@ -373,8 +349,6 @@
Oxuma siyahısı
Axtar
-
- Kitabxana
Tənzimləmələr
@@ -401,8 +375,6 @@
Bütün vərəqləri qapat
Vərəqləri paylaş
-
- Kolleksiyaya saxla
Vərəq menyusu
@@ -523,8 +495,6 @@
%1$s silindi
-
- Seçilən əlfəcinlər silinir
GERİ AL
@@ -576,14 +546,11 @@
Sönülü
+
Kolleksiyalar
Kolleksiya menyusu
-
- Kolleksiya yoxdur
-
- Kolleksiyalarınız burada göstəriləcəklər.
Vərəqləri seç
@@ -702,8 +669,6 @@
Şrift ölçüsü
-
- Açıq Vərəqlər
%d vərəq
@@ -721,8 +686,6 @@
Çərəzlər
Sayt icazələri
-
- Səyahət tarixçəsi
Çıx
@@ -747,14 +710,15 @@
Firefox-a daxil ol
Sync aktivdir
-
- Standart
Tənzimləmələri aç
Məxfiliyiniz
+
+ Qapat
+
Səyahətə başlayın
@@ -762,6 +726,21 @@
Mövzunuzu seçin
+
+ Avtomatik
+
+ Tünd mövzu
+
+ Açıq mövzu
+
+
+ Vərəqlər göndərildi!
+
+ Vərəq göndərildi!
+
+ Göndərilə bilmədi
+
+ Kodu skanla
E-poçt ilə daxil ol
@@ -778,28 +757,14 @@
İzlənmədən səyahət edin
Ətraflı öyrən
-
- Standart
-
- Standart (məsləhətlidir)
-
- Tarazlaşdırılmış qoruma və verimlilik.
-
- Səhifələr normal yüklənəcəklər, amma daha az izləyici əngəllənəcək.
Standart izlənmə qorumasında nələr əngəllənir
Sərt
-
- Sərt (Ön seçilən)
-
- Sərt (məsləhətlidir)
Sərt izlənmə qorumasında nələr əngəllənir
Fərdi
-
- Hansı izləyici və skriptlərin əngəllənəcəyini seçin
Fərdi izlənmə qorumasında nələr əngəllənir
@@ -1015,4 +980,5 @@
Bu hesabı silmək istədiyinizə əminsiniz?
Sil
-
+
+
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 746d7cb07..0662229e0 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -1032,11 +1032,6 @@
Abrir configurações
Sua privacidade
-
- Projetamos o %s para lhe dar o controle sobre o que você compartilha
- online e o que compartilha conosco.
-
Leia nosso aviso de privacidade
diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml
index 2275cf749..767cf0fb0 100644
--- a/app/src/main/res/values-ro/strings.xml
+++ b/app/src/main/res/values-ro/strings.xml
@@ -400,7 +400,7 @@
Autentificare
- Autentifică-te pentru reconectare
+ Intră în cont pentru reconectare
Șterge contul
@@ -632,7 +632,7 @@
Editează dosarul
- Autentifică-te pentru a vedea marcajele sincronizate
+ Intră în cont pentru a vedea marcajele sincronizate
URL
@@ -782,7 +782,7 @@
Folosite recent
- Autentifică-te în Sync
+ Intră în contul Sync
Trimite pe toate dispozitivele
@@ -792,7 +792,7 @@
Conectează alt dispozitiv
- Pentru a trimite o filă, autentifică-te în Firefox pe cel puțin un alt dispozitiv.
+ Pentru a trimite o filă, intră în contul Firefox de pe cel puțin un alt dispozitiv.
Am înțeles
@@ -986,7 +986,7 @@
Autentificare…
- Autentifică-te în Firefox
+ Intră în contul Firefox
Rămâi deconectat
@@ -1068,7 +1068,7 @@
Gata de scanare
- Autentifică-te cu camera
+ Autentificare cu camera
Folosește e-mailul în schimb
@@ -1246,7 +1246,7 @@
Reconectare
- Autentifică-te în Sync
+ Intră în contul Sync
Date de autentificare salvate
@@ -1464,7 +1464,7 @@
Afișează o listă de file de pe celelalte dispozitive.
- Autentifică-te în Sync
+ Intră în contul Sync
diff --git a/app/src/test/java/org/mozilla/fenix/ext/ListTest.kt b/app/src/test/java/org/mozilla/fenix/ext/ListTest.kt
new file mode 100644
index 000000000..e6ff4a337
--- /dev/null
+++ b/app/src/test/java/org/mozilla/fenix/ext/ListTest.kt
@@ -0,0 +1,77 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.ext
+
+import mozilla.components.browser.state.state.content.DownloadState
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
+import org.mozilla.fenix.library.downloads.DownloadItem
+import java.io.File
+
+@RunWith(FenixRobolectricTestRunner::class)
+class ListTest {
+
+ @Test
+ fun `Test download in list but not on disk removed from list`() {
+ val filePath1 = "filepath.txt"
+ val filePath3 = "filepath3.txt"
+
+ var file1 = File(filePath1)
+ var file3 = File(filePath3)
+
+ // Create files
+ file1.createNewFile()
+ file3.createNewFile()
+
+ val item1 = DownloadItem("71", "filepath.txt", filePath1, "71 Mb", "Image/png", DownloadState.Status.COMPLETED)
+ val item2 = DownloadItem("71", "filepath2.txt", "filepath2.txt", "71 Mb", "Image/png", DownloadState.Status.COMPLETED)
+ val item3 = DownloadItem("71", "filepath3.txt", filePath3, "71 Mb", "Image/png", DownloadState.Status.COMPLETED)
+
+ val testList = mutableListOf(item1, item2, item3)
+ val comparisonList: MutableList = mutableListOf(item1, item3)
+
+ val resultList = testList.filterNotExistsOnDisk()
+
+ assertEquals(comparisonList, resultList)
+
+ // Cleanup files
+ file1.delete()
+ file3.delete()
+ }
+
+ @Test
+ fun `Test download in list and on disk remain in list`() {
+ val filePath1 = "filepath.txt"
+ val filePath2 = "filepath.txt"
+ val filePath3 = "filepath3.txt"
+
+ var file1 = File(filePath1)
+ var file2 = File(filePath2)
+ var file3 = File(filePath3)
+
+ // Create files
+ file1.createNewFile()
+ file2.createNewFile()
+ file3.createNewFile()
+
+ val item1 = DownloadItem("71", "filepath.txt", filePath1, "71 Mb", "text/plain", DownloadState.Status.COMPLETED)
+ val item2 = DownloadItem("72", "filepath2.txt", filePath2, "71 Mb", "text/plain", DownloadState.Status.COMPLETED)
+ val item3 = DownloadItem("73", "filepath3.txt", filePath3, "71 Mb", "text/plain", DownloadState.Status.COMPLETED)
+
+ val testList = mutableListOf(item1, item2, item3)
+ val comparisonList: MutableList = mutableListOf(item1, item2, item3)
+
+ val resultList = testList.filterNotExistsOnDisk()
+
+ assertEquals(comparisonList, resultList)
+
+ // Cleanup files
+ file1.delete()
+ file2.delete()
+ file3.delete()
+ }
+}
diff --git a/app/src/test/java/org/mozilla/fenix/library/downloads/DownloadControllerTest.kt b/app/src/test/java/org/mozilla/fenix/library/downloads/DownloadControllerTest.kt
index 68ae555dd..75d3d7418 100644
--- a/app/src/test/java/org/mozilla/fenix/library/downloads/DownloadControllerTest.kt
+++ b/app/src/test/java/org/mozilla/fenix/library/downloads/DownloadControllerTest.kt
@@ -10,6 +10,7 @@ import io.mockk.verify
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineScope
+import mozilla.components.browser.state.state.content.DownloadState
import org.junit.Assert.assertFalse
import org.junit.Before
import org.junit.Test
@@ -20,7 +21,7 @@ import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@ExperimentalCoroutinesApi
@RunWith(FenixRobolectricTestRunner::class)
class DownloadControllerTest {
- private val downloadItem = DownloadItem(0, "title", "url", "77", "jpg")
+ private val downloadItem = DownloadItem("0", "title", "url", "77", "jpg", DownloadState.Status.COMPLETED)
private val scope: CoroutineScope = TestCoroutineScope()
private val store: DownloadFragmentStore = mockk(relaxed = true)
private val state: DownloadFragmentState = mockk(relaxed = true)
diff --git a/app/src/test/java/org/mozilla/fenix/library/downloads/DownloadInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/library/downloads/DownloadInteractorTest.kt
index 3e552d6ab..74a80f888 100644
--- a/app/src/test/java/org/mozilla/fenix/library/downloads/DownloadInteractorTest.kt
+++ b/app/src/test/java/org/mozilla/fenix/library/downloads/DownloadInteractorTest.kt
@@ -7,11 +7,12 @@ package org.mozilla.fenix.library.downloads
import io.mockk.every
import io.mockk.mockk
import io.mockk.verifyAll
+import mozilla.components.browser.state.state.content.DownloadState
import org.junit.Assert.assertTrue
import org.junit.Test
class DownloadInteractorTest {
- private val downloadItem = DownloadItem(0, "title", "url", "5.6 mb", "png")
+ private val downloadItem = DownloadItem("0", "title", "url", "5.6 mb", "png", DownloadState.Status.COMPLETED)
val controller: DownloadController = mockk(relaxed = true)
val interactor = DownloadInteractor(controller)
diff --git a/app/src/test/java/org/mozilla/fenix/widget/VoiceSearchActivityTest.kt b/app/src/test/java/org/mozilla/fenix/widget/VoiceSearchActivityTest.kt
index 6ba449f6b..504c5fbe1 100644
--- a/app/src/test/java/org/mozilla/fenix/widget/VoiceSearchActivityTest.kt
+++ b/app/src/test/java/org/mozilla/fenix/widget/VoiceSearchActivityTest.kt
@@ -7,12 +7,14 @@ package org.mozilla.fenix.widget
import android.app.Activity
import android.content.ComponentName
import android.content.Intent
+import android.content.IntentFilter
import android.os.Bundle
import android.speech.RecognizerIntent.ACTION_RECOGNIZE_SPEECH
import android.speech.RecognizerIntent.EXTRA_LANGUAGE_MODEL
import android.speech.RecognizerIntent.EXTRA_RESULTS
import android.speech.RecognizerIntent.LANGUAGE_MODEL_FREE_FORM
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
+import androidx.test.core.app.ApplicationProvider
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
@@ -21,6 +23,7 @@ import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mozilla.fenix.FenixApplication
import org.mozilla.fenix.HomeActivity.Companion.OPEN_TO_BROWSER_AND_LOAD
import org.mozilla.fenix.IntentReceiverActivity
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@@ -28,7 +31,7 @@ import org.mozilla.fenix.widget.VoiceSearchActivity.Companion.PREVIOUS_INTENT
import org.mozilla.fenix.widget.VoiceSearchActivity.Companion.SPEECH_PROCESSING
import org.mozilla.fenix.widget.VoiceSearchActivity.Companion.SPEECH_REQUEST_CODE
import org.robolectric.Robolectric
-import org.robolectric.Shadows
+import org.robolectric.Shadows.shadowOf
import org.robolectric.android.controller.ActivityController
import org.robolectric.shadows.ShadowActivity
@@ -37,7 +40,7 @@ import org.robolectric.shadows.ShadowActivity
class VoiceSearchActivityTest {
private lateinit var controller: ActivityController
- private lateinit var activity: Activity
+ private lateinit var activity: VoiceSearchActivity
private lateinit var shadow: ShadowActivity
@Before
@@ -47,21 +50,36 @@ class VoiceSearchActivityTest {
controller = Robolectric.buildActivity(VoiceSearchActivity::class.java, intent)
activity = controller.get()
- shadow = Shadows.shadowOf(activity)
+ shadow = shadowOf(activity)
+ }
+
+ private fun allowVoiceIntentToResolveActivity() {
+ val context = ApplicationProvider.getApplicationContext()
+ val shadowPackageManager = shadowOf(context.packageManager)
+ val component = ComponentName("com.test", "Test")
+ shadowPackageManager.addActivityIfNotPresent(component)
+ shadowPackageManager.addIntentFilterForActivity(
+ component,
+ IntentFilter(ACTION_RECOGNIZE_SPEECH).apply { addCategory(Intent.CATEGORY_DEFAULT) })
}
@Test
fun `process intent with speech processing set to true`() {
+ allowVoiceIntentToResolveActivity()
controller.create()
val intentForResult = shadow.peekNextStartedActivityForResult()
assertEquals(SPEECH_REQUEST_CODE, intentForResult.requestCode)
assertEquals(ACTION_RECOGNIZE_SPEECH, intentForResult.intent.action)
- assertEquals(LANGUAGE_MODEL_FREE_FORM, intentForResult.intent.getStringExtra(EXTRA_LANGUAGE_MODEL))
+ assertEquals(
+ LANGUAGE_MODEL_FREE_FORM,
+ intentForResult.intent.getStringExtra(EXTRA_LANGUAGE_MODEL)
+ )
}
@Test
fun `process intent with speech processing set to false`() {
+ allowVoiceIntentToResolveActivity()
val intent = Intent()
intent.putExtra(SPEECH_PROCESSING, false)
@@ -75,6 +93,7 @@ class VoiceSearchActivityTest {
@Test
fun `process null intent`() {
+ allowVoiceIntentToResolveActivity()
val controller = Robolectric.buildActivity(VoiceSearchActivity::class.java, null)
val activity = controller.get()
@@ -85,6 +104,7 @@ class VoiceSearchActivityTest {
@Test
fun `save previous intent to instance state`() {
+ allowVoiceIntentToResolveActivity()
val previousIntent = Intent().apply {
putExtra(SPEECH_PROCESSING, true)
}
@@ -101,6 +121,7 @@ class VoiceSearchActivityTest {
@Test
fun `process intent with speech processing in previous intent set to true`() {
+ allowVoiceIntentToResolveActivity()
val savedInstanceState = Bundle()
val previousIntent = Intent().apply {
putExtra(SPEECH_PROCESSING, true)
@@ -115,6 +136,7 @@ class VoiceSearchActivityTest {
@Test
fun `handle speech result`() {
+ allowVoiceIntentToResolveActivity()
controller.create()
val resultIntent = Intent().apply {
@@ -129,13 +151,17 @@ class VoiceSearchActivityTest {
val browserIntent = shadow.peekNextStartedActivity()
assertTrue(activity.isFinishing)
- assertEquals(ComponentName(activity, IntentReceiverActivity::class.java), browserIntent.component)
+ assertEquals(
+ ComponentName(activity, IntentReceiverActivity::class.java),
+ browserIntent.component
+ )
assertEquals("hello world", browserIntent.getStringExtra(SPEECH_PROCESSING))
assertTrue(browserIntent.getBooleanExtra(OPEN_TO_BROWSER_AND_LOAD, false))
}
@Test
fun `handle invalid result code`() {
+ allowVoiceIntentToResolveActivity()
controller.create()
val resultIntent = Intent()
@@ -147,4 +173,10 @@ class VoiceSearchActivityTest {
assertTrue(activity.isFinishing)
}
+
+ @Test
+ fun `handle no activity able to resolve voice intent`() {
+ controller.create()
+ assertTrue(activity.isFinishing)
+ }
}
diff --git a/buildSrc/src/main/java/AndroidComponents.kt b/buildSrc/src/main/java/AndroidComponents.kt
index ff43297b6..818040206 100644
--- a/buildSrc/src/main/java/AndroidComponents.kt
+++ b/buildSrc/src/main/java/AndroidComponents.kt
@@ -3,5 +3,5 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
object AndroidComponents {
- const val VERSION = "56.0.20200819190136"
+ const val VERSION = "56.0.20200821184145"
}
diff --git a/taskcluster/fenix_taskgraph/parameters.py b/taskcluster/fenix_taskgraph/parameters.py
index 11a4d9a75..1a1bbb9b3 100644
--- a/taskcluster/fenix_taskgraph/parameters.py
+++ b/taskcluster/fenix_taskgraph/parameters.py
@@ -13,7 +13,7 @@ from voluptuous import All, Any, Optional, Range, Required
BETA_SEMVER = re.compile(r'^v\d+\.\d+\.\d+-beta\.\d+$')
-PRODUCTION_SEMVER = re.compile(r'^v\d+\.\d+\.\d+(-rc\.\d+)?$')
+RELEASE_SEMVER = re.compile(r'^v\d+\.\d+\.\d+(-rc\.\d+)?$')
extend_parameters_schema({
@@ -48,8 +48,8 @@ def resolve_release_type(head_tag):
return ""
elif BETA_SEMVER.match(head_tag):
return "beta"
- elif PRODUCTION_SEMVER.match(head_tag):
- return "production"
+ elif RELEASE_SEMVER.match(head_tag):
+ return "release"
else:
raise ValueError('Github tag must be in semver format and prefixed with a "v", '
- 'e.g.: "v1.0.0-beta.0" (beta), "v1.0.0-rc.0" (production) or "v1.0.0" (production)')
+ 'e.g.: "v1.0.0-beta.0" (beta), "v1.0.0-rc.0" (release) or "v1.0.0" (release)')