diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java
deleted file mode 100644
index 1708393..0000000
--- a/.mvn/wrapper/MavenWrapperDownloader.java
+++ /dev/null
@@ -1,142 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import java.net.*;
-import java.io.*;
-import java.nio.channels.*;
-import java.util.Properties;
-
-public class MavenWrapperDownloader
-{
- private static final String WRAPPER_VERSION = "3.1.1";
-
- /**
- * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
- */
- private static final String DEFAULT_DOWNLOAD_URL =
- "https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/" + WRAPPER_VERSION
- + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
-
- /**
- * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to use instead of the
- * default one.
- */
- private static final String MAVEN_WRAPPER_PROPERTIES_PATH = ".mvn/wrapper/maven-wrapper.properties";
-
- /**
- * Path where the maven-wrapper.jar will be saved to.
- */
- private static final String MAVEN_WRAPPER_JAR_PATH = ".mvn/wrapper/maven-wrapper.jar";
-
- /**
- * Name of the property which should be used to override the default download url for the wrapper.
- */
- private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
-
- public static void main( String args[] )
- {
- System.out.println( "- Downloader started" );
- File baseDirectory = new File( args[0] );
- System.out.println( "- Using base directory: " + baseDirectory.getAbsolutePath() );
-
- // If the maven-wrapper.properties exists, read it and check if it contains a custom
- // wrapperUrl parameter.
- File mavenWrapperPropertyFile = new File( baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH );
- String url = DEFAULT_DOWNLOAD_URL;
- if ( mavenWrapperPropertyFile.exists() )
- {
- FileInputStream mavenWrapperPropertyFileInputStream = null;
- try
- {
- mavenWrapperPropertyFileInputStream = new FileInputStream( mavenWrapperPropertyFile );
- Properties mavenWrapperProperties = new Properties();
- mavenWrapperProperties.load( mavenWrapperPropertyFileInputStream );
- url = mavenWrapperProperties.getProperty( PROPERTY_NAME_WRAPPER_URL, url );
- }
- catch ( IOException e )
- {
- System.out.println( "- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'" );
- }
- finally
- {
- try
- {
- if ( mavenWrapperPropertyFileInputStream != null )
- {
- mavenWrapperPropertyFileInputStream.close();
- }
- }
- catch ( IOException e )
- {
- // Ignore ...
- }
- }
- }
- System.out.println( "- Downloading from: " + url );
-
- File outputFile = new File( baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH );
- if ( !outputFile.getParentFile().exists() )
- {
- if ( !outputFile.getParentFile().mkdirs() )
- {
- System.out.println( "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath()
- + "'" );
- }
- }
- System.out.println( "- Downloading to: " + outputFile.getAbsolutePath() );
- try
- {
- downloadFileFromURL( url, outputFile );
- System.out.println( "Done" );
- System.exit( 0 );
- }
- catch ( Throwable e )
- {
- System.out.println( "- Error downloading" );
- e.printStackTrace();
- System.exit( 1 );
- }
- }
-
- private static void downloadFileFromURL( String urlString, File destination )
- throws Exception
- {
- if ( System.getenv( "MVNW_USERNAME" ) != null && System.getenv( "MVNW_PASSWORD" ) != null )
- {
- String username = System.getenv( "MVNW_USERNAME" );
- char[] password = System.getenv( "MVNW_PASSWORD" ).toCharArray();
- Authenticator.setDefault( new Authenticator()
- {
- @Override
- protected PasswordAuthentication getPasswordAuthentication()
- {
- return new PasswordAuthentication( username, password );
- }
- } );
- }
- URL website = new URL( urlString );
- ReadableByteChannel rbc;
- rbc = Channels.newChannel( website.openStream() );
- FileOutputStream fos = new FileOutputStream( destination );
- fos.getChannel().transferFrom( rbc, 0, Long.MAX_VALUE );
- fos.close();
- rbc.close();
- }
-
-}
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
index 61a2ef1..346d645 100644
--- a/.mvn/wrapper/maven-wrapper.properties
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -14,5 +14,5 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip
-wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
diff --git a/mvnw b/mvnw
index eaa3d30..8d937f4 100755
--- a/mvnw
+++ b/mvnw
@@ -8,7 +8,7 @@
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
-# https://www.apache.org/licenses/LICENSE-2.0
+# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
@@ -19,7 +19,7 @@
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
-# Maven Start Up Batch script
+# Apache Maven Wrapper startup batch script, version 3.2.0
#
# Required ENV vars:
# ------------------
@@ -27,7 +27,6 @@
#
# Optional ENV vars
# -----------------
-# M2_HOME - location of maven2's installed home dir
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
# e.g. to debug Maven itself, use
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
@@ -54,7 +53,7 @@ fi
cygwin=false;
darwin=false;
mingw=false
-case "`uname`" in
+case "$(uname)" in
CYGWIN*) cygwin=true ;;
MINGW*) mingw=true;;
Darwin*) darwin=true
@@ -62,9 +61,9 @@ case "`uname`" in
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
if [ -z "$JAVA_HOME" ]; then
if [ -x "/usr/libexec/java_home" ]; then
- export JAVA_HOME="`/usr/libexec/java_home`"
+ JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
else
- export JAVA_HOME="/Library/Java/Home"
+ JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
fi
fi
;;
@@ -72,68 +71,38 @@ esac
if [ -z "$JAVA_HOME" ] ; then
if [ -r /etc/gentoo-release ] ; then
- JAVA_HOME=`java-config --jre-home`
+ JAVA_HOME=$(java-config --jre-home)
fi
fi
-if [ -z "$M2_HOME" ] ; then
- ## resolve links - $0 may be a link to maven's home
- PRG="$0"
-
- # need this for relative symlinks
- while [ -h "$PRG" ] ; do
- ls=`ls -ld "$PRG"`
- link=`expr "$ls" : '.*-> \(.*\)$'`
- if expr "$link" : '/.*' > /dev/null; then
- PRG="$link"
- else
- PRG="`dirname "$PRG"`/$link"
- fi
- done
-
- saveddir=`pwd`
-
- M2_HOME=`dirname "$PRG"`/..
-
- # make it fully qualified
- M2_HOME=`cd "$M2_HOME" && pwd`
-
- cd "$saveddir"
- # echo Using m2 at $M2_HOME
-fi
-
# For Cygwin, ensure paths are in UNIX format before anything is touched
if $cygwin ; then
- [ -n "$M2_HOME" ] &&
- M2_HOME=`cygpath --unix "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
- JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+ JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
[ -n "$CLASSPATH" ] &&
- CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
+ CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
fi
# For Mingw, ensure paths are in UNIX format before anything is touched
if $mingw ; then
- [ -n "$M2_HOME" ] &&
- M2_HOME="`(cd "$M2_HOME"; pwd)`"
- [ -n "$JAVA_HOME" ] &&
- JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
+ [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
+ JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
fi
if [ -z "$JAVA_HOME" ]; then
- javaExecutable="`which javac`"
- if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
+ javaExecutable="$(which javac)"
+ if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
# readlink(1) is not available as standard on Solaris 10.
- readLink=`which readlink`
- if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
+ readLink=$(which readlink)
+ if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
if $darwin ; then
- javaHome="`dirname \"$javaExecutable\"`"
- javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
+ javaHome="$(dirname "\"$javaExecutable\"")"
+ javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
else
- javaExecutable="`readlink -f \"$javaExecutable\"`"
+ javaExecutable="$(readlink -f "\"$javaExecutable\"")"
fi
- javaHome="`dirname \"$javaExecutable\"`"
- javaHome=`expr "$javaHome" : '\(.*\)/bin'`
+ javaHome="$(dirname "\"$javaExecutable\"")"
+ javaHome=$(expr "$javaHome" : '\(.*\)/bin')
JAVA_HOME="$javaHome"
export JAVA_HOME
fi
@@ -149,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then
JAVACMD="$JAVA_HOME/bin/java"
fi
else
- JAVACMD="`\\unset -f command; \\command -v java`"
+ JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
fi
fi
@@ -163,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then
echo "Warning: JAVA_HOME environment variable is not set."
fi
-CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
-
# traverses directory structure from process work directory to filesystem root
# first directory with .mvn subdirectory is considered project base directory
find_maven_basedir() {
-
if [ -z "$1" ]
then
echo "Path not specified to find_maven_basedir"
@@ -184,96 +150,99 @@ find_maven_basedir() {
fi
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
if [ -d "${wdir}" ]; then
- wdir=`cd "$wdir/.."; pwd`
+ wdir=$(cd "$wdir/.." || exit 1; pwd)
fi
# end of workaround
done
- echo "${basedir}"
+ printf '%s' "$(cd "$basedir" || exit 1; pwd)"
}
# concatenates all lines of a file
concat_lines() {
if [ -f "$1" ]; then
- echo "$(tr -s '\n' ' ' < "$1")"
+ # Remove \r in case we run on Windows within Git Bash
+ # and check out the repository with auto CRLF management
+ # enabled. Otherwise, we may read lines that are delimited with
+ # \r\n and produce $'-Xarg\r' rather than -Xarg due to word
+ # splitting rules.
+ tr -s '\r\n' ' ' < "$1"
+ fi
+}
+
+log() {
+ if [ "$MVNW_VERBOSE" = true ]; then
+ printf '%s\n' "$1"
fi
}
-BASE_DIR=`find_maven_basedir "$(pwd)"`
+BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
if [ -z "$BASE_DIR" ]; then
exit 1;
fi
+MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
+log "$MAVEN_PROJECTBASEDIR"
+
##########################################################################################
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
# This allows using the maven wrapper in projects that prohibit checking in binary data.
##########################################################################################
-if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
- if [ "$MVNW_VERBOSE" = true ]; then
- echo "Found .mvn/wrapper/maven-wrapper.jar"
- fi
+wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
+if [ -r "$wrapperJarPath" ]; then
+ log "Found $wrapperJarPath"
else
- if [ "$MVNW_VERBOSE" = true ]; then
- echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
- fi
+ log "Couldn't find $wrapperJarPath, downloading it ..."
+
if [ -n "$MVNW_REPOURL" ]; then
- jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar"
+ wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
else
- jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar"
+ wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
fi
- while IFS="=" read key value; do
- case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
+ while IFS="=" read -r key value; do
+ # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
+ safeValue=$(echo "$value" | tr -d '\r')
+ case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
esac
- done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
- if [ "$MVNW_VERBOSE" = true ]; then
- echo "Downloading from: $jarUrl"
- fi
- wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
+ done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
+ log "Downloading from: $wrapperUrl"
+
if $cygwin; then
- wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
+ wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
fi
if command -v wget > /dev/null; then
- if [ "$MVNW_VERBOSE" = true ]; then
- echo "Found wget ... using wget"
- fi
+ log "Found wget ... using wget"
+ [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
- wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+ wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
else
- wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+ wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
fi
elif command -v curl > /dev/null; then
- if [ "$MVNW_VERBOSE" = true ]; then
- echo "Found curl ... using curl"
- fi
+ log "Found curl ... using curl"
+ [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
- curl -o "$wrapperJarPath" "$jarUrl" -f
+ curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
else
- curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
+ curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
fi
-
else
- if [ "$MVNW_VERBOSE" = true ]; then
- echo "Falling back to using Java to download"
- fi
- javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
+ log "Falling back to using Java to download"
+ javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
+ javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
# For Cygwin, switch paths to Windows format before running javac
if $cygwin; then
- javaClass=`cygpath --path --windows "$javaClass"`
+ javaSource=$(cygpath --path --windows "$javaSource")
+ javaClass=$(cygpath --path --windows "$javaClass")
fi
- if [ -e "$javaClass" ]; then
- if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
- if [ "$MVNW_VERBOSE" = true ]; then
- echo " - Compiling MavenWrapperDownloader.java ..."
- fi
- # Compiling the Java class
- ("$JAVA_HOME/bin/javac" "$javaClass")
+ if [ -e "$javaSource" ]; then
+ if [ ! -e "$javaClass" ]; then
+ log " - Compiling MavenWrapperDownloader.java ..."
+ ("$JAVA_HOME/bin/javac" "$javaSource")
fi
- if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
- # Running the downloader
- if [ "$MVNW_VERBOSE" = true ]; then
- echo " - Running MavenWrapperDownloader.java ..."
- fi
- ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
+ if [ -e "$javaClass" ]; then
+ log " - Running MavenWrapperDownloader.java ..."
+ ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
fi
fi
fi
@@ -282,35 +251,58 @@ fi
# End of extension
##########################################################################################
-export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
-if [ "$MVNW_VERBOSE" = true ]; then
- echo $MAVEN_PROJECTBASEDIR
+# If specified, validate the SHA-256 sum of the Maven wrapper jar file
+wrapperSha256Sum=""
+while IFS="=" read -r key value; do
+ case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
+ esac
+done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
+if [ -n "$wrapperSha256Sum" ]; then
+ wrapperSha256Result=false
+ if command -v sha256sum > /dev/null; then
+ if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then
+ wrapperSha256Result=true
+ fi
+ elif command -v shasum > /dev/null; then
+ if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
+ wrapperSha256Result=true
+ fi
+ else
+ echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
+ echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
+ exit 1
+ fi
+ if [ $wrapperSha256Result = false ]; then
+ echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
+ echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
+ echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
+ exit 1
+ fi
fi
+
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
# For Cygwin, switch paths to Windows format before running java
if $cygwin; then
- [ -n "$M2_HOME" ] &&
- M2_HOME=`cygpath --path --windows "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
- JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+ JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
[ -n "$CLASSPATH" ] &&
- CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
+ CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
- MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
+ MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
fi
# Provide a "standardized" way to retrieve the CLI args that will
# work with both Windows and non-Windows executions.
-MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
export MAVEN_CMD_LINE_ARGS
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+# shellcheck disable=SC2086 # safe args
exec "$JAVACMD" \
$MAVEN_OPTS \
$MAVEN_DEBUG_OPTS \
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
- "-Dmaven.home=${M2_HOME}" \
"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/mvnw.cmd b/mvnw.cmd
index abb7c32..f80fbad 100644
--- a/mvnw.cmd
+++ b/mvnw.cmd
@@ -1,188 +1,205 @@
-@REM ----------------------------------------------------------------------------
-@REM Licensed to the Apache Software Foundation (ASF) under one
-@REM or more contributor license agreements. See the NOTICE file
-@REM distributed with this work for additional information
-@REM regarding copyright ownership. The ASF licenses this file
-@REM to you under the Apache License, Version 2.0 (the
-@REM "License"); you may not use this file except in compliance
-@REM with the License. You may obtain a copy of the License at
-@REM
-@REM https://www.apache.org/licenses/LICENSE-2.0
-@REM
-@REM Unless required by applicable law or agreed to in writing,
-@REM software distributed under the License is distributed on an
-@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-@REM KIND, either express or implied. See the License for the
-@REM specific language governing permissions and limitations
-@REM under the License.
-@REM ----------------------------------------------------------------------------
-
-@REM ----------------------------------------------------------------------------
-@REM Maven Start Up Batch script
-@REM
-@REM Required ENV vars:
-@REM JAVA_HOME - location of a JDK home dir
-@REM
-@REM Optional ENV vars
-@REM M2_HOME - location of maven2's installed home dir
-@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
-@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
-@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
-@REM e.g. to debug Maven itself, use
-@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
-@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
-@REM ----------------------------------------------------------------------------
-
-@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
-@echo off
-@REM set title of command window
-title %0
-@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
-@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
-
-@REM set %HOME% to equivalent of $HOME
-if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
-
-@REM Execute a user defined script before this one
-if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
-@REM check for pre script, once with legacy .bat ending and once with .cmd ending
-if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
-if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
-:skipRcPre
-
-@setlocal
-
-set ERROR_CODE=0
-
-@REM To isolate internal variables from possible post scripts, we use another setlocal
-@setlocal
-
-@REM ==== START VALIDATION ====
-if not "%JAVA_HOME%" == "" goto OkJHome
-
-echo.
-echo Error: JAVA_HOME not found in your environment. >&2
-echo Please set the JAVA_HOME variable in your environment to match the >&2
-echo location of your Java installation. >&2
-echo.
-goto error
-
-:OkJHome
-if exist "%JAVA_HOME%\bin\java.exe" goto init
-
-echo.
-echo Error: JAVA_HOME is set to an invalid directory. >&2
-echo JAVA_HOME = "%JAVA_HOME%" >&2
-echo Please set the JAVA_HOME variable in your environment to match the >&2
-echo location of your Java installation. >&2
-echo.
-goto error
-
-@REM ==== END VALIDATION ====
-
-:init
-
-@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
-@REM Fallback to current working directory if not found.
-
-set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
-IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
-
-set EXEC_DIR=%CD%
-set WDIR=%EXEC_DIR%
-:findBaseDir
-IF EXIST "%WDIR%"\.mvn goto baseDirFound
-cd ..
-IF "%WDIR%"=="%CD%" goto baseDirNotFound
-set WDIR=%CD%
-goto findBaseDir
-
-:baseDirFound
-set MAVEN_PROJECTBASEDIR=%WDIR%
-cd "%EXEC_DIR%"
-goto endDetectBaseDir
-
-:baseDirNotFound
-set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
-cd "%EXEC_DIR%"
-
-:endDetectBaseDir
-
-IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
-
-@setlocal EnableExtensions EnableDelayedExpansion
-for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
-@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
-
-:endReadAdditionalConfig
-
-SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
-set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
-set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
-
-set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar"
-
-FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
- IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
-)
-
-@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
-@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
-if exist %WRAPPER_JAR% (
- if "%MVNW_VERBOSE%" == "true" (
- echo Found %WRAPPER_JAR%
- )
-) else (
- if not "%MVNW_REPOURL%" == "" (
- SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar"
- )
- if "%MVNW_VERBOSE%" == "true" (
- echo Couldn't find %WRAPPER_JAR%, downloading it ...
- echo Downloading from: %DOWNLOAD_URL%
- )
-
- powershell -Command "&{"^
- "$webclient = new-object System.Net.WebClient;"^
- "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
- "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
- "}"^
- "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
- "}"
- if "%MVNW_VERBOSE%" == "true" (
- echo Finished downloading %WRAPPER_JAR%
- )
-)
-@REM End of extension
-
-@REM Provide a "standardized" way to retrieve the CLI args that will
-@REM work with both Windows and non-Windows executions.
-set MAVEN_CMD_LINE_ARGS=%*
-
-%MAVEN_JAVA_EXE% ^
- %JVM_CONFIG_MAVEN_PROPS% ^
- %MAVEN_OPTS% ^
- %MAVEN_DEBUG_OPTS% ^
- -classpath %WRAPPER_JAR% ^
- "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
- %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
-if ERRORLEVEL 1 goto error
-goto end
-
-:error
-set ERROR_CODE=1
-
-:end
-@endlocal & set ERROR_CODE=%ERROR_CODE%
-
-if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
-@REM check for post script, once with legacy .bat ending and once with .cmd ending
-if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
-if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
-:skipRcPost
-
-@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
-if "%MAVEN_BATCH_PAUSE%"=="on" pause
-
-if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
-
-cmd /C exit /B %ERROR_CODE%
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Apache Maven Wrapper startup batch script, version 3.2.0
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
+if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+
+FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Found %WRAPPER_JAR%
+ )
+) else (
+ if not "%MVNW_REPOURL%" == "" (
+ SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ )
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Couldn't find %WRAPPER_JAR%, downloading it ...
+ echo Downloading from: %WRAPPER_URL%
+ )
+
+ powershell -Command "&{"^
+ "$webclient = new-object System.Net.WebClient;"^
+ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+ "}"^
+ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
+ "}"
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Finished downloading %WRAPPER_JAR%
+ )
+)
+@REM End of extension
+
+@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
+SET WRAPPER_SHA_256_SUM=""
+FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
+)
+IF NOT %WRAPPER_SHA_256_SUM%=="" (
+ powershell -Command "&{"^
+ "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
+ "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
+ " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
+ " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
+ " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
+ " exit 1;"^
+ "}"^
+ "}"
+ if ERRORLEVEL 1 goto error
+)
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% ^
+ %JVM_CONFIG_MAVEN_PROPS% ^
+ %MAVEN_OPTS% ^
+ %MAVEN_DEBUG_OPTS% ^
+ -classpath %WRAPPER_JAR% ^
+ "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
+ %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
+if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%"=="on" pause
+
+if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
+
+cmd /C exit /B %ERROR_CODE%
diff --git a/pom.xml b/pom.xml
index 3124868..ba8f899 100644
--- a/pom.xml
+++ b/pom.xml
@@ -8,16 +8,22 @@
1.0.0-SNAPSHOT
3.8.1
- 11
+ 17
+ 17
+ 17
UTF-8
UTF-8
quarkus-bom
io.quarkus.platform
- 2.13.3.Final
+ 3.7.3
true
- 3.0.0-M7
- 3.0.0
- 2.13.3.Final
+ 3.2.5
+ 3.2.0
+ 3.7.3
+ 2.1.0
+ 2.15.1
+ 2.2
+ 5.10.0
@@ -31,16 +37,16 @@
+
+ org.kohsuke
+ github-api
+ 1.319
+
io.quarkiverse.githubapp
quarkus-github-app
- 1.14.0
+ ${quarkus-github-app.version}
-
-
-
-
-
io.quarkus
quarkus-openshift
@@ -49,10 +55,14 @@
io.quarkus
quarkus-arc
+
+ io.quarkus
+ quarkus-scheduler
+
commons-io
commons-io
- 2.11.0
+ ${commons-io.version}
io.quarkus
@@ -62,13 +72,13 @@
org.hamcrest
hamcrest
- 2.2
+ ${hamcrest.version}
test
org.mockito
mockito-core
- 4.10.0
+ ${mockito-core.version}
test
diff --git a/src/main/java/org/keycloak/gh/bot/AddRegressionLabelToBugs.java b/src/main/java/org/keycloak/gh/bot/AddRegressionLabelToBugs.java
new file mode 100644
index 0000000..e77d2e3
--- /dev/null
+++ b/src/main/java/org/keycloak/gh/bot/AddRegressionLabelToBugs.java
@@ -0,0 +1,28 @@
+package org.keycloak.gh.bot;
+
+import io.quarkiverse.githubapp.event.Issue;
+import org.keycloak.gh.bot.labels.Kind;
+import org.keycloak.gh.bot.utils.IssueParser;
+import org.keycloak.gh.bot.utils.Labels;
+import org.kohsuke.github.GHEventPayload;
+import org.kohsuke.github.GHIssue;
+
+import java.io.IOException;
+
+/**
+ * Adds 'area/...' label to bugs by using the area selected by the reporter in the area dropdown on the bug issue form.
+ */
+public class AddRegressionLabelToBugs {
+
+ void onOpen(@Issue.Opened GHEventPayload.Issue issuePayload) throws IOException {
+ GHIssue issue = issuePayload.getIssue();
+
+ if (Labels.hasLabel(issue, Labels.KIND_BUG)) {
+ boolean regression = IssueParser.isRegression(issuePayload.getIssue().getBody());
+ if (regression) {
+ issue.addLabels(Kind.REGRESSION.toLabel());
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/org/keycloak/gh/bot/AddTriageToReopenedBugs.java b/src/main/java/org/keycloak/gh/bot/AddTriageToReopenedBugs.java
new file mode 100644
index 0000000..9159320
--- /dev/null
+++ b/src/main/java/org/keycloak/gh/bot/AddTriageToReopenedBugs.java
@@ -0,0 +1,24 @@
+package org.keycloak.gh.bot;
+
+import io.quarkiverse.githubapp.event.Issue;
+import org.keycloak.gh.bot.labels.Kind;
+import org.keycloak.gh.bot.labels.Status;
+import org.kohsuke.github.GHEventPayload;
+import org.kohsuke.github.GHIssue;
+import org.kohsuke.github.GHLabel;
+
+import java.io.IOException;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class AddTriageToReopenedBugs {
+
+ void onEdit(@Issue.Reopened GHEventPayload.Issue payload) throws IOException {
+ GHIssue issue = payload.getIssue();
+ Set labels = payload.getIssue().getLabels().stream().map(GHLabel::getName).collect(Collectors.toSet());
+ if (labels.contains(Kind.BUG.toLabel())) {
+ issue.addLabels(Status.TRIAGE.toLabel(), Status.REOPENED.toLabel());
+ }
+ }
+
+}
diff --git a/src/main/java/org/keycloak/gh/bot/BugActionMessages.java b/src/main/java/org/keycloak/gh/bot/BugActionMessages.java
new file mode 100644
index 0000000..d3e79eb
--- /dev/null
+++ b/src/main/java/org/keycloak/gh/bot/BugActionMessages.java
@@ -0,0 +1,36 @@
+package org.keycloak.gh.bot;
+
+import io.quarkus.runtime.Startup;
+import jakarta.inject.Singleton;
+import org.keycloak.gh.bot.labels.Action;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.Locale;
+import java.util.Properties;
+import java.util.concurrent.TimeUnit;
+
+@Singleton
+@Startup
+public class BugActionMessages {
+
+ Properties properties;
+
+ public BugActionMessages() {
+ properties = new Properties();
+ try {
+ properties.load(BugActions.class.getResourceAsStream("bug-action-messages.properties"));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public String getBugActionComment(Action action) {
+ return properties.getProperty("action-" + action.name());
+ }
+
+ public String getExpireComment(long value, TimeUnit timeUnit) {
+ return MessageFormat.format(properties.getProperty("expired"), value, timeUnit.name().toLowerCase(Locale.ENGLISH));
+ }
+
+}
diff --git a/src/main/java/org/keycloak/gh/bot/BugActionRetriageMissingInfo.java b/src/main/java/org/keycloak/gh/bot/BugActionRetriageMissingInfo.java
new file mode 100644
index 0000000..bb22272
--- /dev/null
+++ b/src/main/java/org/keycloak/gh/bot/BugActionRetriageMissingInfo.java
@@ -0,0 +1,40 @@
+package org.keycloak.gh.bot;
+
+import io.quarkiverse.githubapp.event.Issue;
+import io.quarkiverse.githubapp.event.IssueComment;
+import org.jboss.logging.Logger;
+import org.keycloak.gh.bot.labels.Status;
+import org.kohsuke.github.GHEventPayload;
+import org.kohsuke.github.GHIssue;
+import org.kohsuke.github.GHIssueState;
+import org.kohsuke.github.GHLabel;
+import org.kohsuke.github.GHUser;
+
+import java.io.IOException;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class BugActionRetriageMissingInfo {
+
+ private static final Logger logger = Logger.getLogger(BugActionRetriageMissingInfo.class);
+
+ void onEdit(@Issue.Edited GHEventPayload.Issue payload) throws IOException {
+ check(payload.getIssue(), payload.getSender());
+ }
+
+ void onComment(@IssueComment.Created GHEventPayload.IssueComment payload) throws IOException {
+ check(payload.getIssue(), payload.getSender());
+ }
+
+ void check(GHIssue issue, GHUser sender) throws IOException {
+ if (issue.getState().equals(GHIssueState.OPEN) && sender.getLogin().equals(issue.getUser().getLogin())) {
+ Set labels = issue.getLabels().stream().map(GHLabel::getName).collect(Collectors.toSet());
+ if (labels.contains(Status.MISSING_INFORMATION.toLabel())) {
+ issue.addLabels(Status.TRIAGE.toLabel());
+ issue.removeLabels(Status.MISSING_INFORMATION.toLabel(), Status.AUTO_EXPIRE.toLabel());
+ logger.infov("Moving back to triage: issue={0}", issue.getNumber());
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/org/keycloak/gh/bot/BugActionScheduleAutoBump.java b/src/main/java/org/keycloak/gh/bot/BugActionScheduleAutoBump.java
new file mode 100644
index 0000000..6043005
--- /dev/null
+++ b/src/main/java/org/keycloak/gh/bot/BugActionScheduleAutoBump.java
@@ -0,0 +1,76 @@
+package org.keycloak.gh.bot;
+
+import io.quarkus.runtime.Startup;
+import io.quarkus.scheduler.Scheduled;
+import jakarta.annotation.PostConstruct;
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.jboss.logging.Logger;
+import org.keycloak.gh.bot.labels.Kind;
+import org.keycloak.gh.bot.labels.Priority;
+import org.keycloak.gh.bot.labels.Status;
+import org.kohsuke.github.GHIssue;
+import org.kohsuke.github.GitHub;
+import org.kohsuke.github.PagedIterator;
+
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.List;
+
+@Startup
+@Singleton
+public class BugActionScheduleAutoBump {
+
+ private static final Logger logger = Logger.getLogger(BugActionScheduleAutoBump.class);
+
+ @ConfigProperty(name = "autoBump.low.reactions")
+ int bumpLowReactions;
+ @ConfigProperty(name = "autoBump.normal.reactions")
+ int bumpNormalReactions;
+
+ @Inject
+ GitHubInstallationProvider gitHubProvider;
+
+ @Inject
+ BugActionMessages messages;
+
+ String rootQuery;
+
+ @PostConstruct
+ public void init() {
+ rootQuery = "repo:" + gitHubProvider.getRepositoryFullName() + " is:issue is:open label:" + Kind.BUG + " label:" + Status.AUTO_EXPIRE;
+ }
+
+ @Scheduled(cron = "{autoBump.cron}", concurrentExecution = Scheduled.ConcurrentExecution.SKIP)
+ public void checkIssuesWithLowAndNormalPriority() throws IOException {
+ logger.info("Checking issues with auto-bump");
+ GitHub gitHub = gitHubProvider.getGitHub();
+
+ bump(gitHub, Priority.LOW, bumpLowReactions, Priority.NORMAL);
+ bump(gitHub, Priority.NORMAL, bumpNormalReactions, Priority.IMPORTANT);
+ }
+
+ private void bump(GitHub gitHub, Priority currentPriority, int bumpReactions, Priority newPriority) throws IOException {
+ String query = rootQuery + " label:" + currentPriority + " reactions:>=" + bumpReactions;
+
+ logger.debugv("Query: {0}", query);
+
+ PagedIterator itr = gitHub.searchIssues().q(query).list().iterator();
+ while (itr.hasNext()) {
+ GHIssue issue = itr.next();
+
+ List removeLabels = new LinkedList<>();
+ removeLabels.add(currentPriority.toLabel());
+ if (newPriority.equals(Priority.IMPORTANT)) {
+ removeLabels.add(Status.AUTO_BUMP.toLabel());
+ removeLabels.add(Status.AUTO_EXPIRE.toLabel());
+ }
+
+ issue.removeLabels(removeLabels.toArray(new String[0]));
+ issue.addLabels(newPriority.toLabel(), Status.BUMPED_BY_BOT.toLabel());
+ logger.infov("Bumped issue={0}, from={1}, to={2}", issue.getNumber(), currentPriority, newPriority);
+ }
+ }
+
+}
diff --git a/src/main/java/org/keycloak/gh/bot/BugActionScheduleAutoExpire.java b/src/main/java/org/keycloak/gh/bot/BugActionScheduleAutoExpire.java
new file mode 100644
index 0000000..aca5208
--- /dev/null
+++ b/src/main/java/org/keycloak/gh/bot/BugActionScheduleAutoExpire.java
@@ -0,0 +1,71 @@
+package org.keycloak.gh.bot;
+
+import io.quarkus.runtime.Startup;
+import io.quarkus.scheduler.Scheduled;
+import jakarta.annotation.PostConstruct;
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.jboss.logging.Logger;
+import org.keycloak.gh.bot.labels.Kind;
+import org.keycloak.gh.bot.labels.Priority;
+import org.keycloak.gh.bot.labels.Status;
+import org.keycloak.gh.bot.utils.DateUtil;
+import org.kohsuke.github.GHIssue;
+import org.kohsuke.github.GHIssueStateReason;
+import org.kohsuke.github.GitHub;
+import org.kohsuke.github.PagedIterator;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+@Startup
+@Singleton
+public class BugActionScheduleAutoExpire {
+
+ private static final Logger logger = Logger.getLogger(BugActionScheduleAutoExpire.class);
+
+ @ConfigProperty(name = "autoExpire.low.expiresDays")
+ private long lowPriorityExpiresDays;
+ @ConfigProperty(name = "autoExpire.normal.expiresDays")
+ private long normalPriorityExpiresDays;
+
+ @Inject
+ GitHubInstallationProvider gitHubProvider;
+
+ @Inject
+ BugActionMessages messages;
+
+ String rootQuery;
+
+ @PostConstruct
+ public void init() {
+ rootQuery = "repo:" + gitHubProvider.getRepositoryFullName() + " is:issue is:open label:" + Kind.BUG.toLabel() + " label:" + Status.AUTO_EXPIRE.toLabel();
+ }
+
+ @Scheduled(cron = "{autoExpire.cron}", concurrentExecution = Scheduled.ConcurrentExecution.SKIP)
+ public void checkIssuesWithLowAndNormalPriority() throws IOException {
+ logger.info("Checking issues with low and normal priority");
+ GitHub gitHub = gitHubProvider.getGitHub();
+
+ expire(gitHub, Priority.LOW, normalPriorityExpiresDays);
+ expire(gitHub, Priority.NORMAL, normalPriorityExpiresDays);
+ }
+
+ private void expire(GitHub gitHub, Priority priority, long days) throws IOException {
+ String query = rootQuery + " label:" + priority + " updated:<" + DateUtil.minusDaysString(lowPriorityExpiresDays);
+ String message = messages.getExpireComment(days, TimeUnit.DAYS);
+
+ logger.debugv("Query: {0}", query);
+
+ PagedIterator itr = gitHub.searchIssues().q(query).list().iterator();
+ while (itr.hasNext()) {
+ GHIssue issue = itr.next();
+ issue.comment(message);
+ issue.addLabels(Status.EXPIRED_BY_BOT.toLabel());
+ issue.close(GHIssueStateReason.NOT_PLANNED);
+ logger.infov("Expired issue={0}", issue.getNumber());
+ }
+ }
+
+}
diff --git a/src/main/java/org/keycloak/gh/bot/BugActionScheduleExpireMissingInfo.java b/src/main/java/org/keycloak/gh/bot/BugActionScheduleExpireMissingInfo.java
new file mode 100644
index 0000000..61becc0
--- /dev/null
+++ b/src/main/java/org/keycloak/gh/bot/BugActionScheduleExpireMissingInfo.java
@@ -0,0 +1,129 @@
+package org.keycloak.gh.bot;
+
+import io.quarkus.runtime.Startup;
+import io.quarkus.scheduler.Scheduled;
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.jboss.logging.Logger;
+import org.keycloak.gh.bot.labels.Status;
+import org.kohsuke.github.GHIssue;
+import org.kohsuke.github.GHIssueComment;
+import org.kohsuke.github.GHIssueStateReason;
+import org.kohsuke.github.GHRepository;
+import org.kohsuke.github.GitHub;
+import org.kohsuke.github.PagedIterator;
+
+import java.io.IOException;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+@Startup
+@Singleton
+public class BugActionScheduleExpireMissingInfo {
+
+ private static final Logger logger = Logger.getLogger(BugActionScheduleExpireMissingInfo.class);
+
+ @ConfigProperty(name = "missingInfo.expiration.unit")
+ private TimeUnit expirationUnit;
+ @ConfigProperty(name = "missingInfo.expiration.value")
+ private long expirationValue;
+
+ private final LastChecked lastChecked = new LastChecked();
+
+ @Inject
+ GitHubInstallationProvider gitHubProvider;
+
+ @Inject
+ BugActionMessages messages;
+
+ @Scheduled(cron = "{missingInfo.cron}", concurrentExecution = Scheduled.ConcurrentExecution.SKIP)
+ public void checkIssuesWithMissingInformation() throws IOException {
+ logger.info("Checking issues with missing information");
+ GitHub gitHub = gitHubProvider.getGitHub();
+
+ GHRepository repository = gitHub.getRepository(gitHubProvider.getRepositoryFullName());
+
+ PagedIterator missingInfoItr = repository.queryIssues().label(Status.MISSING_INFORMATION.toLabel()).label(Status.AUTO_EXPIRE.toLabel()).list().iterator();
+
+ while (missingInfoItr.hasNext()) {
+ GHIssue issue = missingInfoItr.next();
+
+ if (lastChecked.shouldCheck(issue)) {
+ GHIssueComment lastBotComment = null;
+
+ for (GHIssueComment c : issue.getComments()) {
+ if (c.getUser().getLogin().equals(gitHubProvider.getBotLogin())) {
+ if (lastBotComment == null || lastBotComment.getUpdatedAt().before(c.getUpdatedAt())) {
+ lastBotComment = c;
+ }
+ }
+ }
+
+ if (lastBotComment == null) {
+ logger.warnv("Bot comment not found: issue={0}", issue.getNumber());
+ } else {
+ lastChecked.checked(issue, lastBotComment);
+ }
+ } else {
+ long lastBotComment = lastChecked.getLastBotComment(issue).getTime();
+ long expires = lastBotComment + expirationUnit.toMillis(expirationValue);
+
+ if (System.currentTimeMillis() > expires) {
+ String comment = messages.getExpireComment(expirationValue, expirationUnit);
+ issue.comment(comment);
+ issue.removeLabels(Status.MISSING_INFORMATION.toLabel());
+ issue.close(GHIssueStateReason.NOT_PLANNED);
+ lastChecked.remove(issue);
+ logger.infov("Expired: issue={0}", issue.getNumber());
+ }
+ }
+ }
+
+ lastChecked.clean();
+ }
+
+ public class LastChecked {
+
+ Map lastBotComment = new HashMap<>();
+
+ Set visited = new HashSet<>();
+
+ public boolean shouldCheck(GHIssue issue) {
+ int issueNumber = issue.getNumber();
+ visited.add(issueNumber);
+ return !lastBotComment.containsKey(issueNumber);
+ }
+
+ public void checked(GHIssue issue, GHIssueComment issueLastBotComment) throws IOException {
+ int issueNumber = issue.getNumber();
+ visited.add(issueNumber);
+ lastBotComment.put(issueNumber, issueLastBotComment.getUpdatedAt());
+ }
+
+ public void remove(GHIssue issue) {
+ visited.remove(issue.getNumber());
+ lastBotComment.remove(issue.getNumber());
+ }
+
+ public Date getLastBotComment(GHIssue issue) {
+ return lastBotComment.get(issue.getNumber());
+ }
+
+ public void clean() {
+ lastBotComment.keySet().removeIf(integer -> !visited.contains(integer));
+ visited.clear();
+
+ if (!lastBotComment.isEmpty()) {
+ logger.infov("Monitoring: issues={0}", lastBotComment.keySet().stream().map(i -> Integer.toString(i)).collect(Collectors.joining(",")));
+ }
+ }
+
+ }
+
+}
diff --git a/src/main/java/org/keycloak/gh/bot/BugActions.java b/src/main/java/org/keycloak/gh/bot/BugActions.java
new file mode 100644
index 0000000..594cce0
--- /dev/null
+++ b/src/main/java/org/keycloak/gh/bot/BugActions.java
@@ -0,0 +1,281 @@
+package org.keycloak.gh.bot;
+
+import io.quarkiverse.githubapp.event.Issue;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import org.jboss.logging.Logger;
+import org.keycloak.gh.bot.labels.Action;
+import org.keycloak.gh.bot.labels.Kind;
+import org.keycloak.gh.bot.labels.Label;
+import org.keycloak.gh.bot.labels.Priority;
+import org.keycloak.gh.bot.labels.Status;
+import org.keycloak.gh.bot.utils.Labels;
+import org.kohsuke.github.GHEventPayload;
+import org.kohsuke.github.GHIssue;
+import org.kohsuke.github.GHIssueComment;
+import org.kohsuke.github.GHIssueState;
+import org.kohsuke.github.GHIssueStateReason;
+import org.kohsuke.github.GHLabel;
+import org.kohsuke.github.GHMilestone;
+import org.kohsuke.github.GHRepository;
+import org.kohsuke.github.PagedIterator;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+@ApplicationScoped
+public class BugActions {
+
+ private static final Logger logger = Logger.getLogger(BugActions.class);
+
+ private static final Pattern MAJOR_VERSION_PATTERN = Pattern.compile("(\\d+)\\.\\d+.\\d+");
+
+ private static final Map actions = initActions(
+ BugAction.create(Action.QUESTION)
+ .comment()
+ .closeAsNotPlanned(),
+ BugAction.create(Action.MISSING_INFO)
+ .comment()
+ .status(Status.MISSING_INFORMATION)
+ .autoExpire(),
+ BugAction.create(Action.OLD_RELEASE)
+ .comment()
+ .status(Status.MISSING_INFORMATION)
+ .autoExpire(),
+ BugAction.create(Action.INVALID)
+ .comment()
+ .closeAsNotPlanned(),
+ BugAction.create(Action.PRIORITY_REGRESSION)
+ .priority(Priority.BLOCKER)
+ .kind(Kind.REGRESSION)
+ .nextMilestone(),
+ BugAction.create(Action.PRIORITY_IMPORTANT)
+ .priority(Priority.IMPORTANT),
+ BugAction.create(Action.PRIORITY_NORMAL)
+ .priority(Priority.NORMAL)
+ .comment()
+ .helpWanted()
+ .autoExpire()
+ .autoBump()
+ .removeMilestone(),
+ BugAction.create(Action.PRIORITY_LOW)
+ .priority(Priority.LOW)
+ .comment()
+ .helpWanted()
+ .autoExpire()
+ .autoBump()
+ .removeMilestone()
+ );
+
+ @Inject
+ GitHubInstallationProvider gitHubProvider;
+
+ @Inject
+ BugActionMessages messages;
+
+ public void runAction(Action action, GHIssue issue) throws IOException {
+ Optional first = actions.values().stream().filter(a -> a.action.equals(action)).findFirst();
+ if (first.isPresent()) {
+ runAction(first.get(), issue);
+ } else {
+ throw new IllegalArgumentException("Unknown action " + action);
+ }
+ }
+
+ private void runAction(BugAction action, GHIssue issue) throws IOException {
+ if (action != null) {
+ logger.infov("Running action={0} on issue={1}", action.action.toLabel(), issue.getHtmlUrl());
+
+ Set labels = issue.getLabels().stream().map(GHLabel::getName).collect(Collectors.toSet());
+
+ if (labels.contains(Labels.KIND_BUG)) {
+ List labelsToAdd = new LinkedList<>();
+ List labelsToRemove = new LinkedList<>();
+
+ if (action.priority != null) {
+ labels.stream().filter(Priority::isInstance).forEach(labelsToRemove::add);
+
+ labelsToAdd.add(action.priority.toLabel());
+ }
+
+ labels.stream().filter(Status::isInstance).forEach(labelsToRemove::add);
+
+ if (action.status != null) {
+ labelsToAdd.add(action.status.toLabel());
+ }
+
+ if (action.kind != null) {
+ labelsToAdd.add(action.kind.toLabel());
+ }
+
+ if (action.helpWanted) {
+ labelsToAdd.add(Label.HELP_WANTED.toLabel());
+ }
+
+ if (action.autoExpire) {
+ labelsToAdd.add(Status.AUTO_EXPIRE.toLabel());
+ }
+
+ if (action.autoBump) {
+ labelsToAdd.add(Status.AUTO_BUMP.toLabel());
+ }
+
+ labelsToRemove.add(action.action.toLabel());
+
+ labelsToRemove.removeAll(labelsToAdd);
+ issue.removeLabels(labelsToRemove.toArray(new String[0]));
+
+ if (!labelsToAdd.isEmpty()) {
+ issue.addLabels(labelsToAdd.toArray(new String[0]));
+ }
+
+ if (action.closeAsNotPlanned) {
+ issue.close(GHIssueStateReason.NOT_PLANNED);
+ } else {
+ if (issue.getState().equals(GHIssueState.CLOSED)) {
+ issue.reopen();
+ }
+
+ if (action.setNextMileStone) {
+ GHMilestone nextMajorRelease = getNextMajorRelease(issue.getRepository());
+ issue.setMilestone(nextMajorRelease);
+ } else if (action.removeMileStone) {
+ issue.setMilestone(null);
+ }
+ }
+
+ // Comment should be the last thing the bot does!
+ if (action.comment) {
+ for (GHIssueComment c : issue.getComments()) {
+ if (c.getUser().getLogin().equals(gitHubProvider.getBotLogin())) {
+ c.delete();
+ }
+ }
+
+ issue.comment(messages.getBugActionComment(action.action));
+ }
+ }
+ }
+ }
+
+ static Map initActions(BugAction... bugActions) {
+ Map map = new HashMap<>();
+ for (BugAction bugAction : bugActions) {
+ map.put(bugAction.action.toLabel(), bugAction);
+ }
+ return map;
+ }
+
+ private GHMilestone getNextMajorRelease(GHRepository repository) {
+ PagedIterator itr = repository.listMilestones(GHIssueState.OPEN).iterator();
+ GHMilestone nextMajorMilestone = null;
+ Integer nextMajorRelease = null;
+ while (itr.hasNext()) {
+ GHMilestone next = itr.next();
+ Matcher matcher = MAJOR_VERSION_PATTERN.matcher(next.getTitle());
+ if (matcher.matches()) {
+ Integer majorVersion = Integer.valueOf(matcher.group(1));
+ if (nextMajorRelease == null || majorVersion < nextMajorRelease) {
+ nextMajorRelease = majorVersion;
+ nextMajorMilestone = next;
+ }
+ }
+ }
+ return nextMajorMilestone;
+ }
+
+ static Properties initProperties() {
+ Properties properties = new Properties();
+ try {
+ properties.load(BugActions.class.getResourceAsStream("bug-action-messages.properties"));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return properties;
+ }
+
+ static final class BugAction {
+
+ Action action;
+ boolean comment = false;
+
+ Priority priority;
+ Status status;
+ Kind kind;
+ boolean closeAsNotPlanned = false;
+ boolean setNextMileStone = false;
+ boolean removeMileStone = false;
+ boolean helpWanted = false;
+ boolean autoExpire = false;
+ boolean autoBump = false;
+
+ public static BugAction create(Action action) {
+ return new BugAction(action);
+ }
+
+ public BugAction(Action action) {
+ this.action = action;
+ }
+
+ BugAction comment() {
+ comment = true;
+ return this;
+ }
+
+ BugAction closeAsNotPlanned() {
+ closeAsNotPlanned = true;
+ return this;
+ }
+
+ BugAction priority(Priority priority) {
+ this.priority = priority;
+ return this;
+ }
+
+ BugAction status(Status status) {
+ this.status = status;
+ return this;
+ }
+
+ BugAction kind(Kind kind) {
+ this.kind = kind;
+ return this;
+ }
+
+ BugAction helpWanted() {
+ this.helpWanted = true;
+ return this;
+ }
+
+ BugAction nextMilestone() {
+ this.setNextMileStone = true;
+ return this;
+ }
+
+ BugAction removeMilestone() {
+ this.removeMileStone = true;
+ return this;
+ }
+
+ BugAction autoExpire() {
+ this.autoExpire = true;
+ return this;
+ }
+
+ BugAction autoBump() {
+ this.autoBump = true;
+ return this;
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/keycloak/gh/bot/BugActionsOnComment.java b/src/main/java/org/keycloak/gh/bot/BugActionsOnComment.java
new file mode 100644
index 0000000..5545343
--- /dev/null
+++ b/src/main/java/org/keycloak/gh/bot/BugActionsOnComment.java
@@ -0,0 +1,45 @@
+package org.keycloak.gh.bot;
+
+import io.quarkiverse.githubapp.event.IssueComment;
+import jakarta.inject.Inject;
+import org.keycloak.gh.bot.labels.Action;
+import org.keycloak.gh.bot.labels.Kind;
+import org.keycloak.gh.bot.utils.Labels;
+import org.kohsuke.github.GHEventPayload;
+import org.kohsuke.github.GHLabel;
+import org.kohsuke.github.GHPermissionType;
+
+import java.io.IOException;
+
+public class BugActionsOnComment {
+
+ @Inject
+ BugActions bugActions;
+
+ void onLabeled(@IssueComment.Created GHEventPayload.IssueComment payload) throws IOException {
+ if (Labels.hasLabel(payload.getIssue(), Kind.BUG.toLabel())) {
+ Action action = getAction(payload.getComment().getBody());
+
+ GHPermissionType permission = payload.getRepository().getPermission(payload.getSender());
+ if (GHPermissionType.WRITE.equals(permission) || GHPermissionType.ADMIN.equals(permission)) {
+ bugActions.runAction(action, payload.getIssue());
+ }
+ }
+ }
+
+ Action getAction(String body) {
+ int lastNewLine = body.lastIndexOf("\n");
+ if (lastNewLine > 0) {
+ body = body.substring(lastNewLine).trim();
+ } else {
+ body = body.trim();
+ }
+
+ if (body.startsWith("~") && Action.isInstance(body.replace("~", "action/"))) {
+ return Action.fromAction(body.substring(1));
+ }
+
+ return null;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/keycloak/gh/bot/BugActionsOnLabel.java b/src/main/java/org/keycloak/gh/bot/BugActionsOnLabel.java
new file mode 100644
index 0000000..5bd7fe7
--- /dev/null
+++ b/src/main/java/org/keycloak/gh/bot/BugActionsOnLabel.java
@@ -0,0 +1,26 @@
+package org.keycloak.gh.bot;
+
+import io.quarkiverse.githubapp.event.Issue;
+import jakarta.inject.Inject;
+import org.keycloak.gh.bot.labels.Action;
+import org.kohsuke.github.GHEventPayload;
+import org.kohsuke.github.GHIssue;
+
+import java.io.IOException;
+
+public class BugActionsOnLabel {
+
+ @Inject
+ BugActions bugActions;
+
+ void onLabeled(@Issue.Labeled GHEventPayload.Issue payload) throws IOException {
+ String label = payload.getLabel().getName();
+ GHIssue issue = payload.getIssue();
+
+ if (Action.isInstance(label)) {
+ Action action = Action.fromLabel(label);
+ bugActions.runAction(action, issue);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/keycloak/gh/bot/BumpPriorityForTeam.java b/src/main/java/org/keycloak/gh/bot/BumpPriorityForTeam.java
new file mode 100644
index 0000000..dabf7fd
--- /dev/null
+++ b/src/main/java/org/keycloak/gh/bot/BumpPriorityForTeam.java
@@ -0,0 +1,38 @@
+package org.keycloak.gh.bot;
+
+import io.quarkiverse.githubapp.event.Issue;
+import org.keycloak.gh.bot.labels.Label;
+import org.keycloak.gh.bot.labels.Priority;
+import org.keycloak.gh.bot.labels.Status;
+import org.kohsuke.github.GHEventPayload;
+import org.kohsuke.github.GHIssue;
+import org.kohsuke.github.GHLabel;
+
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.List;
+
+public class BumpPriorityForTeam {
+
+ void onOpen(@Issue.Labeled GHEventPayload.Issue payload) throws IOException {
+ GHLabel label = payload.getLabel();
+ GHIssue issue = payload.getIssue();
+ if (label.getName().equals("team/rh-iam")) {
+ Priority priority = Priority.IMPORTANT;
+
+ List removeLabels = new LinkedList<>();
+ removeLabels.add(Status.AUTO_BUMP.toLabel());
+ removeLabels.add(Status.AUTO_EXPIRE.toLabel());
+ removeLabels.add(Label.HELP_WANTED.toLabel());
+
+ removeLabels.addAll(payload.getIssue().getLabels().stream().map(GHLabel::getName)
+ .filter(l -> !l.equals(priority.toLabel()))
+ .filter(Priority::isInstance).toList());
+
+ issue.removeLabels(removeLabels.toArray(new String[0]));
+
+ issue.addLabels(Priority.IMPORTANT.toLabel());
+ }
+ }
+
+}
diff --git a/src/main/java/org/keycloak/gh/bot/GitHubInstallationProvider.java b/src/main/java/org/keycloak/gh/bot/GitHubInstallationProvider.java
new file mode 100644
index 0000000..cb0a22f
--- /dev/null
+++ b/src/main/java/org/keycloak/gh/bot/GitHubInstallationProvider.java
@@ -0,0 +1,56 @@
+package org.keycloak.gh.bot;
+
+import io.quarkiverse.githubapp.GitHubClientProvider;
+import io.quarkus.runtime.Startup;
+import jakarta.annotation.PostConstruct;
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
+import org.jboss.logging.Logger;
+import org.kohsuke.github.GHApp;
+import org.kohsuke.github.GHAppInstallation;
+import org.kohsuke.github.GitHub;
+
+import java.io.IOException;
+
+@Startup
+@Singleton
+public class GitHubInstallationProvider {
+
+ private static final Logger logger = Logger.getLogger(GitHubInstallationProvider.class);
+
+ @Inject
+ GitHubClientProvider gitHubClientProvider;
+
+ private long installationId = -1;
+ private String botLogin = null;
+ private String repositoryFullName = null;
+
+ @PostConstruct
+ public void init() throws IOException {
+ GitHub appClient = gitHubClientProvider.getApplicationClient();
+ GHApp app = appClient.getApp();
+
+ GHAppInstallation installation = app.listInstallations().iterator().next();
+ botLogin = app.getSlug() + "[bot]";
+
+ installationId = installation.getId();
+
+ GitHub installationClient = gitHubClientProvider.getInstallationClient(installationId);
+ repositoryFullName = installationClient.getInstallation().listRepositories().iterator().next().getFullName();
+
+ logger.infov("Init: repository={0}, bot={1}, installation={2}", repositoryFullName, botLogin, installationId);
+ }
+
+ public GitHub getGitHub() {
+ return gitHubClientProvider.getInstallationClient(installationId);
+ }
+
+ public String getRepositoryFullName() {
+ return repositoryFullName;
+ }
+
+ public String getBotLogin() {
+ return botLogin;
+ }
+
+}
diff --git a/src/main/java/org/keycloak/gh/bot/UpdatedLabelsOnPriorityChange.java b/src/main/java/org/keycloak/gh/bot/UpdatedLabelsOnPriorityChange.java
new file mode 100644
index 0000000..131be9f
--- /dev/null
+++ b/src/main/java/org/keycloak/gh/bot/UpdatedLabelsOnPriorityChange.java
@@ -0,0 +1,35 @@
+package org.keycloak.gh.bot;
+
+import io.quarkiverse.githubapp.event.Issue;
+import org.keycloak.gh.bot.labels.Priority;
+import org.keycloak.gh.bot.labels.Status;
+import org.kohsuke.github.GHEventPayload;
+import org.kohsuke.github.GHIssue;
+import org.kohsuke.github.GHLabel;
+
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.List;
+
+public class UpdatedLabelsOnPriorityChange {
+
+ void onOpen(@Issue.Labeled GHEventPayload.Issue payload) throws IOException {
+ GHLabel label = payload.getLabel();
+ GHIssue issue = payload.getIssue();
+ if (Priority.isInstance(label.getName())) {
+ Priority priority = Priority.fromLabel(label.getName());
+ if (priority.equals(Priority.IMPORTANT) || priority.equals(Priority.BLOCKER)) {
+ List removeLabels = new LinkedList<>();
+ removeLabels.add(Status.AUTO_BUMP.toLabel());
+ removeLabels.add(Status.AUTO_EXPIRE.toLabel());
+
+ removeLabels.addAll(payload.getIssue().getLabels().stream().map(GHLabel::getName)
+ .filter(l -> !l.equals(label.getName()))
+ .filter(Priority::isInstance).toList());
+
+ issue.removeLabels(removeLabels.toArray(new String[0]));
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/org/keycloak/gh/bot/cli/KeycloakBotCli.java b/src/main/java/org/keycloak/gh/bot/cli/KeycloakBotCli.java
deleted file mode 100644
index bca71bf..0000000
--- a/src/main/java/org/keycloak/gh/bot/cli/KeycloakBotCli.java
+++ /dev/null
@@ -1,95 +0,0 @@
-package org.keycloak.gh.bot.cli;
-
-import org.kohsuke.github.GHCheckRun;
-import org.kohsuke.github.GHCommentAuthorAssociation;
-import org.kohsuke.github.GHEvent;
-import org.kohsuke.github.GHEventPayload;
-import org.kohsuke.github.GHIssue;
-import org.kohsuke.github.GHIssueComment;
-import org.kohsuke.github.GHPullRequest;
-import org.kohsuke.github.GHRepository;
-import org.kohsuke.github.GHWorkflowRun;
-import org.kohsuke.github.GitHub;
-import org.kohsuke.github.ReactionContent;
-
-import java.io.IOException;
-import java.util.List;
-import java.util.Set;
-
-//@Cli(name = "@bot", commands = { KeycloakBotCli.RerunCommand.class })
-public class KeycloakBotCli {
-
- interface Commands {
- void run(GHEventPayload.IssueComment payload, GitHub gitHub) throws IOException;
- }
-
-// @Command(name = "rerun")
- static class RerunCommand implements Commands {
-
- private Set ALLOWED = Set.of(GHCommentAuthorAssociation.OWNER, GHCommentAuthorAssociation.MEMBER);
-
- public void run(GHEventPayload.IssueComment payload, GitHub gitHub) throws IOException {
- if (!ALLOWED.contains(payload.getComment().getAuthorAssociation())) {
- payload.getComment().createReaction(ReactionContent.MINUS_ONE);
- return;
- }
-
- GHRepository repository = payload.getRepository();
- GHIssue issue = payload.getIssue();
- GHIssueComment comment = payload.getComment();
-
- if (!issue.isPullRequest()) {
- comment.createReaction(ReactionContent.MINUS_ONE);
- return;
- }
-
- GHPullRequest pullRequest = repository.getPullRequest(issue.getNumber());
-
- List ghWorkflowRuns = repository.queryWorkflowRuns()
- .branch(pullRequest.getHead().getRef())
- .event(GHEvent.PULL_REQUEST)
- .actor(pullRequest.getUser())
- .status(GHWorkflowRun.Status.COMPLETED)
- .list().toList();
-
- for (GHWorkflowRun r : ghWorkflowRuns) {
- if (r.getConclusion().equals(GHWorkflowRun.Conclusion.FAILURE) ||
- r.getConclusion().equals(GHWorkflowRun.Conclusion.FAILURE) ||
- r.getConclusion().equals(GHWorkflowRun.Conclusion.FAILURE)) {
- System.out.println("Rerunning: " + r.getName());
- r.rerun();
- }
- }
-
-
- for (GHCheckRun checkRun : repository.getCommit(pullRequest.getHead().getSha()).getCheckRuns()) {
- GHCheckRun.Conclusion conclusion = checkRun.getConclusion();
-
- if (!(conclusion.equals(GHCheckRun.Conclusion.FAILURE) ||
- conclusion.equals(GHCheckRun.Conclusion.CANCELLED) ||
- conclusion.equals(GHCheckRun.Conclusion.TIMED_OUT))) {
- continue;
- }
-
- if (!checkRun.getApp().getSlug().equals("github-actions")) {
- continue;
- }
-
- }
-
-
- //
-
- for (GHCheckRun r : repository.getCheckRuns(pullRequest.getMergeCommitSha())) {
- System.out.println("Conclusion : " + r.getConclusion());
-
- }
-
- System.out.println("Hello s");
-
- comment.createReaction(ReactionContent.PLUS_ONE);
- }
-
- }
-
-}
diff --git a/src/main/java/org/keycloak/gh/bot/labels/Action.java b/src/main/java/org/keycloak/gh/bot/labels/Action.java
new file mode 100644
index 0000000..95798d0
--- /dev/null
+++ b/src/main/java/org/keycloak/gh/bot/labels/Action.java
@@ -0,0 +1,35 @@
+package org.keycloak.gh.bot.labels;
+
+public enum Action {
+
+ QUESTION,
+ MISSING_INFO,
+ OLD_RELEASE,
+ INVALID,
+ PRIORITY_REGRESSION,
+ PRIORITY_IMPORTANT,
+ PRIORITY_NORMAL,
+ PRIORITY_LOW;
+
+ @Override
+ public String toString() {
+ return EnumUtils.toLabel(this);
+ }
+
+ public String toLabel() {
+ return EnumUtils.toLabel(this);
+ }
+
+ public static boolean isInstance(String label) {
+ return EnumUtils.isInstance(label, Action.class);
+ }
+
+ public static Action fromAction(String action) {
+ return EnumUtils.fromValue(action, Action.class);
+ }
+
+ public static Action fromLabel(String label) {
+ return EnumUtils.fromLabel(label, Action.class);
+ }
+
+}
diff --git a/src/main/java/org/keycloak/gh/bot/labels/EnumUtils.java b/src/main/java/org/keycloak/gh/bot/labels/EnumUtils.java
new file mode 100644
index 0000000..5bc7f62
--- /dev/null
+++ b/src/main/java/org/keycloak/gh/bot/labels/EnumUtils.java
@@ -0,0 +1,53 @@
+package org.keycloak.gh.bot.labels;
+
+import java.util.Arrays;
+import java.util.Locale;
+
+ class EnumUtils {
+
+ public static > boolean isInstance(String label, Class enumClass) {
+ String[] split = label.split("/");
+ if (split.length != 2) {
+ return false;
+ }
+ String group = classNameToCategory(enumClass);
+ if (!group.equals(split[0])) {
+ return false;
+ }
+ String enumName = valueToEnumName(split[1]);
+ return Arrays.stream(enumClass.getEnumConstants()).anyMatch(e -> e.name().equals(enumName));
+ }
+
+ public static > String toLabel(Enum e) {
+ String group = classNameToCategory(e.getClass());
+ String value = enumNameToValue(e);
+ return group + "/" + value;
+ }
+
+ public static > T fromLabel(String label, Class enumClass) {
+ String[] split = label.split("/");
+ String group = classNameToCategory(enumClass);
+ if (!group.equals(split[0])) {
+ throw new IllegalArgumentException("Invalid group " + split[0] + " for label " + label);
+ }
+ String value = valueToEnumName(split[1]);
+ return Enum.valueOf(enumClass, value);
+ }
+
+ public static > T fromValue(String value, Class enumClass) {
+ return Enum.valueOf(enumClass, valueToEnumName(value));
+ }
+
+ private static String valueToEnumName(String name) {
+ return name.toUpperCase(Locale.ENGLISH).replace('-', '_');
+ }
+
+ private static > String enumNameToValue(Enum e) {
+ return e.name().toLowerCase(Locale.ENGLISH).replace('_', '-');
+ }
+
+ private static String classNameToCategory(Class> enumClass) {
+ return enumClass.getSimpleName().toLowerCase(Locale.ENGLISH);
+ }
+
+}
diff --git a/src/main/java/org/keycloak/gh/bot/labels/Kind.java b/src/main/java/org/keycloak/gh/bot/labels/Kind.java
new file mode 100644
index 0000000..8523571
--- /dev/null
+++ b/src/main/java/org/keycloak/gh/bot/labels/Kind.java
@@ -0,0 +1,29 @@
+package org.keycloak.gh.bot.labels;
+
+public enum Kind {
+
+ BUG,
+ REGRESSION;
+
+ @Override
+ public String toString() {
+ return EnumUtils.toLabel(this);
+ }
+
+ public String toLabel() {
+ return EnumUtils.toLabel(this);
+ }
+
+ public static boolean isInstance(String label) {
+ return EnumUtils.isInstance(label, Kind.class);
+ }
+
+ public static Kind fromKind(String kind) {
+ return EnumUtils.fromValue(kind, Kind.class);
+ }
+
+ public static Kind fromLabel(String label) {
+ return EnumUtils.fromLabel(label, Kind.class);
+ }
+
+}
diff --git a/src/main/java/org/keycloak/gh/bot/labels/Label.java b/src/main/java/org/keycloak/gh/bot/labels/Label.java
new file mode 100644
index 0000000..9c6284f
--- /dev/null
+++ b/src/main/java/org/keycloak/gh/bot/labels/Label.java
@@ -0,0 +1,18 @@
+package org.keycloak.gh.bot.labels;
+
+import java.util.Locale;
+
+public enum Label {
+
+ HELP_WANTED;
+
+ @Override
+ public String toString() {
+ return toLabel();
+ }
+
+ public String toLabel() {
+ return name().toLowerCase(Locale.ENGLISH).replace('_', ' ');
+ }
+
+}
diff --git a/src/main/java/org/keycloak/gh/bot/labels/Priority.java b/src/main/java/org/keycloak/gh/bot/labels/Priority.java
new file mode 100644
index 0000000..9f25073
--- /dev/null
+++ b/src/main/java/org/keycloak/gh/bot/labels/Priority.java
@@ -0,0 +1,31 @@
+package org.keycloak.gh.bot.labels;
+
+public enum Priority {
+
+ LOW,
+ NORMAL,
+ IMPORTANT,
+ BLOCKER;
+
+ @Override
+ public String toString() {
+ return EnumUtils.toLabel(this);
+ }
+
+ public String toLabel() {
+ return EnumUtils.toLabel(this);
+ }
+
+ public static boolean isInstance(String label) {
+ return EnumUtils.isInstance(label, Priority.class);
+ }
+
+ public static Priority fromPriority(String priority) {
+ return EnumUtils.fromValue(priority, Priority.class);
+ }
+
+ public static Priority fromLabel(String label) {
+ return EnumUtils.fromLabel(label, Priority.class);
+ }
+
+}
diff --git a/src/main/java/org/keycloak/gh/bot/labels/Status.java b/src/main/java/org/keycloak/gh/bot/labels/Status.java
new file mode 100644
index 0000000..8e924d5
--- /dev/null
+++ b/src/main/java/org/keycloak/gh/bot/labels/Status.java
@@ -0,0 +1,34 @@
+package org.keycloak.gh.bot.labels;
+
+public enum Status {
+
+ AUTO_EXPIRE,
+ AUTO_BUMP,
+ EXPIRED_BY_BOT,
+ MISSING_INFORMATION,
+ BUMPED_BY_BOT,
+ TRIAGE,
+ REOPENED;
+
+ @Override
+ public String toString() {
+ return EnumUtils.toLabel(this);
+ }
+
+ public String toLabel() {
+ return EnumUtils.toLabel(this);
+ }
+
+ public static boolean isInstance(String label) {
+ return EnumUtils.isInstance(label, Status.class);
+ }
+
+ public static Status fromStatus(String status) {
+ return EnumUtils.fromValue(status, Status.class);
+ }
+
+ public static Status fromLabel(String label) {
+ return EnumUtils.fromLabel(label, Status.class);
+ }
+
+}
diff --git a/src/main/java/org/keycloak/gh/bot/utils/DateUtil.java b/src/main/java/org/keycloak/gh/bot/utils/DateUtil.java
new file mode 100644
index 0000000..0c664b3
--- /dev/null
+++ b/src/main/java/org/keycloak/gh/bot/utils/DateUtil.java
@@ -0,0 +1,15 @@
+package org.keycloak.gh.bot.utils;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Locale;
+
+public class DateUtil {
+
+ private static final DateTimeFormatter DATE_STRING_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd", Locale.ENGLISH);
+
+ public static String minusDaysString(long days) {
+ return DATE_STRING_FORMATTER.format(LocalDateTime.now().minusDays(days));
+ }
+
+}
diff --git a/src/main/java/org/keycloak/gh/bot/utils/IssueParser.java b/src/main/java/org/keycloak/gh/bot/utils/IssueParser.java
index 18cf8f8..b6f7564 100644
--- a/src/main/java/org/keycloak/gh/bot/utils/IssueParser.java
+++ b/src/main/java/org/keycloak/gh/bot/utils/IssueParser.java
@@ -18,4 +18,8 @@ public static String getAreaFromBody(String body) {
}
}
+ public static boolean isRegression(String body) {
+ return body.contains("- [X] The issue is a regressions");
+ }
+
}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 2796ba3..4be8398 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -3,8 +3,19 @@ quarkus.application.version=${buildNumber:999-SNAPSHOT}
quarkus.openshift.labels."app"=keycloak-github-bot
quarkus.openshift.annotations."kubernetes.io/tls-acme"=true
-quarkus.openshift.env.vars.QUARKUS_GITHUB_APP_APP_ID=252342
+quarkus.openshift.env.vars.QUARKUS_GITHUB_APP_APP_ID=817634
quarkus.openshift.env.vars.QUARKUS_GITHUB_APP_APP_NAME=keycloak-github-bot
quarkus.openshift.env.vars.QUARKUS_OPTS=-Dquarkus.http.host=0.0.0.0 -Xmx150m
quarkus.openshift.env.secrets=keycloak-github-bot
+missingInfo.cron=0 4 * * * ?
+missingInfo.expiration.unit=DAYS
+missingInfo.expiration.value=14
+
+autoBump.cron=0 5 * * * ?
+autoBump.low.reactions=5
+autoBump.normal.reactions=10
+
+autoExpire.cron=0 6 * * * ?
+autoExpire.low.expiresDays=90
+autoExpire.normal.expiresDays=180
diff --git a/src/main/resources/org/keycloak/gh/bot/bug-action-messages.properties b/src/main/resources/org/keycloak/gh/bot/bug-action-messages.properties
new file mode 100644
index 0000000..988feee
--- /dev/null
+++ b/src/main/resources/org/keycloak/gh/bot/bug-action-messages.properties
@@ -0,0 +1,35 @@
+action-QUESTION=\
+ Thanks for reporting this issue, but this looks like a request for help and not a bug.\n\
+ \n\
+ For help and questions around Keycloak please see [keycloak.org/community](https://www.keycloak.org/community).
+
+action-MISSING_INFO=\
+ Thanks for reporting this issue, but there is insufficient information or lack of steps to reproduce.\n\
+ \n\
+ Please provide additional details, otherwise the issue will be automatically closed within 14 days.
+
+action-OLD_RELEASE=\
+ Thanks for reporting this issue, but as this is reported against an older and unsupported release we are not able \
+ to evaluate the issue. Please verify with the latest release and update the issue if the issue still exists.\n\
+ \n\
+ Please provide additional details, otherwise the issue will be automatically closed within 14 days.
+
+action-INVALID=\
+ Thanks for reporting this issue. However, after review this is not considered a valid issue, or has been recently resolved.\n\
+ \n\
+ As the issue is not valid it will be automatically closed.
+
+action-PRIORITY_NORMAL=\
+ Due to the amount of issues reported by the community we are not able to prioritise resolving this issue at the moment.\n\
+ \n\
+ If you are affected by this issue, upvote it by adding a :thumbsup: in the description. We would also welcome a \
+ contribution to fix the issue.
+
+action-PRIORITY_LOW=\
+ Due to the amount of issues reported by the community we are not able to prioritise resolving this issue at the moment.\n\
+ \n\
+ If you are affected by this issue, upvote it by adding a :thumbsup: in the description. We would also welcome a \
+ contribution to fix the issue.
+
+expired=\
+ Due to lack of updates in the last {0} {1} this issue will be automatically closed.
\ No newline at end of file
diff --git a/src/test/java/org/keycloak/gh/bot/labels/EnumTest.java b/src/test/java/org/keycloak/gh/bot/labels/EnumTest.java
new file mode 100644
index 0000000..c88246f
--- /dev/null
+++ b/src/test/java/org/keycloak/gh/bot/labels/EnumTest.java
@@ -0,0 +1,50 @@
+package org.keycloak.gh.bot.labels;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class EnumTest {
+
+ @Test
+ public void testToLabel() {
+ Assertions.assertEquals("priority/important", Priority.IMPORTANT.toLabel());
+ Assertions.assertEquals("label:priority/important", "label:" + Priority.IMPORTANT);
+ Assertions.assertEquals("status/missing-information", Status.MISSING_INFORMATION.toLabel());
+ Assertions.assertEquals("status/bumped-by-bot", Status.BUMPED_BY_BOT.toLabel());
+ Assertions.assertEquals("label:status/missing-information", "label:" + Status.MISSING_INFORMATION);
+ Assertions.assertEquals("kind/bug", Kind.BUG.toLabel());
+ Assertions.assertEquals("label:kind/bug", "label:" + Kind.BUG);
+ }
+
+ @Test
+ public void fromLabel() {
+ Assertions.assertEquals(Priority.IMPORTANT, Priority.fromLabel("priority/important"));
+ Assertions.assertEquals(Status.MISSING_INFORMATION, Status.fromLabel("status/missing-information"));
+ Assertions.assertEquals(Status.BUMPED_BY_BOT, Status.fromLabel("status/bumped-by-bot"));
+ Assertions.assertEquals(Kind.BUG, Kind.fromLabel("kind/bug"));
+ }
+
+ @Test
+ public void fromName() {
+ Assertions.assertEquals(Priority.IMPORTANT, Priority.fromPriority("important"));
+ Assertions.assertEquals(Status.MISSING_INFORMATION, Status.fromStatus("missing-information"));
+ Assertions.assertEquals(Kind.BUG, Kind.fromKind("bug"));
+ }
+
+ @Test
+ public void basicLabels() {
+ Assertions.assertEquals("help wanted", Label.HELP_WANTED.toLabel());
+ Assertions.assertEquals("label:\"help wanted\"", "label:\"" + Label.HELP_WANTED.toLabel() + "\"");
+ }
+
+ @Test
+ public void isInstance() {
+ Assertions.assertTrue(Priority.isInstance("priority/important"));
+ Assertions.assertFalse(Priority.isInstance("priority/something"));
+ Assertions.assertFalse(Priority.isInstance("something/important"));
+ Assertions.assertFalse(Priority.isInstance("important"));
+ Assertions.assertTrue(Status.isInstance("status/missing-information"));
+ Assertions.assertFalse(Status.isInstance("something"));
+ }
+
+}