Update the gradle build tasks to generate play store builds.
Configure the gradle builds to sign and build the release version of the Godot Android Editor
This commit is contained in:
parent
3943de2e6c
commit
30e3e301e0
@ -54,6 +54,9 @@ if lib_arch_dir != "":
|
|||||||
if env["target"] == "release":
|
if env["target"] == "release":
|
||||||
lib_type_dir = "release"
|
lib_type_dir = "release"
|
||||||
elif env["target"] == "release_debug":
|
elif env["target"] == "release_debug":
|
||||||
|
if env["tools"] and env["store_release"]:
|
||||||
|
lib_type_dir = "release"
|
||||||
|
else:
|
||||||
lib_type_dir = "debug"
|
lib_type_dir = "debug"
|
||||||
else: # debug
|
else: # debug
|
||||||
lib_type_dir = "dev"
|
lib_type_dir = "dev"
|
||||||
|
@ -24,6 +24,7 @@ def get_opts():
|
|||||||
("ndk_platform", 'Target platform (android-<api>, e.g. "android-19")', "android-19"),
|
("ndk_platform", 'Target platform (android-<api>, e.g. "android-19")', "android-19"),
|
||||||
EnumVariable("android_arch", "Target architecture", "armv7", ("armv7", "arm64v8", "x86", "x86_64")),
|
EnumVariable("android_arch", "Target architecture", "armv7", ("armv7", "arm64v8", "x86", "x86_64")),
|
||||||
BoolVariable("android_neon", "Enable NEON support (armv7 only)", True),
|
BoolVariable("android_neon", "Enable NEON support (armv7 only)", True),
|
||||||
|
BoolVariable("store_release", "Editor build for Google Play Store (for official builds only)", False),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -132,7 +132,7 @@ ext.generateGodotLibraryVersion = { List<String> requiredKeys ->
|
|||||||
String statusValue = map["status"]
|
String statusValue = map["status"]
|
||||||
if (statusValue == null) {
|
if (statusValue == null) {
|
||||||
statusCode = 0
|
statusCode = 0
|
||||||
} else if (statusValue.startsWith("alpha")) {
|
} else if (statusValue.startsWith("alpha") || statusValue.startsWith("dev")) {
|
||||||
statusCode = 1
|
statusCode = 1
|
||||||
} else if (statusValue.startsWith("beta")) {
|
} else if (statusValue.startsWith("beta")) {
|
||||||
statusCode = 2
|
statusCode = 2
|
||||||
|
@ -9,7 +9,7 @@ buildscript {
|
|||||||
dependencies {
|
dependencies {
|
||||||
classpath libraries.androidGradlePlugin
|
classpath libraries.androidGradlePlugin
|
||||||
classpath libraries.kotlinGradlePlugin
|
classpath libraries.kotlinGradlePlugin
|
||||||
classpath 'io.github.gradle-nexus:publish-plugin:1.1.0'
|
classpath 'io.github.gradle-nexus:publish-plugin:1.3.0'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,8 +36,11 @@ allprojects {
|
|||||||
|
|
||||||
ext {
|
ext {
|
||||||
supportedAbis = ["armv7", "arm64v8", "x86", "x86_64"]
|
supportedAbis = ["armv7", "arm64v8", "x86", "x86_64"]
|
||||||
supportedTargetsMap = [release: "release", dev: "debug", debug: "release_debug"]
|
|
||||||
supportedFlavors = ["editor", "template"]
|
supportedFlavors = ["editor", "template"]
|
||||||
|
supportedTargetsMapByFlavors = [
|
||||||
|
"editor": [release: "release_debug", dev: "debug", debug: "release_debug"],
|
||||||
|
"template": [release: "release", dev: "debug", debug: "release_debug"]
|
||||||
|
]
|
||||||
|
|
||||||
// Used by gradle to specify which architecture to build for by default when running
|
// Used by gradle to specify which architecture to build for by default when running
|
||||||
// `./gradlew build` (this command is usually used by Android Studio).
|
// `./gradlew build` (this command is usually used by Android Studio).
|
||||||
@ -49,6 +52,7 @@ ext {
|
|||||||
|
|
||||||
def rootDir = "../../.."
|
def rootDir = "../../.."
|
||||||
def binDir = "$rootDir/bin/"
|
def binDir = "$rootDir/bin/"
|
||||||
|
def androidEditorBuildsDir = "$binDir/android_editor_builds/"
|
||||||
|
|
||||||
def getSconsTaskName(String flavor, String buildType, String abi) {
|
def getSconsTaskName(String flavor, String buildType, String abi) {
|
||||||
return "compileGodotNativeLibs" + flavor.capitalize() + buildType.capitalize() + abi.capitalize()
|
return "compileGodotNativeLibs" + flavor.capitalize() + buildType.capitalize() + abi.capitalize()
|
||||||
@ -175,13 +179,7 @@ def templateExcludedBuildTask() {
|
|||||||
if (!isAndroidStudio()) {
|
if (!isAndroidStudio()) {
|
||||||
logger.lifecycle("Excluding Android studio build tasks")
|
logger.lifecycle("Excluding Android studio build tasks")
|
||||||
for (String flavor : supportedFlavors) {
|
for (String flavor : supportedFlavors) {
|
||||||
for (String buildType : supportedTargetsMap.keySet()) {
|
for (String buildType : supportedTargetsMapByFlavors[flavor].keySet()) {
|
||||||
if (buildType == "release" && flavor == "editor") {
|
|
||||||
// The editor can't be used with target=release as debugging tools are then not
|
|
||||||
// included, and it would crash on errors instead of reporting them.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String abi : selectedAbis) {
|
for (String abi : selectedAbis) {
|
||||||
excludedTasks += ":lib:" + getSconsTaskName(flavor, buildType, abi)
|
excludedTasks += ":lib:" + getSconsTaskName(flavor, buildType, abi)
|
||||||
}
|
}
|
||||||
@ -195,7 +193,7 @@ def templateBuildTasks() {
|
|||||||
def tasks = []
|
def tasks = []
|
||||||
|
|
||||||
// Only build the apks and aar files for which we have native shared libraries.
|
// Only build the apks and aar files for which we have native shared libraries.
|
||||||
for (String target : supportedTargetsMap.keySet()) {
|
for (String target : supportedTargetsMapByFlavors["template"].keySet()) {
|
||||||
File targetLibs = new File("lib/libs/" + target)
|
File targetLibs = new File("lib/libs/" + target)
|
||||||
if (targetLibs != null
|
if (targetLibs != null
|
||||||
&& targetLibs.isDirectory()
|
&& targetLibs.isDirectory()
|
||||||
@ -221,18 +219,46 @@ def isAndroidStudio() {
|
|||||||
return sysProps != null && sysProps['idea.platform.prefix'] != null
|
return sysProps != null && sysProps['idea.platform.prefix'] != null
|
||||||
}
|
}
|
||||||
|
|
||||||
task copyEditorDebugBinaryToBin(type: Copy) {
|
task copyEditorReleaseApkToBin(type: Copy) {
|
||||||
dependsOn ':editor:assembleDebug'
|
dependsOn ':editor:assembleRelease'
|
||||||
from('editor/build/outputs/apk/debug')
|
from('editor/build/outputs/apk/release')
|
||||||
into(binDir)
|
into(androidEditorBuildsDir)
|
||||||
include('android_editor.apk')
|
include('android_editor-release*.apk')
|
||||||
}
|
}
|
||||||
|
|
||||||
task copyEditorDevBinaryToBin(type: Copy) {
|
task copyEditorReleaseAabToBin(type: Copy) {
|
||||||
|
dependsOn ':editor:bundleRelease'
|
||||||
|
from('editor/build/outputs/bundle/release')
|
||||||
|
into(androidEditorBuildsDir)
|
||||||
|
include('android_editor-release*.aab')
|
||||||
|
}
|
||||||
|
|
||||||
|
task copyEditorDebugApkToBin(type: Copy) {
|
||||||
|
dependsOn ':editor:assembleDebug'
|
||||||
|
from('editor/build/outputs/apk/debug')
|
||||||
|
into(androidEditorBuildsDir)
|
||||||
|
include('android_editor-debug.apk')
|
||||||
|
}
|
||||||
|
|
||||||
|
task copyEditorDebugAabToBin(type: Copy) {
|
||||||
|
dependsOn ':editor:bundleDebug'
|
||||||
|
from('editor/build/outputs/bundle/debug')
|
||||||
|
into(androidEditorBuildsDir)
|
||||||
|
include('android_editor-debug.aab')
|
||||||
|
}
|
||||||
|
|
||||||
|
task copyEditorDevApkToBin(type: Copy) {
|
||||||
dependsOn ':editor:assembleDev'
|
dependsOn ':editor:assembleDev'
|
||||||
from('editor/build/outputs/apk/dev')
|
from('editor/build/outputs/apk/dev')
|
||||||
into(binDir)
|
into(androidEditorBuildsDir)
|
||||||
include('android_editor_dev.apk')
|
include('android_editor-dev.apk')
|
||||||
|
}
|
||||||
|
|
||||||
|
task copyEditorDevAabToBin(type: Copy) {
|
||||||
|
dependsOn ':editor:bundleDev'
|
||||||
|
from('editor/build/outputs/bundle/dev')
|
||||||
|
into(androidEditorBuildsDir)
|
||||||
|
include('android_editor-dev.aab')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -247,18 +273,14 @@ task generateGodotEditor {
|
|||||||
|
|
||||||
def tasks = []
|
def tasks = []
|
||||||
|
|
||||||
for (String target : supportedTargetsMap.keySet()) {
|
for (String target : supportedTargetsMapByFlavors["editor"].keySet()) {
|
||||||
if (target == "release") {
|
|
||||||
// The editor can't be used with target=release as debugging tools are then not
|
|
||||||
// included, and it would crash on errors instead of reporting them.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
File targetLibs = new File("lib/libs/tools/" + target)
|
File targetLibs = new File("lib/libs/tools/" + target)
|
||||||
if (targetLibs != null
|
if (targetLibs != null
|
||||||
&& targetLibs.isDirectory()
|
&& targetLibs.isDirectory()
|
||||||
&& targetLibs.listFiles() != null
|
&& targetLibs.listFiles() != null
|
||||||
&& targetLibs.listFiles().length > 0) {
|
&& targetLibs.listFiles().length > 0) {
|
||||||
tasks += "copyEditor${target.capitalize()}BinaryToBin"
|
tasks += "copyEditor${target.capitalize()}ApkToBin"
|
||||||
|
tasks += "copyEditor${target.capitalize()}AabToBin"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -306,9 +328,11 @@ task cleanGodotEditor(type: Delete) {
|
|||||||
// Delete the generated binary apks
|
// Delete the generated binary apks
|
||||||
delete("editor/build/outputs/apk")
|
delete("editor/build/outputs/apk")
|
||||||
|
|
||||||
// Delete the Godot editor apks in the Godot bin directory
|
// Delete the generated aab binaries
|
||||||
delete("$binDir/android_editor.apk")
|
delete("editor/build/outputs/bundle")
|
||||||
delete("$binDir/android_editor_dev.apk")
|
|
||||||
|
// Delete the Godot editor apks & aabs in the Godot bin directory
|
||||||
|
delete(androidEditorBuildsDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -13,22 +13,67 @@ dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
// Build number added as a suffix to the version code, and incremented for each build/upload to
|
// Retrieve the build number from the environment variable; default to 0 if none is specified.
|
||||||
// the Google Play store.
|
// The build number is added as a suffix to the version code for upload to the Google Play store.
|
||||||
// This should be reset on each stable release of Godot.
|
getEditorBuildNumber = { ->
|
||||||
editorBuildNumber = 0
|
int buildNumber = 0
|
||||||
|
String versionStatus = System.getenv("GODOT_VERSION_STATUS")
|
||||||
|
if (versionStatus != null && !versionStatus.isEmpty()) {
|
||||||
|
try {
|
||||||
|
buildNumber = Integer.parseInt(versionStatus.replaceAll("[^0-9]", ""));
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
buildNumber = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildNumber
|
||||||
|
}
|
||||||
// Value by which the Godot version code should be offset by to make room for the build number
|
// Value by which the Godot version code should be offset by to make room for the build number
|
||||||
editorBuildNumberOffset = 100
|
editorBuildNumberOffset = 100
|
||||||
|
|
||||||
|
// Return the keystore file used for signing the release build.
|
||||||
|
getGodotKeystoreFile = { ->
|
||||||
|
def keyStore = System.getenv("GODOT_ANDROID_SIGN_KEYSTORE")
|
||||||
|
if (keyStore == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return file(keyStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the key alias used for signing the release build.
|
||||||
|
getGodotKeyAlias = { ->
|
||||||
|
def kAlias = System.getenv("GODOT_ANDROID_KEYSTORE_ALIAS")
|
||||||
|
return kAlias
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the password for the key used for signing the release build.
|
||||||
|
getGodotSigningPassword = { ->
|
||||||
|
def signingPassword = System.getenv("GODOT_ANDROID_SIGN_PASSWORD")
|
||||||
|
return signingPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if the environment variables contains the configuration for signing the release
|
||||||
|
// build.
|
||||||
|
hasReleaseSigningConfigs = { ->
|
||||||
|
def keystoreFile = getGodotKeystoreFile()
|
||||||
|
def keyAlias = getGodotKeyAlias()
|
||||||
|
def signingPassword = getGodotSigningPassword()
|
||||||
|
|
||||||
|
return keystoreFile != null && keystoreFile.isFile()
|
||||||
|
&& keyAlias != null && !keyAlias.isEmpty()
|
||||||
|
&& signingPassword != null && !signingPassword.isEmpty()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def generateVersionCode() {
|
def generateVersionCode() {
|
||||||
int libraryVersionCode = getGodotLibraryVersionCode()
|
int libraryVersionCode = getGodotLibraryVersionCode()
|
||||||
return (libraryVersionCode * editorBuildNumberOffset) + editorBuildNumber
|
return (libraryVersionCode * editorBuildNumberOffset) + getEditorBuildNumber()
|
||||||
}
|
}
|
||||||
|
|
||||||
def generateVersionName() {
|
def generateVersionName() {
|
||||||
String libraryVersionName = getGodotLibraryVersionName()
|
String libraryVersionName = getGodotLibraryVersionName()
|
||||||
return libraryVersionName + ".$editorBuildNumber"
|
int buildNumber = getEditorBuildNumber()
|
||||||
|
return buildNumber == 0 ? libraryVersionName : libraryVersionName + ".$buildNumber"
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@ -45,6 +90,7 @@ android {
|
|||||||
targetSdkVersion versions.targetSdk
|
targetSdkVersion versions.targetSdk
|
||||||
|
|
||||||
missingDimensionStrategy 'products', 'editor'
|
missingDimensionStrategy 'products', 'editor'
|
||||||
|
setProperty("archivesBaseName", "android_editor")
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
@ -56,6 +102,15 @@ android {
|
|||||||
jvmTarget = versions.javaVersion
|
jvmTarget = versions.javaVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
release {
|
||||||
|
storeFile getGodotKeystoreFile()
|
||||||
|
storePassword getGodotSigningPassword()
|
||||||
|
keyAlias getGodotKeyAlias()
|
||||||
|
keyPassword getGodotSigningPassword()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
dev {
|
dev {
|
||||||
initWith debug
|
initWith debug
|
||||||
@ -65,14 +120,14 @@ android {
|
|||||||
debug {
|
debug {
|
||||||
initWith release
|
initWith release
|
||||||
|
|
||||||
// Need to swap with the release signing config when this is ready for public release.
|
applicationIdSuffix ".debug"
|
||||||
signingConfig signingConfigs.debug
|
signingConfig signingConfigs.debug
|
||||||
}
|
}
|
||||||
|
|
||||||
release {
|
release {
|
||||||
// This buildtype is disabled below.
|
if (hasReleaseSigningConfigs()) {
|
||||||
// The editor can't be used with target=release only, as debugging tools are then not
|
signingConfig signingConfigs.release
|
||||||
// included, and it would crash on errors instead of reporting them.
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,20 +137,4 @@ android {
|
|||||||
doNotStrip '**/*.so'
|
doNotStrip '**/*.so'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable 'release' buildtype.
|
|
||||||
// The editor can't be used with target=release only, as debugging tools are then not
|
|
||||||
// included, and it would crash on errors instead of reporting them.
|
|
||||||
variantFilter { variant ->
|
|
||||||
if (variant.buildType.name == "release") {
|
|
||||||
setIgnore(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
applicationVariants.all { variant ->
|
|
||||||
variant.outputs.all { output ->
|
|
||||||
def suffix = variant.name == "dev" ? "_dev" : ""
|
|
||||||
output.outputFileName = "android_editor${suffix}.apk"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="godot_editor_name_string">Godot Editor 3 (debug)</string>
|
||||||
|
</resources>
|
@ -80,19 +80,11 @@ android {
|
|||||||
release.jniLibs.srcDirs = ['libs/release']
|
release.jniLibs.srcDirs = ['libs/release']
|
||||||
|
|
||||||
// Editor jni library
|
// Editor jni library
|
||||||
|
editorRelease.jniLibs.srcDirs = ['libs/tools/release']
|
||||||
editorDebug.jniLibs.srcDirs = ['libs/tools/debug']
|
editorDebug.jniLibs.srcDirs = ['libs/tools/debug']
|
||||||
editorDev.jniLibs.srcDirs = ['libs/tools/dev']
|
editorDev.jniLibs.srcDirs = ['libs/tools/dev']
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable 'editorRelease'.
|
|
||||||
// The editor can't be used with target=release as debugging tools are then not
|
|
||||||
// included, and it would crash on errors instead of reporting them.
|
|
||||||
variantFilter { variant ->
|
|
||||||
if (variant.name == "editorRelease") {
|
|
||||||
setIgnore(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
libraryVariants.all { variant ->
|
libraryVariants.all { variant ->
|
||||||
def flavorName = variant.getFlavorName()
|
def flavorName = variant.getFlavorName()
|
||||||
if (flavorName == null || flavorName == "") {
|
if (flavorName == null || flavorName == "") {
|
||||||
@ -102,11 +94,14 @@ android {
|
|||||||
boolean toolsFlag = flavorName == "editor"
|
boolean toolsFlag = flavorName == "editor"
|
||||||
|
|
||||||
def buildType = variant.buildType.name
|
def buildType = variant.buildType.name
|
||||||
if (buildType == null || buildType == "" || !supportedTargetsMap.containsKey(buildType)) {
|
if (buildType == null || buildType == "" || !supportedTargetsMapByFlavors[flavorName].containsKey(buildType)) {
|
||||||
throw new GradleException("Invalid build type: $buildType")
|
throw new GradleException("Invalid build type: $buildType")
|
||||||
}
|
}
|
||||||
|
|
||||||
def sconsTarget = supportedTargetsMap[buildType]
|
boolean productionBuild = buildType != "dev"
|
||||||
|
boolean storeRelease = buildType == "release"
|
||||||
|
|
||||||
|
def sconsTarget = supportedTargetsMapByFlavors[flavorName][buildType]
|
||||||
if (sconsTarget == null || sconsTarget == "") {
|
if (sconsTarget == null || sconsTarget == "") {
|
||||||
throw new GradleException("Invalid scons target: $sconsTarget")
|
throw new GradleException("Invalid scons target: $sconsTarget")
|
||||||
}
|
}
|
||||||
@ -126,10 +121,10 @@ android {
|
|||||||
def sconsExts = (org.gradle.internal.os.OperatingSystem.current().isWindows()
|
def sconsExts = (org.gradle.internal.os.OperatingSystem.current().isWindows()
|
||||||
? [".bat", ".cmd", ".ps1", ".exe"]
|
? [".bat", ".cmd", ".ps1", ".exe"]
|
||||||
: [""])
|
: [""])
|
||||||
logger.lifecycle("Looking for $sconsName executable path")
|
logger.debug("Looking for $sconsName executable path")
|
||||||
for (ext in sconsExts) {
|
for (ext in sconsExts) {
|
||||||
String sconsNameExt = sconsName + ext
|
String sconsNameExt = sconsName + ext
|
||||||
logger.lifecycle("Checking $sconsNameExt")
|
logger.debug("Checking $sconsNameExt")
|
||||||
|
|
||||||
sconsExecutableFile = org.gradle.internal.os.OperatingSystem.current().findInPath(sconsNameExt)
|
sconsExecutableFile = org.gradle.internal.os.OperatingSystem.current().findInPath(sconsNameExt)
|
||||||
if (sconsExecutableFile != null) {
|
if (sconsExecutableFile != null) {
|
||||||
@ -149,7 +144,7 @@ android {
|
|||||||
if (sconsExecutableFile == null) {
|
if (sconsExecutableFile == null) {
|
||||||
throw new GradleException("Unable to find executable path for the '$sconsName' command.")
|
throw new GradleException("Unable to find executable path for the '$sconsName' command.")
|
||||||
} else {
|
} else {
|
||||||
logger.lifecycle("Found executable path for $sconsName: ${sconsExecutableFile.absolutePath}")
|
logger.debug("Found executable path for $sconsName: ${sconsExecutableFile.absolutePath}")
|
||||||
}
|
}
|
||||||
|
|
||||||
for (String selectedAbi : selectedAbis) {
|
for (String selectedAbi : selectedAbis) {
|
||||||
@ -161,7 +156,7 @@ android {
|
|||||||
def taskName = getSconsTaskName(flavorName, buildType, selectedAbi)
|
def taskName = getSconsTaskName(flavorName, buildType, selectedAbi)
|
||||||
tasks.create(name: taskName, type: Exec) {
|
tasks.create(name: taskName, type: Exec) {
|
||||||
executable sconsExecutableFile.absolutePath
|
executable sconsExecutableFile.absolutePath
|
||||||
args "--directory=${pathToRootDir}", "platform=android", "tools=${toolsFlag}", "target=${sconsTarget}", "android_arch=${selectedAbi}", "-j" + Runtime.runtime.availableProcessors()
|
args "--directory=${pathToRootDir}", "platform=android", "store_release=${storeRelease}", "production=${productionBuild}", "tools=${toolsFlag}", "target=${sconsTarget}", "android_arch=${selectedAbi}", "-j" + Runtime.runtime.availableProcessors()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schedule the tasks so the generated libs are present before the aar file is packaged.
|
// Schedule the tasks so the generated libs are present before the aar file is packaged.
|
||||||
|
Loading…
Reference in New Issue
Block a user