This is an automated email from the ASF dual-hosted git repository. sergehuber pushed a commit to branch UNOMI-887-dev-tooling-bundle in repository https://gitbox.apache.org/repos/asf/unomi.git
commit 601488ff32a9ad301ad4a4c0ce076c02eea1d27b Author: Serge Huber <[email protected]> AuthorDate: Sun Jun 28 09:02:23 2026 +0200 UNOMI-887: Align build.ps1 with build.sh and add generic backport agent docs. Bring the Windows build script to feature parity with build.sh, add ASF and PowerShell help headers, and layer generic branch-backport guidance under the Unomi-specific backport rules for agents and Cursor. --- .cursor/rules/branch-backport.mdc | 90 +++ .cursor/rules/unomi-3-dev-backport.mdc | 60 +- AGENTS.md | 47 +- build.ps1 | 1233 +++++++++++++++++++------------- 4 files changed, 879 insertions(+), 551 deletions(-) diff --git a/.cursor/rules/branch-backport.mdc b/.cursor/rules/branch-backport.mdc new file mode 100644 index 000000000..aa4eb00b7 --- /dev/null +++ b/.cursor/rules/branch-backport.mdc @@ -0,0 +1,90 @@ +--- +description: Generic workflow for backporting changes from a source branch to a target branch without regressions +alwaysApply: true +--- + +# Branch backport rules (generic — mandatory) + +Use whenever porting work **from a source branch to a target branch** (release line, long-lived dev branch, stacked PR parent, etc.). + +Set branch names once per task (examples: `SOURCE=feature/x` `TARGET=main`, or `SOURCE=unomi-3-dev` `TARGET=master`): + +```bash +cd "$(git rev-parse --show-toplevel)" +git fetch origin +SOURCE=<source-branch> # where the change lives today +TARGET=<target-branch> # where it must land (canonical for existing files) +``` + +Repo-specific overlays (e.g. UNOMI-875): `.cursor/rules/unomi-3-dev-backport.mdc` — read **after** this file when applicable. + +## Never do this + +- **Never** `git checkout origin/$SOURCE -- <path>` on files that **already exist on** `origin/$TARGET`. +- **Never** assume “file differs on source ⇒ target is missing it” — the target often has **review-only fixes** the source never received. +- **Never** `git cherry-pick` commits from `$SOURCE` history onto `$TARGET` for bulk backport work without per-commit review (source history is often pre-merge noise). +- **Never** downgrade pinned versions on `$TARGET` (images, BOM, lockfiles) to match `$SOURCE` without explicit review. + +## Always do this + +1. **Branch from** `origin/$TARGET` (or current stack parent), not from `$SOURCE`. +2. **Inventory:** `git diff --stat origin/$TARGET origin/$SOURCE -- <paths>` — size only; not a port list. +3. **Commit history audit** on every path that exists on `$TARGET` — see below. +4. **Classify each path:** ADD (new on target) · MODIFY (exists on target) · MERGE (build/CI/deps — both sides changed). +5. **Port:** + - **ADD:** `git checkout origin/$SOURCE -- <new-file>` is OK if the file is absent on `$TARGET`. + - **MODIFY:** start from `origin/$TARGET`; apply additive hunks from `$SOURCE` by hand. + - **MERGE:** never replace whole file; keep `$TARGET`-only lines while adding `$SOURCE` features. +6. **Pre-commit diff audit** on every modified existing file — see below. +7. **Build/test** affected modules before opening PR; state in PR body that history + diff audits were run. + +## Commit history audit (MODIFY / MERGE) + +> Read **`$TARGET` history** to protect review work. Do **not** use `$SOURCE` commit SHAs as a port shortcut. + +```bash +PATH=path/to/file + +# Target-only commits (review PRs, bugfixes never merged to source): +git log origin/$SOURCE..origin/$TARGET --oneline -- "$PATH" + +# Recent target context (PR numbers in subjects): +git log origin/$TARGET -20 --oneline -- "$PATH" + +# If your branch deletes lines, trace them on target: +git diff origin/$TARGET -- "$PATH" +git blame origin/$TARGET -L <start>,<end> -- "$PATH" +git log -1 --format='%h %s (%ci)' <commit-from-blame> +``` + +| Signal | Interpret | +|---|---| +| Non-empty `origin/$SOURCE..origin/$TARGET` log | Target has fixes source lacks — **do not overwrite** file from source | +| Empty target-only log | Still run diff audit; may be safe ADD or symmetric drift | +| Non-empty target-only log **and** substantive removals in your branch | **STOP** — read those commits before continuing | + +Optional: `git log origin/$TARGET..origin/$SOURCE --oneline -- "$PATH"` lists what source added — candidate material, **not** a license to blind checkout. + +## Pre-commit diff regression audit + +```bash +PATH=path/to/file + +# What target has that source lacks (fixes we must keep): +git diff origin/$SOURCE origin/$TARGET -- "$PATH" + +# What our branch removes from target (should be empty or whitespace/comments only): +git diff origin/$TARGET -- "$PATH" | rg '^-' | rg -v '^---|^-import |^-package |^-\s*$|^- \*|^-\s*/\*|^-\s*\*' + +# Gate: substantive removal count (threshold ~5 → investigate) +git diff origin/$TARGET -- "$PATH" | rg '^-' | rg -v '^---|^-import |^-package |^-\s*$|^- \*|^-\s*/\*|^-\s*\*' | wc -l +``` + +Cross-check: if target-only log is non-empty **and** removal count > 0, read target-only commits before committing. + +## Validation before PR + +- [ ] `SOURCE` / `TARGET` named in PR description +- [ ] Commit history audit for every modified **existing** file +- [ ] No substantive `-` lines vs `origin/$TARGET` on modified existing files +- [ ] Module compile / targeted tests for touched areas diff --git a/.cursor/rules/unomi-3-dev-backport.mdc b/.cursor/rules/unomi-3-dev-backport.mdc index 895312ec7..c69e9fa10 100644 --- a/.cursor/rules/unomi-3-dev-backport.mdc +++ b/.cursor/rules/unomi-3-dev-backport.mdc @@ -1,49 +1,55 @@ --- -description: Mandatory workflow for backporting unomi-3-dev → master (UNOMI-875 Phase 2) +description: UNOMI-875 overlay — backporting unomi-3-dev → master (extends branch-backport.mdc) alwaysApply: true --- -# unomi-3-dev → master backport rules (mandatory) +# unomi-3-dev → master (UNOMI-875 Phase 2) -Epic: **UNOMI-875**. Source branch: `unomi-3-dev`. Target: `master`. +**Generic workflow first:** `.cursor/rules/branch-backport.mdc` (history audit, diff audit, ADD/MODIFY/MERGE). -Local plan (git-ignored): `.local-notes/unomi-3-dev-backport-plan-phase2.md` +For this epic, set: -## Never do this +```bash +SOURCE=unomi-3-dev +TARGET=master +``` -- **Never** `git checkout origin/unomi-3-dev -- <path>` on files that **already exist on master** (especially Java, CI, docker compose, `build.sh`). -- **Never** port by commit hash from `unomi-3-dev` history. -- **Never** replace `master` review refactors with 3-dev monoliths (REST mappers, tracing, test harness, shell CRUD fixes). -- **Never** downgrade pinned versions on master (OpenSearch/Elasticsearch images, `pom.xml` properties) to match 3-dev without explicit review. +Local tracker (git-ignored): `.local-notes/unomi-3-dev-backport-plan-phase2.md` — §2e playbook, §1 exclusion register. -## Always do this +## Unomi-specific never -1. **Assume master may be ahead.** A diff vs `unomi-3-dev` ≠ missing feature. Check merged PRs (#771–#783, etc.) first. -2. **Start from master** for every modified file: read `origin/master` version, then cherry-pick additive hunks from 3-dev. -3. **Before staging**, run regression audit on touched paths: +- **Never** replace `master` review refactors with 3-dev monoliths (REST mappers, tracing lifecycle, test harness, shell CRUD fixes). +- **Never** port items in plan §1 exclusion register: REST exception mappers (implement on master patterns), migration scripts (UNOMI-943), security WIP, wholesale 3-dev test harness. +- **Never** wholesale-checkout `.github/workflows` (master ahead: #780, #957), `AGENTS.md` (#769), migration groovy, `.asf.yaml` rulesets. -```bash -cd "$(git rev-parse --show-toplevel)" -git fetch origin -# Master-only lines we must not drop: -git diff origin/unomi-3-dev origin/master -- <path> -# What our branch removes from master (should be empty or whitespace-only): -git diff origin/master -- <path> | rg '^-' | rg -v '^---|^-import |^-package |^-\s*$|^- \*|^-\s*/\*|^-\s*\*' -``` +## Unomi-specific always -4. **Safe to add wholesale** (new on master): new scripts, Postman JSON, docs, `build.ps1`, setup/clear helpers, new Java classes (e.g. `DistributionCompleter`). -5. **Merge surgically**: `build.sh`, `pom.xml`, CI workflows — keep master CI/non-interactive, Javadoc, IT memory sampler (#780, #957). -6. **Exclusion register** (master wins): see plan §1 — REST mappers, `EventServiceImpl`, `ParserHelper`, security WIP, migration scripts (UNOMI-943), etc. +- Check merged Phase 2 PRs (#771–#783, #755, #763, etc.) before assuming master lacks a 3-dev feature. +- **MERGE surgically:** `build.sh`, root `pom.xml`, CI — keep master non-interactive CI, Javadoc, IT memory sampler (#780, #957). +- **Docker compose:** additive only (`container_name`, debug ports); **keep** master OpenSearch/ES image pins (3-dev had 3.4.0 regressions vs master 3.0.0). ## Shell CRUD (PR J) — learned 2026-06-10 -Shell CSV/table polish from 3-dev is **mostly already on master** (#755, #763). Remaining 3-dev shell diffs often **revert** review fixes (UndeployDefinition, ConsentCrudCommand, ApiKeyCrudCommand, RuleCrudCommand, docker OpenSearch pins). Port only: +Shell CSV/table polish from 3-dev is **mostly already on master** (#755, #763). Remaining 3-dev shell diffs often **revert** review fixes (`UndeployDefinition`, `ConsentCrudCommand`, `ApiKeyCrudCommand`, `RuleCrudCommand`, docker pins). Port only: - New classes (`DistributionCompleter`) - Additive CLI flags (`Setup --show`, `@Completion`) on top of master defaults -## Validation before PR +**PR 1 incident:** `git log origin/unomi-3-dev..origin/master -- tools/shell-dev-commands/` would have flagged #755/#763 before blind checkout reverted ~20 fixes. + +## Per–mega-PR risk (plan §2e) + +| PR | Extra risk | +|---|---| +| **2** (V+U) | Wrong feature name breaks Karaf boot — diff `feature.xml` vs master names | +| **3** (L–P) | Security + persistence — never replace mappers/tracing | +| **4** (F3–F4) | Extend master `AbstractRestExceptionMapper`; do not copy 3-dev monoliths | +| **5** (K) | Router only — never bundle; never blind-checkout `extensions/router/**` | + +## Validation before PR (Unomi) + +Run generic checklist from `branch-backport.mdc`, plus: - `./build.sh --help` - `mvn -pl <affected-modules> -am compile -DskipTests` -- Regression scan: no substantive `-` lines vs `origin/master` on modified existing files +- PR description: “master-first backport; branch-backport + §2e history/diff audit run” diff --git a/AGENTS.md b/AGENTS.md index b47b552eb..8f8798315 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,14 +26,45 @@ Security model: [SECURITY.md](./SECURITY.md) Agents that scan this repository should consult `SECURITY.md` and the threat model it links before reporting issues. -## Backporting from `unomi-3-dev` (UNOMI-875) +## Branch backporting (generic) -When porting Phase 2 work from `unomi-3-dev` to `master`: +When porting changes **from a source branch to a target branch**, follow +`.cursor/rules/branch-backport.mdc`. Summary: -1. **Master is often ahead** — merged review PRs (#771–#783) improved code beyond 3-dev. A file diff does not mean master is missing the feature. -2. **Never blind-checkout** existing files from `unomi-3-dev`. Start from `origin/master` and cherry-pick additive hunks only. -3. **Audit before commit** — compare `origin/master` vs your branch; any removed logic from master is likely a regression (see `.cursor/rules/unomi-3-dev-backport.mdc`). -4. **Safe wholesale adds**: new scripts, docs, Postman collection, new Java classes. **Surgical merge**: `build.sh`, `pom.xml`, CI workflows (keep master CI/Javadoc/IT memory sampling). -5. **Do not port** items in the plan exclusion register (REST mappers, migration scripts UNOMI-943, security WIP, wholesale test harness from 3-dev). +1. **Target is canonical** for files that already exist there — branch from + `origin/<target>`, not from source. +2. **Target may be ahead** — merged review PRs on the target often improve + code beyond the source. A diff ≠ missing feature. +3. **Commit history on the target** — for each existing file: + `git log origin/<source>..origin/<target> -- <path>`. Non-empty output + means target-only fixes you must not drop. +4. **Never blind-checkout** existing files from source + (`git checkout origin/<source> -- <path>`). Port additive hunks only. +5. **Audit before commit** — diff regression scan plus history cross-check; + use `git blame origin/<target>` on any line you remove. +6. **Classify paths:** ADD (new on target) · MODIFY (hand-merge from + target) · MERGE (build/CI/deps — target wins on target-only lines). +7. **Do not** bulk-cherry-pick from source branch history without + per-commit review. -Detailed tracker: `.local-notes/unomi-3-dev-backport-plan-phase2.md` (local, git-ignored). +Cursor rule: `.cursor/rules/branch-backport.mdc` + +## Backporting `unomi-3-dev` → `master` (UNOMI-875) + +Active Phase 2 epic. **Apply generic rules above first**, then +`.cursor/rules/unomi-3-dev-backport.mdc` for Unomi-specific exclusions and +mega-PR risks. + +| | | +|---|---| +| Source | `unomi-3-dev` | +| Target | `master` | +| Local plan | `.local-notes/unomi-3-dev-backport-plan-phase2.md` (git-ignored) | + +Unomi-specific reminders: + +- Master is ahead on REST mappers, tracing, shell CRUD (#755, #763), CI + (#780, #957), docker image pins — do not revert. +- Safe wholesale adds: new scripts, Postman, docs, new Java classes. +- Do not port: migration scripts (UNOMI-943), 3-dev REST mappers, security + WIP, wholesale test harness from 3-dev. diff --git a/build.ps1 b/build.ps1 index bed45c6fd..85c024fea 100644 --- a/build.ps1 +++ b/build.ps1 @@ -5,7 +5,7 @@ # (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 +# 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, @@ -13,16 +13,45 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Error handling -$ErrorActionPreference = "Stop" -$PSDefaultParameterValues['*:ErrorAction'] = 'Stop' +################################################################################ +# +# Apache Unomi build script for Windows (PowerShell equivalent of build.sh) +# +################################################################################ + +<# +.SYNOPSIS + Builds, tests, deploys, and runs Apache Unomi on Windows. + +.DESCRIPTION + Windows counterpart to ./build.sh. Supports the same build modes: unit/integration + tests, OpenSearch/ElasticSearch IT profiles, Karaf debug, deployment, CI mode, + Javadoc validation, and integration-test tracing. Run with -Help for all options. + + Requires: Java 11+, Maven 3.6+, GraphViz (dot), tar (Windows 10+ or Git for Windows). + Integration tests additionally require Docker Desktop. + +.EXAMPLE + .\build.ps1 -Help + +.EXAMPLE + .\build.ps1 -IntegrationTests -UseOpenSearch + +.EXAMPLE + .\build.ps1 -NoKaraf -SkipTests + +.NOTES + Pair with optional setenv.ps1 in the repo root (same role as setenv.sh). + See manual/src/main/asciidoc/building-and-deploying.adoc (build.sh section). +#> -# Script parameters param( [switch]$Help, [switch]$MavenDebug, + [switch]$MavenQuiet, [switch]$Offline, [switch]$SkipTests, + [switch]$SkipUnitTests, [switch]$IntegrationTests, [switch]$Deploy, [switch]$Debug, @@ -32,640 +61,812 @@ param( [switch]$PurgeMavenCache, [string]$KarafHome, [switch]$UseOpenSearch, + [string]$Distribution, + [string]$SearchHeap, + [string]$KarafHeap, [switch]$NoKaraf, [string]$AutoStart, - [switch]$ResolverDebug + [string]$SingleTest, + [switch]$ItDebug, + [int]$ItDebugPort = 5006, + [switch]$ItDebugSuspend, + [switch]$SkipMigrationTests, + [switch]$ResolverDebug, + [switch]$KeepContainer, + [switch]$NoMemorySampler, + [int]$MemoryInterval = 30, + [switch]$Javadoc, + [string]$LogFile, + [switch]$LogFileOnly, + [switch]$Ci ) -# Global variables -$script:HasColors = $Host.UI.SupportsVirtualTerminal -$script:StartTime = $null +$ErrorActionPreference = 'Stop' +$PSDefaultParameterValues['*:ErrorAction'] = 'Stop' -# ANSI color codes (only used if terminal supports them) -if ($HasColors) { - $script:RED = "`e[0;31m" - $script:GREEN = "`e[0;32m" - $script:YELLOW = "`e[1;33m" - $script:BLUE = "`e[0;34m" - $script:MAGENTA = "`e[0;35m" - $script:CYAN = "`e[0;36m" - $script:GRAY = "`e[0;90m" - $script:BOLD = "`e[1m" - $script:NC = "`e[0m" - - # Unicode symbols - $script:CHECK_MARK = "✓" - $script:CROSS_MARK = "✗" - $script:ARROW = "➜" - $script:WARNING = "⚠" - $script:INFO = "ℹ" -} else { - # Plain text alternatives - $script:CHECK_MARK = "(+)" - $script:CROSS_MARK = "(x)" - $script:ARROW = "(>)" - $script:WARNING = "(!)" - $script:INFO = "(i)" +# Preserve invocation for IT run tracing (archive-it-run.sh reads this on Unix) +$script:BuildScriptInvocation = @($MyInvocation.Line) + +# Defaults aligned with build.sh +$script:UseMavenCache = -not $NoMavenCache +$script:ItMemorySampler = -not $NoMemorySampler +$script:KarafDebugSuspend = if ($DebugSuspend) { 'y' } else { 'n' } + +if ($Ci) { + $NoKaraf = $true + $script:UseMavenCache = $false + $env:BUILD_NON_INTERACTIVE = 'true' + $MavenQuiet = $true + $Javadoc = $true +} + +if ($PurgeMavenCache) { + $script:UseMavenCache = $false +} + +if ($UseOpenSearch -and [string]::IsNullOrWhiteSpace($Distribution)) { + $Distribution = 'unomi-distribution-opensearch' +} +if (-not [string]::IsNullOrWhiteSpace($Distribution) -and $Distribution -like '*opensearch*') { + $UseOpenSearch = $true } -# Helper functions +if ($LogFileOnly -and [string]::IsNullOrWhiteSpace($LogFile)) { + Write-Error '--log-file-only requires --log-file PATH' + exit 1 +} + +function Test-NonInteractive { + return [bool]($env:CI -or $env:GITHUB_ACTIONS -or $env:BUILD_NON_INTERACTIVE -eq 'true') +} + +function Initialize-Colors { + $script:HasColors = $false + if ($env:NO_COLOR) { return } + if (-not [Console]::IsOutputRedirected) { + $script:HasColors = $Host.UI.SupportsVirtualTerminal + } + + if ($script:HasColors) { + $script:RED = "`e[0;31m" + $script:GREEN = "`e[0;32m" + $script:YELLOW = "`e[1;33m" + $script:BLUE = "`e[0;34m" + $script:MAGENTA = "`e[0;35m" + $script:CYAN = "`e[0;36m" + $script:GRAY = "`e[0;90m" + $script:BOLD = "`e[1m" + $script:NC = "`e[0m" + $script:CHECK_MARK = '✔' + $script:CROSS_MARK = '✘' + $script:ARROW = '➜' + $script:WARNING = '⚠' + $script:INFO = 'ℹ' + } else { + $script:RED = $script:GREEN = $script:YELLOW = $script:BLUE = '' + $script:MAGENTA = $script:CYAN = $script:GRAY = $script:BOLD = $script:NC = '' + $script:CHECK_MARK = '(+)' + $script:CROSS_MARK = '(x)' + $script:ARROW = '(>)' + $script:WARNING = '(!)' + $script:INFO = '(i)' + } +} + +Initialize-Colors + function Write-Section { param([string]$Text) - $totalWidth = 80 $textLength = $Text.Length $paddingLength = [math]::Floor(($totalWidth - $textLength - 4) / 2) - $leftPadding = " " * $paddingLength - $rightPadding = " " * ($paddingLength + ($totalWidth - $textLength - 4) % 2) - - Write-Host "" - if ($HasColors) { - Write-Host "$BLUE╔════════════════════════════════════════════════════════════════════════════╗$NC" - Write-Host "$BLUE║$NC$leftPadding$BOLD$Text$NC$rightPadding$BLUE║$NC" - Write-Host "$BLUE╚════════════════════════════════════════════════════════════════════════════╝$NC" + $leftPadding = ' ' * $paddingLength + $rightPadding = ' ' * ($paddingLength + (($totalWidth - $textLength - 4) % 2)) + Write-Host '' + if ($script:HasColors) { + Write-Host "$($script:BLUE)╔════════════════════════════════════════════════════════════════════════════╗$($script:NC)" + Write-Host "$($script:BLUE)║$($script:NC)$leftPadding$($script:BOLD)$Text$($script:NC)$rightPadding$($script:BLUE)║$($script:NC)" + Write-Host "$($script:BLUE)╚════════════════════════════════════════════════════════════════════════════╝$($script:NC)" } else { - Write-Host "+------------------------------------------------------------------------------+" + Write-Host '+------------------------------------------------------------------------------+' Write-Host "| $leftPadding$Text$rightPadding |" - Write-Host "+------------------------------------------------------------------------------+" + Write-Host '+------------------------------------------------------------------------------+' } } function Write-Status { - param( - [string]$Status, - [string]$Message - ) - + param([string]$Status, [string]$Message) $symbol = switch ($Status) { - "success" { if ($HasColors) { "$GREEN$CHECK_MARK$NC" } else { $CHECK_MARK } } - "error" { if ($HasColors) { "$RED$CROSS_MARK$NC" } else { $CROSS_MARK } } - "warning" { if ($HasColors) { "$YELLOW$WARNING$NC" } else { $WARNING } } - "info" { if ($HasColors) { "$CYAN$INFO$NC" } else { $INFO } } - default { if ($HasColors) { "$BLUE$ARROW$NC" } else { $ARROW } } + 'success' { $script:CHECK_MARK } + 'error' { $script:CROSS_MARK } + 'warning' { $script:WARNING } + 'info' { $script:INFO } + default { $script:ARROW } } - $color = switch ($Status) { - "success" { $GREEN } - "error" { $RED } - "warning" { $YELLOW } - "info" { $CYAN } - default { "" } - } - - if ($HasColors) { - Write-Host " $symbol $color$Message$NC" + 'success' { $script:GREEN } + 'error' { $script:RED } + 'warning' { $script:YELLOW } + 'info' { $script:CYAN } + default { '' } + } + if ($script:HasColors -and $color) { + Write-Host " $symbol ${color}${Message}$($script:NC)" } else { Write-Host " $symbol $Message" } } -function Write-Progress { - param( - [int]$Current, - [int]$Total, - [string]$Message - ) - +function Write-BuildProgress { + param([int]$Current, [int]$Total, [string]$Message) $percentage = [math]::Floor(($Current * 100) / $Total) - - if ($HasColors) { + if ($script:HasColors) { $filled = [math]::Floor($percentage / 2) $empty = 50 - $filled - - $progress = "[" - $progress += "█" * $filled - $progress += "░" * $empty - $progress += "]" - - Write-Host "`r$CYAN$progress$NC $percentage% $GRAY$Message$NC" -NoNewline + $bar = '[' + ('█' * $filled) + ('░' * $empty) + ']' + Write-Host "`r$($script:CYAN)$bar$($script:NC) $percentage% $($script:GRAY)$Message$($script:NC)" -NoNewline } else { $filled = [math]::Floor($percentage / 4) $empty = 25 - $filled - - $progress = "[" - $progress += "#" * $filled - $progress += "-" * $empty - $progress += "]" - - Write-Host "`r$progress $percentage% $Message" -NoNewline + $bar = '[' + ('#' * $filled) + ('-' * $empty) + ']' + Write-Host "`r$bar $percentage% $Message" -NoNewline } } -function Test-Command { +function Test-CommandExists { param([string]$Name) return [bool](Get-Command -Name $Name -ErrorAction SilentlyContinue) } -function Get-ElapsedTime { - if ($null -eq $script:StartTime) { return "00:00" } - $elapsed = [math]::Floor(((Get-Date) - $script:StartTime).TotalSeconds) - return "{0:d2}:{1:d2}" -f ($elapsed / 60), ($elapsed % 60) -} - -function Start-Timer { - $script:StartTime = Get-Date +function Invoke-PromptContinue { + param([string]$PromptText = 'Continue?') + if (Test-NonInteractive) { + Write-Status 'info' "Non-interactive mode: continuing ($PromptText)" + return + } + $reply = Read-Host "$PromptText (y/N)" + if ($reply -notmatch '^[Yy]$') { exit 1 } } function Show-Usage { - if ($HasColors) { - Write-Host $CYAN - Write-Host @" + Write-Section 'Apache Unomi Build Script' + if ($script:HasColors) { Write-Host $script:CYAN } + Write-Host @' _ _ _____ _ ____ | | | | ___| | | _ \ | |__| | |__ | | | |_) | | __ | __|| | | __/ | | | | |___| |____| | |_| |_\_____|______|_| -"@ - Write-Host $NC - } else { - Write-Host @" - _ _ _____ _ ____ - | | | | ___| | | _ \ - | |__| | |__ | | | |_) | - | __ | __|| | | __/ - | | | | |___| |____| | - |_| |_\_____|______|_| -"@ - } - - Write-Host "Usage: build.ps1 [options]" - Write-Host "" - Write-Host "Options:" - if ($HasColors) { - Write-Host " $CYAN-Help$NC Show this help message" - Write-Host " $CYAN-MavenDebug$NC Enable Maven debug output" - Write-Host " $CYAN-Offline$NC Run Maven in offline mode" - Write-Host " $CYAN-SkipTests$NC Skip all tests" - Write-Host " $CYAN-IntegrationTests$NC Run integration tests" - Write-Host " $CYAN-Deploy$NC Deploy after build" - Write-Host " $CYAN-Debug$NC Run Karaf in debug mode" - Write-Host " $CYAN-DebugPort$NC <port> Set debug port (default: 5005)" - Write-Host " $CYAN-DebugSuspend$NC Suspend JVM until debugger connects" - Write-Host " $CYAN-NoMavenCache$NC Disable Maven build cache" - Write-Host " $CYAN-PurgeMavenCache$NC Purge local Maven cache before building" - Write-Host " $CYAN-KarafHome$NC <path> Set Karaf home directory for deployment" - Write-Host " $CYAN-UseOpenSearch$NC Use OpenSearch instead of ElasticSearch" - Write-Host " $CYAN-NoKaraf$NC Build without starting Karaf" - Write-Host " $CYAN-AutoStart$NC <engine> Auto-start with specified engine" - Write-Host " $CYAN-ResolverDebug$NC Enable Karaf Resolver debug logging for integration tests" - } else { - Write-Host " -Help Show this help message" - Write-Host " -MavenDebug Enable Maven debug output" - Write-Host " -Offline Run Maven in offline mode" - Write-Host " -SkipTests Skip all tests" - Write-Host " -IntegrationTests Run integration tests" - Write-Host " -Deploy Deploy after build" - Write-Host " -Debug Run Karaf in debug mode" - Write-Host " -DebugPort <port> Set debug port (default: 5005)" - Write-Host " -DebugSuspend Suspend JVM until debugger connects" - Write-Host " -NoMavenCache Disable Maven build cache" - Write-Host " -PurgeMavenCache Purge local Maven cache before building" - Write-Host " -KarafHome <path> Set Karaf home directory for deployment" - Write-Host " -UseOpenSearch Use OpenSearch instead of ElasticSearch" - Write-Host " -NoKaraf Build without starting Karaf" - Write-Host " -AutoStart <engine> Auto-start with specified engine" - Write-Host " -ResolverDebug Enable Karaf Resolver debug logging for integration tests" - } - - Write-Host "" - Write-Host "Examples:" - if ($HasColors) { - Write-Host "$GRAY# Build with integration tests using OpenSearch$NC" - Write-Host "$GRAY.\build.ps1 -IntegrationTests -UseOpenSearch$NC" - Write-Host "" - Write-Host "$GRAY# Build in debug mode$NC" - Write-Host "$GRAY.\build.ps1 -Debug -DebugPort 5006 -DebugSuspend$NC" - Write-Host "" - Write-Host "$GRAY# Deploy to specific Karaf instance$NC" - Write-Host "$GRAY.\build.ps1 -Deploy -KarafHome ~\apache-karaf$NC" - } else { - Write-Host "# Build with integration tests using OpenSearch" - Write-Host ".\build.ps1 -IntegrationTests -UseOpenSearch" - Write-Host "" - Write-Host "# Build in debug mode" - Write-Host ".\build.ps1 -Debug -DebugPort 5006 -DebugSuspend" - Write-Host "" - Write-Host "# Deploy to specific Karaf instance" - Write-Host ".\build.ps1 -Deploy -KarafHome ~\apache-karaf" - } - +'@ + if ($script:HasColors) { Write-Host $script:NC } + Write-Host '' + Write-Host 'Usage: .\build.ps1 [options]' + Write-Host '' + Write-Host 'Options:' + Write-Host ' -Help Show this help message' + Write-Host ' -SkipTests Skip all tests' + Write-Host ' -SkipUnitTests Skip unit tests (integration tests can still run)' + Write-Host ' -IntegrationTests Run integration tests' + Write-Host ' -Deploy Deploy after build' + Write-Host ' -MavenDebug Enable Maven debug output' + Write-Host ' -MavenQuiet Disable Maven download progress (quiet mode)' + Write-Host ' -Offline Run Maven in offline mode' + Write-Host ' -Debug Run Karaf in debug mode' + Write-Host ' -DebugPort PORT Set debug port (default: 5005)' + Write-Host ' -DebugSuspend Suspend JVM until debugger connects' + Write-Host ' -NoMavenCache Disable Maven build cache' + Write-Host ' -PurgeMavenCache Purge local Maven cache before building' + Write-Host ' -KarafHome PATH Set Karaf home directory for deployment' + Write-Host ' -UseOpenSearch Use OpenSearch instead of ElasticSearch' + Write-Host ' -Distribution DIST Set Unomi distribution (e.g. unomi-distribution-opensearch)' + Write-Host ' -SearchHeap SIZE Search engine heap for integration tests' + Write-Host ' -KarafHeap SIZE Karaf JVM heap for integration tests' + Write-Host ' -NoKaraf Build without starting Karaf' + Write-Host ' -AutoStart ENGINE Auto-start elasticsearch or opensearch' + Write-Host ' -SingleTest TEST Run a single integration test' + Write-Host ' -ItDebug Enable integration test debug mode' + Write-Host ' -ItDebugPort PORT Set integration test debug port (default: 5006)' + Write-Host ' -ItDebugSuspend Suspend integration test until debugger connects' + Write-Host ' -SkipMigrationTests Skip migration-related tests' + Write-Host ' -ResolverDebug Enable Karaf Resolver debug logging for integration tests' + Write-Host ' -KeepContainer Keep search engine container running after tests' + Write-Host ' -NoMemorySampler Disable JVM/system memory sampling during integration tests' + Write-Host ' -MemoryInterval SEC Memory sample interval in seconds (default: 30)' + Write-Host ' -Javadoc Build and validate Javadoc after install' + Write-Host ' -Ci CI mode: no Karaf, no Maven cache, non-interactive, Javadoc' + Write-Host ' -LogFile PATH Tee all output to PATH (console + file)' + Write-Host ' -LogFileOnly With -LogFile: write to file only, suppress console' + Write-Host '' + Write-Host 'Examples:' + Write-Host ' .\build.ps1 -IntegrationTests -UseOpenSearch' + Write-Host ' .\build.ps1 -SkipUnitTests -IntegrationTests' + Write-Host ' .\build.ps1 -Debug -DebugPort 5006 -DebugSuspend' + Write-Host ' .\build.ps1 -Deploy -KarafHome C:\apache-karaf' + Write-Host ' .\build.ps1 -NoKaraf -AutoStart opensearch' exit 0 } -# Show help if requested -if ($Help) { - Show-Usage -} +if ($Help) { Show-Usage } -# Validate AutoStart parameter if ($AutoStart -and $AutoStart -notin @('elasticsearch', 'opensearch')) { - Write-Status "error" "AutoStart must be either 'elasticsearch' or 'opensearch'" + Write-Error "AutoStart must be either 'elasticsearch' or 'opensearch'" exit 1 } -# Set environment -$ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition -$SetEnvPath = Join-Path $ScriptPath "setenv.ps1" -if (Test-Path $SetEnvPath) { - . $SetEnvPath +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$SetEnvPath = Join-Path $ScriptDir 'setenv.ps1' +if (Test-Path $SetEnvPath) { . $SetEnvPath } + +function Get-UserHome { + if ($env:USERPROFILE) { return $env:USERPROFILE } + if ($env:HOME) { return $env:HOME } + return (Get-Location).Path +} + +function Initialize-ProjectVersion { + if ($env:UNOMI_VERSION) { return } + $version = (& mvn -B -q '-DforceStdout' help:evaluate '-Dexpression=project.version' '-DinteractiveMode=false' 2>$null | Out-String).Trim() + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($version)) { + Write-Error 'Failed to detect project version from Maven' + exit 1 + } + $env:UNOMI_VERSION = $version + Write-Host "Detected project version=$($env:UNOMI_VERSION)" } -# Purge Maven cache if requested +Initialize-ProjectVersion + if ($PurgeMavenCache) { - Write-Host "Purging Maven cache..." - Remove-Item -Force -Recurse -ErrorAction SilentlyContinue ` - "$env:USERPROFILE\.m2\build-cache", - "$env:USERPROFILE\.m2\dependency-cache", - "$env:USERPROFILE\.m2\dependency-cache_v2" - Write-Host "Maven cache purged." + Write-Host 'Purging Maven cache...' + $m2 = Join-Path (Get-UserHome) '.m2' + foreach ($dir in @('build-cache', 'dependency-cache', 'dependency-cache_v2')) { + Remove-Item -Recurse -Force -ErrorAction SilentlyContinue (Join-Path $m2 $dir) + } + Write-Host 'Maven cache purged.' } -function Test-JavaVersion { - try { - $javaVersion = & java -version 2>&1 - if ($javaVersion -match 'version "(\d+)') { - $version = [int]$Matches[1] - if ($version -ge 11) { - Write-Status "success" "Java $version detected" - return $true - } else { - Write-Status "error" "Java $version detected, but version 11 or higher is required" - Write-Status "info" "To install Java 11 or higher:" - Write-Status "info" " - Windows (Chocolatey): choco install temurin11" - Write-Status "info" " - Windows (Manual): Download from https://adoptium.net" - Write-Status "info" " - Windows (Scoop): scoop install temurin11-jdk" - Write-Status "info" "For more details, see BUILDING.md in the project root" - return $false - } - } - } catch { - Write-Status "error" "Java not found" - Write-Status "info" "To install Java 11 or higher:" - Write-Status "info" " - Windows (Chocolatey): choco install temurin11" - Write-Status "info" " - Windows (Manual): Download from https://adoptium.net" - Write-Status "info" " - Windows (Scoop): scoop install temurin11-jdk" - Write-Status "info" "For more details, see BUILDING.md in the project root" +function Test-JavaRequirement { + if (-not (Test-CommandExists 'java')) { + Write-Status 'error' 'java not found' + Write-Host 'Please install Java 11 or higher from https://adoptium.net' return $false } + $javaVersion = & java -version 2>&1 | Out-String + if ($javaVersion -match 'version "(\d+)') { + $major = [int]$Matches[1] + if ($major -ge 11) { + Write-Status 'success' "Java $major detected" + return $true + } + } + Write-Status 'error' "Java version 11 or higher required: $javaVersion" return $false } -function Test-MavenVersion { - try { - $mvnVersion = & mvn -version 2>&1 - if ($mvnVersion -match 'Apache Maven (\d+)\.(\d+)') { - $major = [int]$Matches[1] - $minor = [int]$Matches[2] - if ($major -gt 3 -or ($major -eq 3 -and $minor -ge 6)) { - Write-Status "success" "Maven $major.$minor detected" - return $true - } else { - Write-Status "error" "Maven $major.$minor detected, but version 3.6 or higher is required" - Write-Status "info" "To install Maven 3.6 or higher:" - Write-Status "info" " - Windows (Chocolatey): choco install maven" - Write-Status "info" " - Windows (Manual): Download from https://maven.apache.org" - Write-Status "info" " - Windows (Scoop): scoop install maven" - Write-Status "info" "For more details, see BUILDING.md in the project root" - return $false - } - } - } catch { - Write-Status "error" "Maven not found" - Write-Status "info" "To install Maven 3.6 or higher:" - Write-Status "info" " - Windows (Chocolatey): choco install maven" - Write-Status "info" " - Windows (Manual): Download from https://maven.apache.org" - Write-Status "info" " - Windows (Scoop): scoop install maven" - Write-Status "info" "For more details, see BUILDING.md in the project root" +function Test-MavenRequirement { + if (-not (Test-CommandExists 'mvn')) { + Write-Status 'error' 'mvn not found' return $false } - return $false + if ($Offline) { + Write-Status 'success' 'Maven (offline mode enabled)' + return $true + } + $mvnVersion = (& mvn --version | Select-Object -First 1) + Write-Status 'success' $mvnVersion + return $true +} + +function Test-GraphVizRequirement { + if (-not (Test-CommandExists 'dot')) { + Write-Status 'error' 'GraphViz (dot) not found' + return $false + } + $dotVersion = & dot -V 2>&1 | Out-String + Write-Status 'success' "GraphViz: $($dotVersion.Trim())" + if (-not $env:GRAPHVIZ_DOT) { + $env:GRAPHVIZ_DOT = (Get-Command dot).Source + } + return $true } -function Test-GraphViz { +function Test-PortAvailable { + param([int]$Port) try { - $dotVersion = & dot -V 2>&1 - if ($dotVersion -match 'graphviz version') { - Write-Status "success" "GraphViz detected" - return $true - } + $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, $Port) + $listener.Start() + $listener.Stop() + return $true } catch { - Write-Status "error" "GraphViz (dot) not found - required for documentation generation" - Write-Status "info" "To install GraphViz:" - Write-Status "info" " - Windows (Chocolatey): choco install graphviz" - Write-Status "info" " - Windows (Manual): Download from https://graphviz.org" - Write-Status "info" " - Windows (Scoop): scoop install graphviz" - Write-Status "info" "For more details, see BUILDING.md in the project root" return $false } - return $false } -function Test-SystemRequirements { +function Test-DockerForIntegrationTests { + if (-not (Test-CommandExists 'docker')) { + Write-Status 'error' 'Docker is not installed or not in PATH' + Write-Host 'Integration tests require Docker. Install Docker Desktop for Windows.' + return $false + } + $null = & docker info 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Status 'error' 'Docker is not accessible' + return $false + } + Write-Status 'success' "Docker available: $((& docker --version 2>&1) -join ' ')" + return $true +} + +function Test-IntegrationTestEnvVars { + $detected = @() + if ($env:UNOMI_ELASTICSEARCH_CLUSTERNAME -or $env:UNOMI_ELASTICSEARCH_USERNAME -or $env:UNOMI_ELASTICSEARCH_PASSWORD -or + $env:UNOMI_ELASTICSEARCH_SSL_ENABLE -or $env:UNOMI_ELASTICSEARCH_SSL_TRUST_ALL_CERTIFICATES) { + $detected += 'Elasticsearch' + } + if ($env:UNOMI_OPENSEARCH_CLUSTERNAME -or $env:UNOMI_OPENSEARCH_ADDRESSES -or $env:UNOMI_OPENSEARCH_USERNAME -or + $env:UNOMI_OPENSEARCH_PASSWORD -or $env:UNOMI_OPENSEARCH_SSL_ENABLE -or $env:UNOMI_OPENSEARCH_SSL_TRUST_ALL_CERTIFICATES) { + $detected += 'OpenSearch' + } + if ($detected.Count -eq 0) { return } + Write-Status 'error' "Environment variables for $($detected -join ', ') are set and will interfere with integration tests" + Write-Host '' + Write-Host 'Clear them with clear-elasticsearch.sh / clear-opensearch.sh (Git Bash) or unset in PowerShell.' + exit 1 +} + +function Test-Requirements { + Write-Section 'System Requirements Check' $hasErrors = $false $hasWarnings = $false - - Write-Section "Checking System Requirements" - - # Check available memory - $memory = Get-CimInstance Win32_OperatingSystem - $availableMemoryGB = [math]::Round($memory.FreePhysicalMemory / 1MB, 2) - $totalMemoryGB = [math]::Round($memory.TotalVisibleMemorySize / 1MB, 2) - - if ($availableMemoryGB -lt 2) { - Write-Status "error" "Insufficient memory: $availableMemoryGB GB available, minimum 2 GB required" - $hasErrors = $true - } elseif ($availableMemoryGB -lt 4) { - Write-Status "warning" "Low memory: $availableMemoryGB GB available, recommended 4 GB or more" + + Write-Status 'info' 'Checking required tools...' + if (-not (Test-JavaRequirement)) { $hasErrors = $true } + if (-not (Test-MavenRequirement)) { $hasErrors = $true } + if (Test-CommandExists 'tar') { Write-Status 'success' 'tar' } + else { Write-Status 'error' 'tar not found (required on Windows 10+ or via Git for Windows)'; $hasErrors = $true } + if (-not (Test-GraphVizRequirement)) { $hasErrors = $true } + + if ($IsWindows) { + $os = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue + if ($os) { + $availableMemoryMB = [math]::Round($os.FreePhysicalMemory / 1024) + if ($availableMemoryMB -lt 2048) { + Write-Status 'warning' "Memory: ${availableMemoryMB}MB available (2048MB recommended)" + $hasWarnings = $true + } else { + Write-Status 'success' "Memory: ${availableMemoryMB}MB available" + } + } + + $driveName = (Get-Location).Drive.Name + if ($driveName) { + $drive = Get-PSDrive -Name $driveName -ErrorAction SilentlyContinue + if ($drive) { + $freeMB = [math]::Round($drive.Free / 1MB) + if ($freeMB -lt 1024) { + Write-Status 'warning' "Disk space: ${freeMB}MB available (1024MB recommended)" + $hasWarnings = $true + } else { + Write-Status 'success' "Disk space: ${freeMB}MB available" + } + } + } + } + + $settings = Join-Path (Get-UserHome) '.m2/settings.xml' + if (-not (Test-Path $settings)) { + Write-Status 'warning' 'Maven settings.xml not found' $hasWarnings = $true } else { - Write-Status "success" "Memory: $availableMemoryGB GB available of $totalMemoryGB GB total" - } - - # Check available disk space - $drive = Get-PSDrive -Name (Split-Path -Qualifier $PWD.Path) - $freeSpaceGB = [math]::Round($drive.Free / 1GB, 2) - $totalSpaceGB = [math]::Round(($drive.Free + $drive.Used) / 1GB, 2) - - if ($freeSpaceGB -lt 2) { - Write-Status "error" "Insufficient disk space: $freeSpaceGB GB available, minimum 2 GB required" + Write-Status 'success' 'Maven settings.xml found' + } + + if ($Debug) { + if ($DebugPort -lt 1024 -or $DebugPort -gt 65535) { + Write-Status 'error' "Debug port: $DebugPort (invalid)" + $hasErrors = $true + } elseif (-not (Test-PortAvailable $DebugPort)) { + Write-Status 'error' "Debug port: $DebugPort (already in use)" + $hasErrors = $true + } else { + Write-Status 'success' "Debug port: $DebugPort available" + } + } + + if ($Deploy) { + if ([string]::IsNullOrWhiteSpace($KarafHome)) { + Write-Status 'error' 'Karaf home directory not set for deployment' + $hasErrors = $true + } elseif (-not (Test-Path $KarafHome)) { + Write-Status 'error' "Karaf home directory does not exist: $KarafHome" + $hasErrors = $true + } elseif (-not (Test-Path (Join-Path $KarafHome 'deploy'))) { + Write-Status 'error' "Karaf deploy directory not found: $KarafHome\deploy" + $hasErrors = $true + } else { + Write-Status 'success' "Karaf home directory validated: $KarafHome" + } + } + + if ($SkipTests -and $IntegrationTests) { + Write-Status 'error' 'Cannot use -SkipTests and -IntegrationTests together' + $hasErrors = $true + } + if ($SkipTests -and $SkipUnitTests) { + Write-Status 'error' 'Cannot use -SkipTests and -SkipUnitTests together' $hasErrors = $true - } elseif ($freeSpaceGB -lt 10) { - Write-Status "warning" "Low disk space: $freeSpaceGB GB available, recommended 10 GB or more" - $hasWarnings = $true - } else { - Write-Status "success" "Disk space: $freeSpaceGB GB available of $totalSpaceGB GB total" } - - return @{ - HasErrors = $hasErrors - HasWarnings = $hasWarnings + + if ($IntegrationTests -and -not (Test-DockerForIntegrationTests)) { + $hasErrors = $true } -} -function Test-Requirements { - $hasErrors = $false - $hasWarnings = $false - - Write-Section "Checking Required Tools" - - # Check Java - if (-not (Test-JavaVersion)) { + if (($UseOpenSearch -or $AutoStart -eq 'opensearch') -and [string]::IsNullOrWhiteSpace($env:UNOMI_OPENSEARCH_PASSWORD)) { + Write-Status 'error' 'UNOMI_OPENSEARCH_PASSWORD is not set for OpenSearch' $hasErrors = $true } - - # Check Maven - if (-not (Test-MavenVersion)) { + + if (-not [string]::IsNullOrWhiteSpace($SingleTest) -and -not $IntegrationTests) { + Write-Status 'error' 'SingleTest specified but integration tests are not enabled. Use -IntegrationTests.' + $hasErrors = $true + } + if ($ItDebug -and -not $IntegrationTests) { + Write-Status 'error' 'ItDebug enabled but integration tests are not enabled. Use -IntegrationTests.' $hasErrors = $true } - - # Check GraphViz - if (-not (Test-GraphViz)) { + if ($ItDebug) { + if ($ItDebugPort -lt 1024 -or $ItDebugPort -gt 65535) { + Write-Status 'error' "Integration test debug port: $ItDebugPort (invalid)" + $hasErrors = $true + } elseif (-not (Test-PortAvailable $ItDebugPort)) { + Write-Status 'error' "Integration test debug port: $ItDebugPort (already in use)" + $hasErrors = $true + } else { + Write-Status 'success' "Integration test debug port: $ItDebugPort available" + } + } + + if ($Offline -and $PurgeMavenCache) { + Write-Status 'error' 'Cannot use -PurgeMavenCache in offline mode' $hasErrors = $true } - - # Check system requirements - $sysReq = Test-SystemRequirements - $hasErrors = $hasErrors -or $sysReq.HasErrors - $hasWarnings = $hasWarnings -or $sysReq.HasWarnings - - # Final status - Write-Host "" + + Write-Host '' if ($hasErrors) { - Write-Status "error" "One or more requirements not met. Please fix the errors above and try again." - Write-Status "info" "For more information, see BUILDING.md in the project root" - Write-Status "info" "or visit https://unomi.apache.org/contribute/building-and-deploying.html" + Write-Status 'error' 'Critical requirements not met. Please fix the errors above.' exit 1 - } elseif ($hasWarnings) { - Write-Status "warning" "Requirements met with warnings. You may proceed, but performance might be affected." + } + if ($hasWarnings) { + Write-Status 'warning' 'Some non-critical requirements not met' + Invoke-PromptContinue 'Continue despite warnings?' } else { - Write-Status "success" "All requirements met successfully!" + Write-Status 'success' 'All requirements checked successfully' } - - Write-Host "" } -# Check requirements before proceeding -Test-Requirements +Test-Requirements -# Validate OpenSearch password requirement -if ($UseOpenSearch -or ($AutoStart -and $AutoStart -eq 'opensearch')) { - if (-not $env:UNOMI_OPENSEARCH_PASSWORD -or [string]::IsNullOrWhiteSpace($env:UNOMI_OPENSEARCH_PASSWORD)) { - Write-Status "error" "UNOMI_OPENSEARCH_PASSWORD is not set for OpenSearch" - Write-Status "info" "When using OpenSearch, you must set the UNOMI_OPENSEARCH_PASSWORD environment variable before building/starting." - Write-Host "Examples:" - Write-Host " setx UNOMI_OPENSEARCH_PASSWORD yourStrongPassword (PowerShell - current user)" - Write-Host " $env:UNOMI_OPENSEARCH_PASSWORD='yourStrongPassword'; .\build.ps1 -IntegrationTests -UseOpenSearch" - exit 1 +function Get-MavenArgumentList { + $args = @() + if ($MavenDebug) { $args += '-X' } + if ($Offline) { $args += '-o' } + if (-not $script:UseMavenCache) { $args += '-Dmaven.build.cache.enabled=false' } + if ($MavenQuiet) { $args += '-ntp' } + if ($env:MAVEN_EXTRA_OPTS) { + $args += $env:MAVEN_EXTRA_OPTS.Split(' ', [System.StringSplitOptions]::RemoveEmptyEntries) + } + if (-not [string]::IsNullOrWhiteSpace($Distribution)) { + $args += "-Dunomi.distribution=$Distribution" + Write-Host "Using Unomi distribution: $Distribution" + } + if ($env:GRAPHVIZ_DOT) { + $args += "-Dgraphviz.dot.path=$($env:GRAPHVIZ_DOT)" } -} -# Function to check for conflicting environment variables before integration tests -function Test-IntegrationTestEnvVars { - $detectedVars = @() - $scriptDir = Split-Path -Parent $MyInvocation.PSCommandPath - - # Check for Elasticsearch environment variables - if ($env:UNOMI_ELASTICSEARCH_CLUSTERNAME -or - $env:UNOMI_ELASTICSEARCH_USERNAME -or - $env:UNOMI_ELASTICSEARCH_PASSWORD -or - $env:UNOMI_ELASTICSEARCH_SSL_ENABLE -or - $env:UNOMI_ELASTICSEARCH_SSL_TRUST_ALL_CERTIFICATES) { - $detectedVars += "Elasticsearch" - } - - # Check for OpenSearch environment variables - if ($env:UNOMI_OPENSEARCH_CLUSTERNAME -or - $env:UNOMI_OPENSEARCH_ADDRESSES -or - $env:UNOMI_OPENSEARCH_USERNAME -or - $env:UNOMI_OPENSEARCH_PASSWORD -or - $env:UNOMI_OPENSEARCH_SSL_ENABLE -or - $env:UNOMI_OPENSEARCH_SSL_TRUST_ALL_CERTIFICATES) { - $detectedVars += "OpenSearch" - } - - if ($detectedVars.Count -gt 0) { - Write-Status "error" "Environment variables for $($detectedVars -join ' and ') are set and will interfere with integration tests" - Write-Host "" - Write-Host "Integration tests manage their own search engine configuration and should not" - Write-Host "be run with these environment variables set." - Write-Host "" - Write-Host "To clear the environment variables, run one of the following:" - Write-Host "" - if ($detectedVars -contains "Elasticsearch") { - Write-Host " . $scriptDir\clear-elasticsearch.sh" + $profiles = @() + if ($IntegrationTests) { + Test-IntegrationTestEnvVars + if ($UseOpenSearch) { + $args += '-Duse.opensearch=true' + $args += '-Popensearch' + Write-Host 'Running integration tests with OpenSearch' + } else { + Write-Host 'Running integration tests with ElasticSearch' + } + $args += '-Pintegration-tests' + if ($SearchHeap) { + if ($UseOpenSearch) { $args += "-Dopensearch.heap=$SearchHeap" } + else { $args += "-Delasticsearch.heap=$SearchHeap" } } - if ($detectedVars -contains "OpenSearch") { - Write-Host " . $scriptDir\clear-opensearch.sh" + if ($KarafHeap) { $args += "-Dit.karaf.heap=$KarafHeap" } + if ($SingleTest) { $args += "-Dit.test=$SingleTest"; Write-Host "Running single integration test: $SingleTest" } + if ($ItDebug) { + $debugOpts = "port=$ItDebugPort" + if ($ItDebugSuspend) { $debugOpts += ',hold:true' } else { $debugOpts += ',hold:false' } + $args += "-Dit.karaf.debug=$debugOpts" } - Write-Host "" - Write-Host "Or manually unset the variables in PowerShell:" - if ($detectedVars -contains "Elasticsearch") { - Write-Host " Remove-Item Env:\UNOMI_ELASTICSEARCH_CLUSTERNAME" - Write-Host " Remove-Item Env:\UNOMI_ELASTICSEARCH_USERNAME" - Write-Host " Remove-Item Env:\UNOMI_ELASTICSEARCH_PASSWORD" - Write-Host " Remove-Item Env:\UNOMI_ELASTICSEARCH_SSL_ENABLE" - Write-Host " Remove-Item Env:\UNOMI_ELASTICSEARCH_SSL_TRUST_ALL_CERTIFICATES" + if ($SkipMigrationTests) { $args += '-Dit.test.exclude.pattern=**/migration/**/*IT.java' } + if ($KeepContainer) { $args += '-Dit.keepContainer=true' } + if ($ResolverDebug) { $args += '-Dit.unomi.resolver.debug=true' } + if ($SkipUnitTests) { $args += '-Pskip-unit-tests' } + } else { + if ($SkipTests) { + $profiles += '!integration-tests', '!run-tests' + $args += '-DskipTests' + } elseif ($SkipUnitTests) { + $args += '-Pskip-unit-tests' } - if ($detectedVars -contains "OpenSearch") { - Write-Host " Remove-Item Env:\UNOMI_OPENSEARCH_CLUSTERNAME" - Write-Host " Remove-Item Env:\UNOMI_OPENSEARCH_ADDRESSES" - Write-Host " Remove-Item Env:\UNOMI_OPENSEARCH_USERNAME" - Write-Host " Remove-Item Env:\UNOMI_OPENSEARCH_PASSWORD" - Write-Host " Remove-Item Env:\UNOMI_OPENSEARCH_SSL_ENABLE" - Write-Host " Remove-Item Env:\UNOMI_OPENSEARCH_SSL_TRUST_ALL_CERTIFICATES" + if (-not [string]::IsNullOrWhiteSpace($SingleTest)) { + Write-Status 'warning' 'SingleTest specified but integration tests are not enabled. Use -IntegrationTests.' } - Write-Host "" - Write-Host "After clearing the variables, you can run the integration tests again." - exit 1 } -} -function Get-MavenOptions { - $options = @() - - # Basic options - if ($MavenDebug) { - $options += "-X" - } - if ($Offline) { - $options += "-o" + if ($profiles.Count -gt 0) { + $args += "-P$($profiles -join ',')" } - if ($SkipTests) { - $options += "-DskipTests" - } - if ($IntegrationTests) { - # Check for conflicting environment variables before running integration tests - Test-IntegrationTestEnvVars - $options += "-Pintegration-tests" - } - if ($ResolverDebug) { - $options += "-Dit.unomi.resolver.debug=true" + return $args +} + +function Invoke-MavenPhase { + param( + [string]$Phase, + [string[]]$ExtraArgs, + [switch]$AllowFailure + ) + $mvnArgs = @($Phase) + $ExtraArgs + Write-Host "Running: mvn $($mvnArgs -join ' ')" + & mvn @mvnArgs + $code = $LASTEXITCODE + if ($code -ne 0) { + Write-Status 'error' "Maven $Phase failed" + if ($AllowFailure) { return $code } + exit $code + } + return 0 +} + +function Write-ItRunTraceStart { + param([string[]]$MvnArgs) + $traceDir = Join-Path $ScriptDir 'itests\target' + New-Item -ItemType Directory -Force -Path $traceDir | Out-Null + $traceFile = Join-Path $traceDir 'it-run-trace.properties' + $engine = if ($UseOpenSearch) { 'opensearch' } else { 'elasticsearch' } + @( + '# IT run trace (written by build.ps1 after clean, before install)' + 'trace.phase=started' + "trace.started=$((Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ'))" + "build.invocation=$($script:BuildScriptInvocation -join ' ')" + "maven.clean.command=mvn clean $($MvnArgs -join ' ')" + "maven.install.command=mvn install $($MvnArgs -join ' ')" + "use.opensearch=$UseOpenSearch" + "search.engine=$engine" + "search.heap=$SearchHeap" + "karaf.heap=$KarafHeap" + "single.test=$SingleTest" + "it.debug=$ItDebug" + "it.debug.port=$ItDebugPort" + "it.debug.suspend=$ItDebugSuspend" + "skip.migration.tests=$SkipMigrationTests" + "it.keep.container=$KeepContainer" + "it.memory.sampler=$($script:ItMemorySampler)" + "it.memory.interval=$MemoryInterval" + "maven.debug=$MavenDebug" + "maven.offline=$Offline" + "maven.quiet=$MavenQuiet" + "host=$env:COMPUTERNAME" + ) | Set-Content -Path $traceFile -Encoding UTF8 +} + +function Start-ItMemorySampler { + $sampler = Join-Path $ScriptDir 'itests\sample-it-memory.sh' + if (-not $script:ItMemorySampler -or -not (Test-Path $sampler)) { return } + if (-not (Test-CommandExists 'bash')) { + Write-Status 'warning' 'bash not found; skipping IT memory sampler' + return + } + & bash $sampler start --target-dir (Join-Path $ScriptDir 'itests\target') --interval $MemoryInterval + if ($LASTEXITCODE -ne 0) { Write-Status 'warning' 'Could not start IT memory sampler' } +} + +function Stop-ItMemorySampler { + $sampler = Join-Path $ScriptDir 'itests\sample-it-memory.sh' + if (-not (Test-Path $sampler) -or -not (Test-CommandExists 'bash')) { return } + & bash $sampler stop --target-dir (Join-Path $ScriptDir 'itests\target') 2>$null +} + +function Complete-ItRunTrace { + param([int]$ExitCode) + $traceFile = Join-Path $ScriptDir 'itests\target\it-run-trace.properties' + if (-not (Test-Path $traceFile)) { return } + Add-Content -Path $traceFile -Value "trace.phase=completed" + Add-Content -Path $traceFile -Value "trace.completed=$((Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ'))" + Add-Content -Path $traceFile -Value "maven.exit.code=$ExitCode" +} + +function Write-ItRunOperatorNote { + $sampler = Join-Path $ScriptDir 'itests\sample-it-memory.sh' + if (-not (Test-Path $sampler) -or -not (Test-CommandExists 'bash')) { return } + & bash $sampler operator-note --target-dir (Join-Path $ScriptDir 'itests\target') 2>$null +} + +$script:StartTime = $null +function Start-BuildTimer { $script:StartTime = Get-Date } +function Get-BuildElapsed { + if (-not $script:StartTime) { return '00:00' } + $elapsed = [math]::Floor(((Get-Date) - $script:StartTime).TotalSeconds) + return '{0:d2}:{1:d2}' -f [math]::Floor($elapsed / 60), ($elapsed % 60) +} + +if ($LogFile) { + $logDir = Split-Path -Parent $LogFile + if ($logDir -and -not (Test-Path $logDir)) { New-Item -ItemType Directory -Force -Path $logDir | Out-Null } + Start-Transcript -Path $LogFile -Append | Out-Null +} + +Write-Section 'Apache Unomi Build Script' +$mvnArgs = Get-MavenArgumentList + +Write-Host @' + + ____ _ _ _____ _ ____ + | _ \| | | |_ _| | | _ \ + | |_) | | | | | | | | | | | | + | _ <| | | | | | | | | | | | + | |_) | |__| |_| |_| |____| |_| | + |____/ \____/|_____|______|____/ + +'@ +Write-Host 'Building...' +Write-Host 'Estimated time: 3-5 minutes for build, 50-60 minutes with integration tests' +Start-BuildTimer + +$totalSteps = if ($Javadoc) { 3 } else { 2 } +$currentStep = 0 + +Write-BuildProgress (++$currentStep) $totalSteps 'Cleaning previous build...' +Write-Host '' +Invoke-MavenPhase -Phase 'clean' -ExtraArgs $mvnArgs | Out-Null + +if ($IntegrationTests) { + Write-ItRunTraceStart -MvnArgs $mvnArgs + Start-ItMemorySampler +} + +Write-BuildProgress (++$currentStep) $totalSteps 'Compiling and installing artifacts...' +Write-Host '' +$installExit = Invoke-MavenPhase -Phase 'install' -ExtraArgs $mvnArgs -AllowFailure +if ($IntegrationTests) { + Stop-ItMemorySampler + Complete-ItRunTrace -ExitCode $installExit + Write-ItRunOperatorNote +} +if ($installExit -ne 0) { exit $installExit } + +Write-Status 'success' "Build completed in $(Get-BuildElapsed)" + +if ($Javadoc) { + Write-Section 'Javadoc Validation' + Write-BuildProgress (++$currentStep) $totalSteps 'Generating and validating Javadoc...' + Write-Host '' + $javadocArgs = @('javadoc:javadoc', '-DskipTests') + $mvnArgs + Write-Host "Running: mvn $($javadocArgs -join ' ')" + & mvn @javadocArgs + if ($LASTEXITCODE -ne 0) { + Write-Status 'error' 'Javadoc validation failed — fix doclint errors above before pushing' + exit $LASTEXITCODE + } + Write-Status 'success' 'Javadoc validated successfully' +} + +if ($Deploy) { + Write-Section 'Deploying to Apache Karaf' + $karFile = Join-Path $ScriptDir "kar\target\unomi-kar-$($env:UNOMI_VERSION).kar" + if (-not (Test-Path $karFile)) { + Write-Status 'error' "KAR file not found: $karFile" + exit 1 } - if ($NoMavenCache) { - $options += "-Dmaven.buildcache.enabled=false" + Copy-Item -Force $karFile (Join-Path $KarafHome 'deploy\') + Write-Status 'success' 'KAR package copied successfully' + + $repo = Join-Path $KarafHome 'data\maven\repository' + if (Test-Path $repo) { + Get-ChildItem $repo | Remove-Item -Recurse -Force + Write-Status 'success' 'Karaf Maven repository purged' } - if ($UseOpenSearch) { - $options += "-Popensearch" + $tmp = Join-Path $KarafHome 'data\tmp' + if (Test-Path $tmp) { + Get-ChildItem $tmp | Remove-Item -Recurse -Force + Write-Status 'success' 'Karaf temporary files purged' } - - return $options -join " " + Write-Status 'success' 'Deployment completed' } -function Invoke-MavenBuild { - Write-Section "Building Apache Unomi" - - Start-Timer - - $mvnOptions = Get-MavenOptions - $buildCommand = "mvn clean install $mvnOptions" - - if ($ResolverDebug) { - Write-Status "info" "Karaf Resolver debug logging enabled for integration tests" +if (-not $NoKaraf) { + $packageTarget = Join-Path $ScriptDir 'package\target' + if (-not (Test-Path $packageTarget)) { + Write-Status 'error' 'Build directory not found. Did the build complete successfully?' + exit 1 } - - Write-Status "info" "Running: $buildCommand" - Write-Host "" - + + Push-Location $packageTarget try { - Invoke-Expression $buildCommand - if ($LASTEXITCODE -ne 0) { - Write-Status "error" "Maven build failed with exit code $LASTEXITCODE" - Write-Status "info" "Check the build output above for errors" - Write-Status "info" "For more information, see BUILDING.md in the project root" + $archive = Join-Path $packageTarget "unomi-$($env:UNOMI_VERSION).tar.gz" + if (-not (Test-Path $archive)) { + Write-Status 'error' "Unomi package not found: $archive" exit 1 } - Write-Status "success" "Build completed successfully in $(Get-ElapsedTime)" - } catch { - Write-Status "error" "Maven build failed: $_" - Write-Status "info" "Check the build output above for errors" - Write-Status "info" "For more information, see BUILDING.md in the project root" - exit 1 - } -} + & tar -xzf $archive + $unomiHome = Join-Path $packageTarget "unomi-$($env:UNOMI_VERSION)" -function Deploy-ToKaraf { - param( - [string]$KarafPath - ) - - Write-Section "Deploying to Apache Karaf" - - if (-not $KarafPath) { - Write-Status "error" "Karaf home directory not specified" - Write-Status "info" "Please specify the Karaf home directory using -KarafHome" - exit 1 - } - - if (-not (Test-Path $KarafPath)) { - Write-Status "error" "Karaf home directory not found: $KarafPath" - Write-Status "info" "Please ensure the directory exists and try again" - exit 1 - } - - # Stop Karaf if it's running - $karafPid = $null - if (Test-Path "$KarafPath\data\karaf.pid") { - $karafPid = Get-Content "$KarafPath\data\karaf.pid" - $process = Get-Process -Id $karafPid -ErrorAction SilentlyContinue - if ($process) { - Write-Status "info" "Stopping Karaf instance (PID: $karafPid)" - Stop-Process -Id $karafPid -Force - Start-Sleep -Seconds 5 + foreach ($pair in @( + @{ Src = Join-Path $ScriptDir 'GeoLite2-City.mmdb'; Dest = Join-Path $unomiHome 'etc\GeoLite2-City.mmdb' } + @{ Src = Join-Path $ScriptDir 'allCountries.zip'; Dest = Join-Path $unomiHome 'etc\allCountries.zip' } + )) { + if (Test-Path $pair.Src) { + Copy-Item -Force $pair.Src $pair.Dest + } } - } - - # Clean Karaf directories - Write-Status "info" "Cleaning Karaf directories" - Remove-Item -Force -Recurse -ErrorAction SilentlyContinue ` - "$KarafPath\data", - "$KarafPath\cache", - "$KarafPath\log" - - # Copy artifacts - Write-Status "info" "Copying artifacts to Karaf deploy directory" - Copy-Item -Force -Recurse "package\target\unomi-*\*" $KarafPath - - # Configure debug options if needed - if ($Debug) { - $debugOpts = "debug" - if ($DebugSuspend) { - $debugOpts += "=suspend" + + $karafOptsParts = @() + if ($AutoStart) { + $karafOptsParts += "-Dunomi.autoStart=$AutoStart" + Write-Status 'info' "Configuring auto-start for $AutoStart" } - $debugOpts += "=$DebugPort" - - $envContent = @" -EXTRA_JAVA_OPTS=`"-agentlib:jdwp=transport=dt_socket,server=y,$debugOpts`" -"@ - Set-Content -Path "$KarafPath\bin\setenv.bat" -Value $envContent - } - - # Start Karaf - if (-not $NoKaraf) { - Write-Status "info" "Starting Karaf" - $karafCmd = "$KarafPath\bin\karaf.bat" + if (-not [string]::IsNullOrWhiteSpace($Distribution)) { + $karafOptsParts += "-Dunomi.distribution=$Distribution" + Write-Status 'info' "Using Unomi distribution: $Distribution" + } + if ($karafOptsParts.Count -gt 0) { + $env:KARAF_OPTS = $karafOptsParts -join ' ' + } + if ($Debug) { - Write-Status "info" "Debug port: $DebugPort (suspend: $DebugSuspend)" + if (-not (Test-PortAvailable $DebugPort)) { + Write-Status 'error' "Port $DebugPort is already in use" + exit 1 + } + $env:KARAF_DEBUG = 'true' + $env:JAVA_DEBUG_OPTS = "-agentlib:jdwp=transport=dt_socket,server=y,suspend=$($script:KarafDebugSuspend),address=$DebugPort" + Write-Status 'info' "Debug mode enabled (port: $DebugPort, suspend: $($script:KarafDebugSuspend))" } - - if ($AutoStart) { - Write-Status "info" "Auto-starting with $AutoStart" - Start-Process $karafCmd -ArgumentList "clean" -NoNewWindow - } else { - Start-Process $karafCmd -NoNewWindow + + $karafBat = Join-Path $unomiHome 'bin\karaf.bat' + if (-not (Test-Path $karafBat)) { + Write-Status 'error' 'Karaf executable not found: karaf.bat' + exit 1 + } + Push-Location (Join-Path $unomiHome 'bin') + try { + & $karafBat + if ($LASTEXITCODE -ne 0) { + Write-Status 'error' 'Karaf failed to start' + exit $LASTEXITCODE + } + } finally { + Pop-Location } - - Write-Status "success" "Karaf started successfully" - Write-Status "info" "Use bin\client.bat to connect to the console" - Write-Status "info" "Default credentials: karaf/karaf" + } finally { + Pop-Location + } +} else { + Write-Status 'info' 'Skipping Karaf startup (-NoKaraf specified)' + if ($AutoStart) { + Write-Status 'info' "Note: auto-start ($AutoStart) will be applied when Karaf is started manually" } } -# Build and deploy -Invoke-MavenBuild +Write-Host @' -if ($Deploy) { - Deploy-ToKaraf -KarafPath $KarafHome -} + ____ ___ _ _ _____ _ + | _ \ / _ \| \ | | ____| | + | | | | | | | \| | _| | | + | |_| | |_| | |\ | |___|_| + |____/ \___/|_| \_|_____(_) + +'@ +Write-Host 'Operation completed successfully.' -Write-Status "success" "Build process completed successfully!" \ No newline at end of file +if ($LogFile) { + Stop-Transcript | Out-Null +}
