Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 10 additions & 18 deletions .github/workflows/ci_cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ jobs:

- name: Check UI dependencies
if: steps.ui-files-changed.outputs.any_modified == 'true' && steps.is_core_repo.outputs.value == 'true'
run: npx knip@5.88.0 -c ui/knip.jsonc
run: yarn knip

- name: Check deployment build.gradle
id: check_deployment_gradle
Expand Down Expand Up @@ -466,16 +466,21 @@ jobs:
- name: Define backend test command
id: test-backend-command
if: ${{ steps.skip-backend-tests.outputs.value != 'true' }}
run: echo "value=./gradlew test -x npmTest" >> $GITHUB_OUTPUT
run: |
if [ "${{ steps.is_core_repo.outputs.value }}" = "true" ]; then
echo "value=./gradlew testBackend" >> $GITHUB_OUTPUT
else
echo "value=./gradlew test -x npmTest" >> $GITHUB_OUTPUT
fi

- name: Define UI component test command
id: test-ui-components-command
if: ${{ steps.skip-ui-component-tests.outputs.value != 'true' }}
run: echo "value=./gradlew -p ui/component npmTest" >> $GITHUB_OUTPUT
run: echo "value=./gradlew testUiComponent" >> $GITHUB_OUTPUT

- name: Define UI app test command
id: test-ui-apps-command
run: echo "value=./gradlew -p ui/app npmTest" >> $GITHUB_OUTPUT
run: echo "value=./gradlew testUiApp" >> $GITHUB_OUTPUT

- name: Define manager docker build command
id: manager-docker-command
Expand Down Expand Up @@ -703,20 +708,7 @@ jobs:

- name: Check for outdated translations
if: steps.is_core_repo.outputs.value == 'true'
run: |
set -e

# Run the tests
./gradlew -p ui/app/shared processTranslations --no-parallel

out=$(git status --porcelain | grep "ui/app/shared/locales" || true)
if [[ -z "$out" ]]; then
echo "✅ No pipeline changes detected."
else
echo "❌ Pipeline files have changed. Please run './gradlew -p ui/app/shared processTranslations --no-parallel'"
echo "$out"
exit 1
fi
run: ./gradlew :ui:app:shared:checkTranslations --no-parallel
timeout-minutes: 20

- name: Run backend tests
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ui-app-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
docker compose -f profile/dev-testing.yml up -d --no-build

# Run the tests
./gradlew -p ui/app npmTest -Pargs="--shard=${{ matrix.shardIndex }}/${{ matrix.shardCount }}"
./gradlew testUiApp -Pargs="--shard=${{ matrix.shardIndex }}/${{ matrix.shardCount }}"
- name: Archive UI app test results
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
Expand Down
16 changes: 1 addition & 15 deletions .github/workflows/ui-check-outdated-translations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,4 @@ jobs:
cache: "gradle"
- name: Check for outdated translations
timeout-minutes: 20
run: |
# Check Outdated Translations
./gradlew -p ui/app/shared processTranslations --no-parallel

out=$(git status --porcelain | grep "ui/app/shared/locales" || true)
if [[ -z "$out" ]]; then
echo "✅ No pipeline changes detected."
else
echo "❌ Pipeline files have changed. Please run:
\`\`\`sh
./gradlew -p ui/app/shared processTranslations --no-parallel
\`\`\`
$out" >> $GITHUB_STEP_SUMMARY
exit 1
fi
run: ./gradlew :ui:app:shared:checkTranslations --no-parallel
2 changes: 1 addition & 1 deletion .github/workflows/ui-component-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
run: find . -name "*.tsbuildinfo" -exec touch {} +
- name: Run UI component tests
timeout-minutes: 20
run: ./gradlew -p ui/component npmTest
run: ./gradlew testUiComponent
- name: Archive UI component test results
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
timeout-minutes: 20
run: |
if [ "${{ steps.is_core_repo.outputs.value }}" = "true" ]; then
./gradlew test -x :test:test -x npmTest unitTestReport
./gradlew testBackendUnit unitTestReport
else
./gradlew test -x :test:test
fi
Expand Down
25 changes: 23 additions & 2 deletions AGENTS.md
Comment thread
wborn marked this conversation as resolved.
Resolved
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,15 @@ When naming scripts, use \<date>_\<time> as version number, date being YYYYMMDD
## Dev environment

The backend is written in Java, we target JDK 21, use modern language features up to that version during implementation.
Gradle is used as the build system; run `./gradlew clean installDist` for a full clean build.
Gradle is used as the build system:

- `./gradlew assemble` builds normal backend and UI artifacts without verification, installing the distribution, or building Docker images.
- `./gradlew check` runs backend and UI tests, Spotless, UI dependency checks, and translation checks.
- `./gradlew build` builds and verifies the project. Use this as the normal pre-PR command.
- `./gradlew installDist` prepares the runnable distribution in `manager/build/install/manager/` and the deployment Docker build context.

Use `./gradlew clean assemble` for a clean artifact build, or `./gradlew clean build` to also run verification.
Lifecycle tasks compose individual tasks, which remain independently runnable for targeted development and CI checks.
In sandboxed environments, add `--offline` (requires dependencies to already be cached).

## Tests
Expand All @@ -32,7 +40,20 @@ ex.response.withCloseable { r ->

### Running tests

When running integration tests, part of the stack (PostgreSQL and Keycloak) must be running. Start it with `docker compose -f profile/dev-testing.yml -p openremote up -d --no-build`.
- `./gradlew testBackend` runs backend unit tests followed by backend integration tests.
- `./gradlew testBackendUnit` runs backend unit tests only.
- `./gradlew testBackendIntegration` runs backend integration tests only.
- `./gradlew testUi` runs UI component tests followed by UI app end-to-end tests.
- `./gradlew testUiComponent` runs UI component tests only.
- `./gradlew testUiApp` runs UI app end-to-end tests only.
- `./gradlew test` runs `testBackend` followed by `testUi`.

Both `check` and `build` include the complete test sequence.
UI module tasks retain the `npmTest` name alongside `npmBuild`; for example, `./gradlew :ui:component:or-element:npmTest` runs one component package's tests.

Backend integration tests and UI app tests require PostgreSQL and Keycloak. Start them with `docker compose -f profile/dev-testing.yml -p openremote up -d --no-build`.

The same stack is required for `check` and `build`. Install the Playwright browser once with `./gradlew :ui:test:installBrowsers` before running UI tests. The verification lifecycle prepares the manager and UI distribution required by UI app tests; before standalone UI tests, run `./gradlew installDist` if the distribution is not already available.

## REST resources

Expand Down
34 changes: 34 additions & 0 deletions README.md
Comment thread
Ekhorn marked this conversation as resolved.
Resolved
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,40 @@ For information and how to set up a development environment, see the [Developer

We work with Java, Groovy, TypeScript, Gradle, Docker, and a wide range of APIs and protocol implementations.

Use JDK 21 and the repository's Yarn version to build from source:

```sh
./gradlew assemble # Build backend and UI artifacts
./gradlew check # Run tests, formatting, dependency, and translation checks
./gradlew build # Build and verify: the normal pre-PR command
./gradlew installDist # Prepare the runnable manager and deployment files
```

For a clean build, use `./gradlew clean assemble`, or `./gradlew clean build` to include verification.
The runnable distribution is installed in `manager/build/install/manager/`; `assemble` does not install it or build Docker images.

Before running `check` or `build`, start PostgreSQL and Keycloak with `docker compose -f profile/dev-testing.yml -p openremote up -d --no-build` and install the Playwright browser with `./gradlew :ui:test:installBrowsers`.
These lifecycle tasks compose individual tasks that remain independently runnable, allowing CI to run checks separately, reuse artifacts, and prepare distributions only where needed.

For targeted test runs:

```sh
./gradlew test # testBackend followed by testUi
./gradlew testBackend # Backend unit tests followed by integration tests
./gradlew testBackendUnit # Backend unit tests only
./gradlew testBackendIntegration # Backend integration tests only
./gradlew testUi # UI component tests followed by app end-to-end tests
./gradlew testUiComponent # UI component tests only
./gradlew testUiApp # UI app end-to-end tests only
```

UI modules retain `npmTest`, alongside `npmBuild`, for individual package tests, e.g. `./gradlew :ui:component:or-element:npmTest`.
Backend integration tests and UI app tests require the PostgreSQL and Keycloak stack described above.
UI tests also require the Playwright browser. Before standalone UI tests, prepare the runnable distribution with `./gradlew installDist` if it is not already available.
Both `check` and `build` include the complete test sequence as well as the other verification checks.

UI compiler tasks coordinate access to shared TypeScript outputs, and Knip waits for scheduled writers to finish. See [Parallel UI tasks](ui/AGENTS.md#parallel-ui-tasks) for the reasons and concurrency limits. Run `./gradlew :ui:testUiLocks` or `yarn test:ui-locks` to verify this coordination without starting backend services or a browser.

We follow the [GitHub Flow](https://docs.github.com/en/get-started/quickstart/github-flow) workflow with tags and releases for published versions of our components; when working on the codebase create descriptive branch names (e.g. `feature/cool_feature_x`, `hotfix/flux_capacitor`, `issue/123`, etc.).

When your changes are complete then create a Pull Request ensuring that your branch is up-to-date with the source branch and that code changes are covered by tests and that the full test suite passes.
Expand Down
91 changes: 74 additions & 17 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -39,25 +39,74 @@ subprojects {
}
}

tasks.register('npmTest') {
// Simplified Gradle task hierarchy. For the full dependency graph, run
// ./gradlew build --task-graph or ./gradlew :deployment:installDist --task-graph
//
// build
// ├── assemble (backend JARs, UI bundles, setup JARs)
// └── check
// ├── test
// │ ├── testBackend (testBackendUnit, testBackendIntegration)
// │ └── testUi (testUiComponent, testUiApp)
// ├── spotlessCheck (spotlessJavaCheck, spotlessGroovyCheck, etc.)
// └── Subproject checks (knip, testKnip, testUiLocks, checkTranslations, etc.)
//
// :deployment:installDist (Docker build context)
// └── Subproject installDist tasks (manager, setup, UI) and license
//
// installDist depends on the artifact-producing tasks it needs (JARs, generated
// sources, UI bundles), not on the assemble or check lifecycle aggregators.
// This avoids building unrelated artifacts or running verification tasks.

tasks.register('testBackendUnit') {
group = 'verification'
description = 'Runs all UI tests sequentially (component then app)'
description = 'Runs backend unit tests'
}

tasks.register('testBackendIntegration') {
group = 'verification'
description = 'Runs backend integration tests'
}

tasks.register('testBackend') {
group = 'verification'
description = 'Runs backend unit tests followed by backend integration tests'
dependsOn tasks.named('testBackendUnit'), tasks.named('testBackendIntegration')
}

tasks.register('testUiComponent') {
group = 'verification'
description = 'Runs UI component tests'
}

tasks.register('testUiApp') {
group = 'verification'
description = 'Runs UI app end-to-end tests'
}

tasks.register('testUi') {
group = 'verification'
description = 'Runs UI component tests followed by UI app end-to-end tests'
dependsOn tasks.named('testUiComponent'), tasks.named('testUiApp')
}

tasks.register('test') {
group = 'verification'
description = 'Executes all backend and UI tests in specific order'
// When './gradlew test' is run, ensure the UI tests are also pulled into the task graph
dependsOn 'npmTest'
description = 'Runs testBackend followed by testUi for all backend and UI tests'
dependsOn tasks.named('testBackend'), tasks.named('testUi')
}

tasks.named('assemble') {
dependsOn subprojects.collect { it.tasks.matching { t -> t.name == 'assemble' } }
}

tasks.named('check') {
dependsOn tasks.named('test')
dependsOn subprojects.collect { it.tasks.matching { t -> t.name == 'check' } }
}

// Enforce order in which we run the backend- and frontend tests
// Order: 1. Unit tests
// -> 2. Integration tests
// -> 3. UI component tests
// -> 4. UI app tests
gradle.projectsEvaluated {
def unitTests = subprojects.findAll { it.path != ':test' }
def unitTests = subprojects.findAll { it.path != ':test' && !it.path.startsWith(':ui') }
.collect { it.tasks.matching { t -> t.name == 'test' } }

def integrationTests = subprojects.findAll { it.path == ':test' }
Expand All @@ -69,15 +118,23 @@ gradle.projectsEvaluated {
def uiAppTests = subprojects.findAll { it.path.startsWith(':ui:app') }
.collect { it.tasks.matching { t -> t.name == 'npmTest' } }

tasks.named('npmTest').configure {
dependsOn(uiComponentTests, uiAppTests)
tasks.named('testBackendUnit').configure {
dependsOn unitTests
}

tasks.named('testBackendIntegration').configure {
dependsOn integrationTests
}

tasks.named('testUiComponent').configure {
dependsOn uiComponentTests
}

def rootTest = tasks.findByName('test') ?: tasks.register('test').get()
rootTest.configure {
dependsOn(unitTests, integrationTests, npmTest)
tasks.named('testUiApp').configure {
dependsOn uiAppTests
}

// Order test execution without making standalone UI test commands run backend tests.
integrationTests.forEach { group ->
group.configureEach { it.mustRunAfter(unitTests) }
}
Expand Down Expand Up @@ -156,7 +213,7 @@ tasks.register('checkCoverageDataExists') {
if (!missingJacocoResults.empty) {
println "Please run the following tasks to be able to generate a report."
println "Required tasks: ${backendTestTaskPaths.join(', ')}"
println "Run: gradle test -x npmTest checkCoverageDataExists"
println "Run: ./gradlew testBackend checkCoverageDataExists"
throw new StopExecutionException()
}
}
Expand Down
4 changes: 2 additions & 2 deletions deployment/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ tasks.register('license', Copy) {
tasks.register('installDist', Copy) {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
dependsOn(parent.getTasksByName('installDist', true).findAll {
// Don't create circular dependency or depend on built in openremote submodule apps
it.project != project && !it.project.path.startsWith(":openremote:ui:app")
// Avoid a circular dependency on this task
it.project != project
})
dependsOn(tasks.named('license'))

Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
"packageManager": "yarn@4.18.0",
"private": true,
"scripts": {
"knip": "knip -c ui/knip.ts",
"test:knip": "yarn workspace @openremote/util test:knip",
"test:ui-locks": "yarn workspace @openremote/util test:ui-locks",
"lint": "cd ui && eslint . --cache --cache-location ../.eslintcache",
"lint:fix": "cd ui && eslint . --fix --cache --cache-location ../.eslintcache",
"format": "cd ui && prettier . --write",
Expand All @@ -16,6 +19,7 @@
"ui/demo/*"
],
"devDependencies": {
"@types/node": "^26.2.0",
"@typescript-eslint/eslint-plugin": "8.40.0",
"@typescript-eslint/parser": "8.40.0",
"eslint": "8.57.1",
Expand All @@ -26,6 +30,7 @@
"eslint-plugin-n": "16.6.2",
"eslint-plugin-promise": "6.6.0",
"eslint-plugin-wc": "3.1.0",
"knip": "6.43.0",
"prettier": "3.9.6",
"typescript": "~5.9.3"
}
Expand Down
4 changes: 3 additions & 1 deletion project.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,9 @@ tasks.register('npmServeProduction', Exec) {

// Add typescript tasks
tasks.register('tscWatch', Exec) {
commandLine npmCommand("npx"), "tsc", "-b", "--watch"
dependsOn getYarnInstallTask()
// TypeScript is a root workspace dependency; keep the build target in this project.
commandLine npmCommand("yarn"), "run", "--top-level", "tsc", "-b", projectDir.absolutePath, "--watch"
}

// Configure Java build
Expand Down
4 changes: 4 additions & 0 deletions setup/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ tasks.register('notificationJar', Jar) {
from sourceSets.notification.output
}

tasks.named('assemble') {
dependsOn tasks.withType(Jar)
}

tasks.named("compileJava") {
dependsOn("load1Jar", "load2Jar")
}
Expand Down
Loading
Loading