diff --git a/.github/workflows/unomi-ci-build-tests.yml b/.github/workflows/unomi-ci-build-tests.yml index e629253cd..2533a8b23 100644 --- a/.github/workflows/unomi-ci-build-tests.yml +++ b/.github/workflows/unomi-ci-build-tests.yml @@ -31,7 +31,7 @@ jobs: sudo apt-get install -y graphviz dot -V - name: Build and Unit tests - run: mvn -U -ntp -e clean install + run: ./build.sh --ci integration-tests: name: Execute integration tests @@ -54,16 +54,22 @@ jobs: distribution: 'temurin' java-version: '17' cache: 'maven' + - name: Install GraphViz + run: | + sudo apt-get update + sudo apt-get install -y graphviz + dot -V - name: Integration tests + env: + MAVEN_EXTRA_OPTS: >- + -Dopensearch.port=${{ matrix.port }} + -Delasticsearch.port=${{ matrix.port }} run: | - FLAGS="-Pintegration-tests" if [ "${{ matrix.search-engine }}" = "opensearch" ]; then - # Trigger OpenSearch profile activation via property; do not pass any -P profile toggles - FLAGS="$FLAGS -Duse.opensearch=true" + ./build.sh --ci --integration-tests --use-opensearch + else + ./build.sh --ci --integration-tests fi - mvn -ntp clean install $FLAGS \ - -Dopensearch.port=${{ matrix.port }} \ - -Delasticsearch.port=${{ matrix.port }} - name: Archive code coverage logs uses: actions/upload-artifact@v4 if: false # UNOMI-746 Reactivate if necessary diff --git a/build.sh b/build.sh index 19dd5d0f6..e6f1ae619 100755 --- a/build.sh +++ b/build.sh @@ -19,15 +19,13 @@ ################################################################################ set -e # Exit on error -trap 'handle_error $? $LINENO $BASH_LINENO "$BASH_COMMAND" $(printf "::%s" ${FUNCNAME[@]:-})' ERR +# Keep trap arguments small: passing full $BASH_COMMAND can exceed ARG_MAX after a failed mvn invocation. +trap 'handle_error $? $LINENO' ERR # Error handling function handle_error() { local exit_code=$1 local line_no=$2 - local bash_lineno=$3 - local last_command=$4 - local func_trace=$5 cat << "EOF" _____ ____ ____ ___ ____ @@ -38,12 +36,8 @@ handle_error() { EOF echo "Error occurred in:" - echo " Command: $last_command" echo " Line: $line_no" echo " Exit code: $exit_code" - if [ ! -z "$func_trace" ]; then - echo " Function trace: $func_trace" - fi exit $exit_code } @@ -222,13 +216,23 @@ print_progress() { fi } +# Non-interactive when run from CI or when explicitly requested (e.g. GitHub Actions). +is_non_interactive() { + [ -n "${CI:-}" ] || [ -n "${GITHUB_ACTIONS:-}" ] || [ "${BUILD_NON_INTERACTIVE:-}" = "true" ] +} + # Function to prompt for continuation prompt_continue() { local prompt_text="$1" if [ -z "$prompt_text" ]; then prompt_text="Continue?" fi - + + if is_non_interactive; then + print_status "info" "Non-interactive mode: continuing ($prompt_text)" + return 0 + fi + read -p "$prompt_text (y/N) " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then @@ -296,6 +300,7 @@ EOF echo -e " ${CYAN}--it-debug-port PORT${NC} Set integration test debug port" echo -e " ${CYAN}--it-debug-suspend${NC} Suspend integration test until debugger connects" echo -e " ${CYAN}--skip-migration-tests${NC} Skip migration-related tests" + echo -e " ${CYAN}--ci${NC} CI mode: no Karaf, no Maven cache, Maven -B -ntp, non-interactive" else cat << "EOF" _ _ _____ _ ____ @@ -329,6 +334,7 @@ EOF echo " --it-debug-port PORT Set integration test debug port" echo " --it-debug-suspend Suspend integration test until debugger connects" echo " --skip-migration-tests Skip migration-related tests" + echo " --ci CI mode: no Karaf, no Maven cache, Maven -B -ntp, non-interactive" fi echo @@ -459,6 +465,11 @@ while [ "$1" != "" ]; do --skip-migration-tests) SKIP_MIGRATION_TESTS=true ;; + --ci) + NO_KARAF=true + USE_MAVEN_CACHE=false + BUILD_NON_INTERACTIVE=true + ;; *) echo "Unknown option: $1" usage @@ -770,6 +781,11 @@ check_requirements() { MVN_CMD="mvn" MVN_OPTS="" +# CI / non-interactive: no download progress UI, batch mode (matches former workflow mvn -ntp) +if is_non_interactive; then + MVN_OPTS="$MVN_OPTS -B -ntp" +fi + # Add Maven debug option if [ "$MAVEN_DEBUG" = true ]; then MVN_OPTS="$MVN_OPTS -X" @@ -784,10 +800,14 @@ if [ "$MAVEN_OFFLINE" = true ]; then # Warn if purge cache is enabled with offline mode if [ "$PURGE_MAVEN_CACHE" = true ]; then echo "Warning: Purging Maven cache while in offline mode may cause build failures" - read -p "Continue anyway? (y/N) " -n 1 -r - echo - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - exit 1 + if is_non_interactive; then + print_status "warning" "Non-interactive mode: continuing despite purge + offline" + else + read -p "Continue anyway? (y/N) " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi fi fi fi @@ -797,13 +817,22 @@ if [ "$USE_MAVEN_CACHE" = false ]; then MVN_OPTS="$MVN_OPTS -Dmaven.build.cache.enabled=false" fi +# Extra Maven options (e.g. CI matrix ports: -Delasticsearch.port=9400) +if [ -n "${MAVEN_EXTRA_OPTS:-}" ]; then + MVN_OPTS="$MVN_OPTS $MAVEN_EXTRA_OPTS" +fi + # Verify Maven settings if [ ! -f ~/.m2/settings.xml ]; then echo "Warning: Maven settings.xml not found at ~/.m2/settings.xml" - read -p "Continue anyway? (y/N) " -n 1 -r - echo - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - exit 1 + if is_non_interactive; then + print_status "info" "Non-interactive mode: continuing without ~/.m2/settings.xml" + else + read -p "Continue anyway? (y/N) " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi fi fi @@ -900,6 +929,7 @@ if [ "$HAS_COLORS" -eq 1 ]; then else echo "Running: $MVN_CMD clean $MVN_OPTS" fi +# shellcheck disable=SC2086 $MVN_CMD clean $MVN_OPTS || { print_status "error" "Maven clean failed" exit 1 @@ -911,6 +941,7 @@ if [ "$HAS_COLORS" -eq 1 ]; then else echo "Running: $MVN_CMD install $MVN_OPTS" fi +# shellcheck disable=SC2086 $MVN_CMD install $MVN_OPTS || { print_status "error" "Maven install failed" exit 1 diff --git a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java index 1c1bd6f8c..676639b0d 100644 --- a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java @@ -467,6 +467,14 @@ protected T keepTrying(String failMessage, Supplier call, Predicate pr return value; } + protected void waitForProfileProperty(String profileId, String propertyName, Object expected) + throws InterruptedException { + keepTrying("Profile " + profileId + " property " + propertyName + " not updated", + () -> profileService.load(profileId), + profile -> profile != null && java.util.Objects.equals(expected, profile.getProperty(propertyName)), + DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); + } + protected void waitForNullValue(String failMessage, Supplier call, int timeout, int retries) throws InterruptedException { int count = 0; while (call.get() != null) { diff --git a/itests/src/test/java/org/apache/unomi/itests/PatchIT.java b/itests/src/test/java/org/apache/unomi/itests/PatchIT.java index 098a03ad2..828262ccc 100644 --- a/itests/src/test/java/org/apache/unomi/itests/PatchIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/PatchIT.java @@ -38,7 +38,7 @@ public class PatchIT extends BaseIT { private Logger LOGGER = LoggerFactory.getLogger(PatchIT.class); @Test - public void testPatch() throws IOException { + public void testPatch() throws IOException, InterruptedException { PropertyType company = profileService.getPropertyType("company"); try { @@ -49,7 +49,10 @@ public void testPatch() throws IOException { profileService.refresh(); - newCompany = profileService.getPropertyType("company"); + newCompany = keepTrying("Failed waiting for patched property type", + () -> profileService.getPropertyType("company"), + pt -> pt != null && "foo".equals(pt.getDefaultValue()), + DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); Assert.assertEquals("foo", newCompany.getDefaultValue()); } finally { profileService.setPropertyType(company); @@ -57,7 +60,7 @@ public void testPatch() throws IOException { } @Test - public void testOverride() throws IOException { + public void testOverride() throws IOException, InterruptedException { PropertyType gender = profileService.getPropertyType("gender"); try { @@ -68,7 +71,10 @@ public void testOverride() throws IOException { profileService.refresh(); - newGender = profileService.getPropertyType("gender"); + newGender = keepTrying("Failed waiting for patched property type", + () -> profileService.getPropertyType("gender"), + pt -> pt != null && "foo".equals(pt.getDefaultValue()), + DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); Assert.assertEquals("foo", newGender.getDefaultValue()); } finally { profileService.setPropertyType(gender); @@ -86,8 +92,8 @@ public void testRemove() throws IOException, InterruptedException { profileService.refresh(); - PropertyType newIncome = profileService.getPropertyType("income"); - Assert.assertNull(newIncome); + waitForNullValue("Failed waiting for property type removal", + () -> profileService.getPropertyType("income"), DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); } finally { profileService.setPropertyType(income); } @@ -105,7 +111,10 @@ public void testPatchOnConditionType() throws IOException, InterruptedException definitionsService.refresh(); - ConditionType newFormCondition = definitionsService.getConditionType("formEventCondition"); + ConditionType newFormCondition = keepTrying("Failed waiting for patched condition type", + () -> definitionsService.getConditionType("formEventCondition"), + ct -> ct != null && !ct.getMetadata().getSystemTags().contains("profileTags"), + DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); Assert.assertFalse(newFormCondition.getMetadata().getSystemTags().contains("profileTags")); } finally { definitionsService.setConditionType(formCondition); @@ -124,7 +133,10 @@ public void testPatchOnActionType() throws IOException, InterruptedException { definitionsService.refresh(); - ActionType newMailAction = definitionsService.getActionType("sendMailAction"); + ActionType newMailAction = keepTrying("Failed waiting for patched action type", + () -> definitionsService.getActionType("sendMailAction"), + at -> at != null && !at.getMetadata().getSystemTags().contains("availableToEndUser"), + DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); Assert.assertFalse(newMailAction.getMetadata().getSystemTags().contains("availableToEndUser")); } finally { definitionsService.setActionType(mailAction); diff --git a/itests/src/test/java/org/apache/unomi/itests/PropertiesUpdateActionIT.java b/itests/src/test/java/org/apache/unomi/itests/PropertiesUpdateActionIT.java index a1e340680..1bc731e42 100644 --- a/itests/src/test/java/org/apache/unomi/itests/PropertiesUpdateActionIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/PropertiesUpdateActionIT.java @@ -97,11 +97,12 @@ public void testUpdateProperties_CurrentProfile() { LOGGER.info("Changes of the event : {}", changes); Assert.assertTrue(changes > 0); + // Current profile on the event is updated in memory; do not poll persistence here. Assert.assertEquals("UPDATED FIRST NAME CURRENT PROFILE", profile.getProperty("firstName")); } @Test - public void testUpdateProperties_NotCurrentProfile() { + public void testUpdateProperties_NotCurrentProfile() throws InterruptedException { Profile profile = profileService.load(PROFILE_TARGET_TEST_ID); Profile profileToUpdate = profileService.load(PROFILE_TEST_ID); Assert.assertNull(profileToUpdate.getProperty("firstName")); @@ -117,8 +118,7 @@ public void testUpdateProperties_NotCurrentProfile() { updateProperties.setProperty(UpdatePropertiesAction.TARGET_TYPE_KEY, "profile"); eventService.send(updateProperties); - profileToUpdate = profileService.load(PROFILE_TEST_ID); - Assert.assertEquals("UPDATED FIRST NAME", profileToUpdate.getProperty("firstName")); + waitForProfileProperty(PROFILE_TEST_ID, "firstName", "UPDATED FIRST NAME"); } @Test diff --git a/itests/src/test/java/org/apache/unomi/itests/graphql/GraphQLListIT.java b/itests/src/test/java/org/apache/unomi/itests/graphql/GraphQLListIT.java index 96a6eb986..139c19063 100644 --- a/itests/src/test/java/org/apache/unomi/itests/graphql/GraphQLListIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/graphql/GraphQLListIT.java @@ -77,15 +77,28 @@ public void testCRUD() throws Exception { refreshPersistence(UserList.class); - Thread.sleep(6000); - - try (CloseableHttpResponse response = post("graphql/list/find-lists.json")) { - final ResponseContext context = ResponseContext.parse(response.getEntity()); - - Assert.assertEquals(1, ((Integer) context.getValue("data.cdp.findLists.totalCount")).intValue()); - Assert.assertEquals("testListId", context.getValue("data.cdp.findLists.edges[0].node.id")); - Assert.assertEquals(profile.getItemId(), context.getValue("data.cdp.findLists.edges[0].node.active.edges[0].node.cdp_profileIDs[0].id")); - } + final ResponseContext findListsContext = keepTrying("Failed waiting for profile in list query", + () -> { + try (CloseableHttpResponse response = post("graphql/list/find-lists.json")) { + return ResponseContext.parse(response.getEntity()); + } catch (Exception e) { + return null; + } + }, + context -> { + if (context == null) { + return false; + } + Integer totalCount = (Integer) context.getValue("data.cdp.findLists.totalCount"); + if (totalCount == null || totalCount != 1) { + return false; + } + Object profileId = context.getValue("data.cdp.findLists.edges[0].node.active.edges[0].node.cdp_profileIDs[0].id"); + return profile.getItemId().equals(profileId); + }, + DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); + + Assert.assertEquals("testListId", findListsContext.getValue("data.cdp.findLists.edges[0].node.id")); try (CloseableHttpResponse response = post("graphql/list/delete-list.json")) { final ResponseContext context = ResponseContext.parse(response.getEntity()); diff --git a/setenv.sh b/setenv.sh index dd042c07f..aa780461e 100755 --- a/setenv.sh +++ b/setenv.sh @@ -17,8 +17,13 @@ # limitations under the License. # ################################################################################ -export UNOMI_VERSION=`mvn org.apache.maven.plugins:maven-help-plugin:2.1.1:evaluate -Dexpression=project.version | grep -Ev '(^\[|Download\w+:)'` -echo Detected project version=$UNOMI_VERSION +# Quiet evaluate: avoid capturing Maven download lines into the environment (breaks CI with ARG_MAX). +export UNOMI_VERSION="$(mvn -B -q -DforceStdout help:evaluate -Dexpression=project.version -DinteractiveMode=false 2>/dev/null)" +if [ -z "$UNOMI_VERSION" ]; then + echo "Failed to detect project version from Maven" >&2 + exit 1 +fi +echo "Detected project version=$UNOMI_VERSION" export KARAF_VERSION=4.4.8 # Uncomment the following line if you need Apache Unomi to start automatically at the first start # export KARAF_OPTS="-Dunomi.autoStart=true"