From 46723432c04bc28225f8f65ffcbe11213ac861d4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 7 Nov 2025 10:24:38 +0000 Subject: [PATCH 01/83] IONOS(config): update submodule 6c76022 (fix(submodule-trigger-workflow): improve conditional check for submodule PR creation) https://github.com/IONOS-Productivity/ncw-config/releases/tag/6c76022 Signed-off-by: github-actions[bot] --- IONOS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IONOS b/IONOS index 82b2772197158..6c76022a44147 160000 --- a/IONOS +++ b/IONOS @@ -1 +1 @@ -Subproject commit 82b27721971588b98846717a87a9e64094c9dae3 +Subproject commit 6c76022a441471d9f96c4fa08a573375e4cc0b91 From 19debab61055bfa329e5c8d74878ce9815f1f48d Mon Sep 17 00:00:00 2001 From: Arsalan Ul Haq Sohni Date: Thu, 6 Nov 2025 15:01:38 +0100 Subject: [PATCH 02/83] IONOS(config): update submodule (remove default footer links) Signed-off-by: Arsalan Ul Haq Sohni --- IONOS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IONOS b/IONOS index 6c76022a44147..0e536e4ca0bac 160000 --- a/IONOS +++ b/IONOS @@ -1 +1 @@ -Subproject commit 6c76022a441471d9f96c4fa08a573375e4cc0b91 +Subproject commit 0e536e4ca0bac07469806275fb23632ab7bfd847 From c17b4eaab264d1cbd3bdc0b675e123abba1f0989 Mon Sep 17 00:00:00 2001 From: Tatjana Kaschperko Lindt Date: Fri, 7 Nov 2025 13:25:50 +0100 Subject: [PATCH 03/83] IONOS(ncw-config, mail): enable admin delegated settings for mail app Signed-off-by: Aliza Held --- IONOS | 2 +- apps-external/mail | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/IONOS b/IONOS index 0e536e4ca0bac..c53f52026b2d6 160000 --- a/IONOS +++ b/IONOS @@ -1 +1 @@ -Subproject commit 0e536e4ca0bac07469806275fb23632ab7bfd847 +Subproject commit c53f52026b2d66fc242656e0139cad33322761c5 diff --git a/apps-external/mail b/apps-external/mail index 4534c95c95b9f..2addc199e43cf 160000 --- a/apps-external/mail +++ b/apps-external/mail @@ -1 +1 @@ -Subproject commit 4534c95c95b9fa0e919256bd88efdb9df656a607 +Subproject commit 2addc199e43cf95ccb20ffe39b379786be9c5ffe From 2e0caa5482fff9f9a618e1e7f0b0323d110ed6bf Mon Sep 17 00:00:00 2001 From: Tatjana Kaschperko Lindt Date: Fri, 7 Nov 2025 15:21:46 +0100 Subject: [PATCH 04/83] Revert "IONOS(config): remove user_oidc submodule and build targets" This reverts commit 4d1c3e1b20a64a40b57dd5a249a931af00406ced. Signed-off-by: Tatjana Kaschperko Lindt --- .github/workflows/build-artifact.yml | 7 +++++++ .gitmodules | 3 +++ apps-external/user_oidc | 1 + 3 files changed, 11 insertions(+) create mode 160000 apps-external/user_oidc diff --git a/.github/workflows/build-artifact.yml b/.github/workflows/build-artifact.yml index fe513c501edae..e2d061bce774f 100644 --- a/.github/workflows/build-artifact.yml +++ b/.github/workflows/build-artifact.yml @@ -209,6 +209,13 @@ jobs: "has_composer": true, "makefile_target": "build_twofactor_totp_app" }, + { + "name": "user_oidc", + "path": "apps-external/user_oidc", + "has_npm": true, + "has_composer": true, + "makefile_target": "build_user_oidc_app" + }, { "name": "whiteboard", "path": "apps-external/whiteboard", diff --git a/.gitmodules b/.gitmodules index cb26b7c31205b..8011d8c0460ec 100644 --- a/.gitmodules +++ b/.gitmodules @@ -79,6 +79,9 @@ [submodule "apps-external/notifications"] path = apps-external/notifications url = git@github.com:IONOS-Productivity/nc-notifications.git +[submodule "apps-external/user_oidc"] + path = apps-external/user_oidc + url = git@github.com:nextcloud/user_oidc.git [submodule "apps-external/end_to_end_encryption"] path = apps-external/end_to_end_encryption url = git@github.com:nextcloud/end_to_end_encryption.git diff --git a/apps-external/user_oidc b/apps-external/user_oidc new file mode 160000 index 0000000000000..192826636cc8b --- /dev/null +++ b/apps-external/user_oidc @@ -0,0 +1 @@ +Subproject commit 192826636cc8b25acfb43a56617db0758869f808 From f56fc129fcda6c04b681ff0e462172e43f6a3836 Mon Sep 17 00:00:00 2001 From: Tatjana Kaschperko Lindt Date: Fri, 7 Nov 2025 15:23:38 +0100 Subject: [PATCH 05/83] IONOS(config): update submodule 1395a7b445f (re-add and disable user_oidc app) Signed-off-by: Tatjana Kaschperko Lindt --- IONOS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IONOS b/IONOS index c53f52026b2d6..1395a7b445f24 160000 --- a/IONOS +++ b/IONOS @@ -1 +1 @@ -Subproject commit c53f52026b2d66fc242656e0139cad33322761c5 +Subproject commit 1395a7b445f249ddec09400ded344de22af58d7b From 183644061a59b527d3ac12546ca41b182e2d981a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 11 Nov 2025 12:37:45 +0000 Subject: [PATCH 06/83] IONOS(config): update submodule 945436e (fix(Makefile): add precheck target for validation before builds) https://github.com/IONOS-Productivity/ncw-config/releases/tag/945436e Signed-off-by: github-actions[bot] --- IONOS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IONOS b/IONOS index 1395a7b445f24..945436e81cf68 160000 --- a/IONOS +++ b/IONOS @@ -1 +1 @@ -Subproject commit 1395a7b445f249ddec09400ded344de22af58d7b +Subproject commit 945436e81cf68f8d4080641124ac9620adf46fe8 From 50ab7e116ddb07748a3c8105b40b1794efa89391 Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Mon, 10 Nov 2025 13:36:27 +0100 Subject: [PATCH 07/83] IONOS(build): sort build targets for external apps yq '.jobs.prepare-matrix.steps[].run' .github/workflows/build-artifact.yml | sed -n "/matrix='/,/'$/p" | sed "s/.*matrix='//;s/'$//" | jq '.' > matrix.original.json jq 'sort_by(.name)' matrix.original.json > matrix.sorted.json Signed-off-by: Misha M.-Kupriyanov --- .github/workflows/build-artifact.yml | 116 +++++++++++++-------------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/.github/workflows/build-artifact.yml b/.github/workflows/build-artifact.yml index e2d061bce774f..728922cef2b12 100644 --- a/.github/workflows/build-artifact.yml +++ b/.github/workflows/build-artifact.yml @@ -56,18 +56,18 @@ jobs: # Create matrix configuration as a compact JSON string matrix='[ { - "name": "richdocuments", - "path": "apps-external/richdocuments", + "name": "activity", + "path": "apps-external/activity", "has_npm": true, "has_composer": true, - "makefile_target": "build_richdocuments_app" + "makefile_target": "build_activity_app" }, { - "name": "viewer", - "path": "apps-external/viewer", + "name": "assistant", + "path": "apps-external/assistant", "has_npm": true, "has_composer": true, - "makefile_target": "build_viewer_app" + "makefile_target": "build_assistant_app" }, { "name": "calendar", @@ -77,11 +77,18 @@ jobs: "makefile_target": "build_calendar_app" }, { - "name": "activity", - "path": "apps-external/activity", + "name": "circles", + "path": "apps-external/circles", + "has_npm": false, + "has_composer": true, + "makefile_target": "build_circles_app" + }, + { + "name": "collectives", + "path": "apps-external/collectives", "has_npm": true, "has_composer": true, - "makefile_target": "build_activity_app" + "makefile_target": "build_collectives_app" }, { "name": "contacts", @@ -91,46 +98,39 @@ jobs: "makefile_target": "build_contacts_app" }, { - "name": "collectives", - "path": "apps-external/collectives", + "name": "deck", + "path": "apps-external/deck", "has_npm": true, "has_composer": true, - "makefile_target": "build_collectives_app" - }, - { - "name": "circles", - "path": "apps-external/circles", - "has_npm": false, - "has_composer": true, - "makefile_target": "build_circles_app" + "makefile_target": "build_deck_app" }, { - "name": "notifications", - "path": "apps-external/notifications", + "name": "end_to_end_encryption", + "path": "apps-external/end_to_end_encryption", "has_npm": true, "has_composer": true, - "makefile_target": "build_notifications_app" + "makefile_target": "build_end_to_end_encryption_app" }, { - "name": "notify_push", - "path": "apps-external/notify_push", - "has_npm": false, + "name": "forms", + "path": "apps-external/forms", + "has_npm": true, "has_composer": true, - "makefile_target": "build_notify_push_app" + "makefile_target": "build_forms_app" }, { - "name": "tasks", - "path": "apps-external/tasks", + "name": "groupfolders", + "path": "apps-external/groupfolders", "has_npm": true, "has_composer": true, - "makefile_target": "build_tasks_app" + "makefile_target": "build_groupfolders_app" }, { - "name": "spreed", - "path": "apps-external/spreed", + "name": "integration_openai", + "path": "apps-external/integration_openai", "has_npm": true, "has_composer": true, - "makefile_target": "build_spreed_app" + "makefile_target": "build_integration_openai_app" }, { "name": "mail", @@ -161,32 +161,32 @@ jobs: "makefile_target": "build_notes_app" }, { - "name": "groupfolders", - "path": "apps-external/groupfolders", + "name": "notifications", + "path": "apps-external/notifications", "has_npm": true, "has_composer": true, - "makefile_target": "build_groupfolders_app" + "makefile_target": "build_notifications_app" }, { - "name": "deck", - "path": "apps-external/deck", - "has_npm": true, + "name": "notify_push", + "path": "apps-external/notify_push", + "has_npm": false, "has_composer": true, - "makefile_target": "build_deck_app" + "makefile_target": "build_notify_push_app" }, { - "name": "end_to_end_encryption", - "path": "apps-external/end_to_end_encryption", + "name": "richdocuments", + "path": "apps-external/richdocuments", "has_npm": true, "has_composer": true, - "makefile_target": "build_end_to_end_encryption_app" + "makefile_target": "build_richdocuments_app" }, { - "name": "forms", - "path": "apps-external/forms", + "name": "spreed", + "path": "apps-external/spreed", "has_npm": true, "has_composer": true, - "makefile_target": "build_forms_app" + "makefile_target": "build_spreed_app" }, { "name": "tables", @@ -195,6 +195,13 @@ jobs: "has_composer": true, "makefile_target": "build_tables_app" }, + { + "name": "tasks", + "path": "apps-external/tasks", + "has_npm": true, + "has_composer": true, + "makefile_target": "build_tasks_app" + }, { "name": "text", "path": "apps-external/text", @@ -217,25 +224,18 @@ jobs: "makefile_target": "build_user_oidc_app" }, { - "name": "whiteboard", - "path": "apps-external/whiteboard", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_whiteboard_app" - }, - { - "name": "assistant", - "path": "apps-external/assistant", + "name": "viewer", + "path": "apps-external/viewer", "has_npm": true, "has_composer": true, - "makefile_target": "build_assistant_app" + "makefile_target": "build_viewer_app" }, { - "name": "integration_openai", - "path": "apps-external/integration_openai", + "name": "whiteboard", + "path": "apps-external/whiteboard", "has_npm": true, "has_composer": true, - "makefile_target": "build_integration_openai_app" + "makefile_target": "build_whiteboard_app" } ]' From 4577d1a08a731687b3d1ab1d5c89b0a4b60bf9e1 Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Tue, 11 Nov 2025 11:09:00 +0100 Subject: [PATCH 08/83] IONOS(build): enable npm for ncw_mailtemplate in build configuration for compatibility reasons Signed-off-by: Misha M.-Kupriyanov --- .github/workflows/build-artifact.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-artifact.yml b/.github/workflows/build-artifact.yml index 728922cef2b12..18361b2caa65d 100644 --- a/.github/workflows/build-artifact.yml +++ b/.github/workflows/build-artifact.yml @@ -149,7 +149,7 @@ jobs: { "name": "ncw_mailtemplate", "path": "apps-external/ncw_mailtemplate", - "has_npm": false, + "has_npm": true, "has_composer": true, "makefile_target": "build_ncw_mailtemplate_app" }, From c55f9bceaa6dbaf618558901af36c9a4a5cd01d2 Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Mon, 10 Nov 2025 14:38:17 +0100 Subject: [PATCH 09/83] IONOS(build): add matrix validation and install dependencies in build workflow Signed-off-by: Misha M.-Kupriyanov --- .github/workflows/build-artifact.yml | 257 +++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) diff --git a/.github/workflows/build-artifact.yml b/.github/workflows/build-artifact.yml index 18361b2caa65d..91cafc4a410dd 100644 --- a/.github/workflows/build-artifact.yml +++ b/.github/workflows/build-artifact.yml @@ -50,6 +50,15 @@ jobs: outputs: external-apps-matrix: ${{ steps.set-matrix.outputs.matrix }} steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + submodules: true + fetch-depth: '1' + + - name: Install dependencies + run: sudo apt-get update && sudo apt-get install -y make jq + - name: Set matrix id: set-matrix run: | @@ -248,6 +257,254 @@ jobs: exit 1 fi + - name: Validate matrix against Makefile + run: | + set +e # Intentionally allow script to continue on error for custom error handling and reporting to GITHUB_STEP_SUMMARY + set -u # Exit on undefined variable + + echo "### 🔍 Matrix Validation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Debug: Check if apps-external exists + echo "Checking apps-external directory..." + if [ ! -d "apps-external" ]; then + echo "❌ **Error:** apps-external directory does not exist!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Directory listing:" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + ls -la >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + echo "Apps-external directory exists. Listing contents:" + ls -la apps-external/ | head -10 + + # Check if jq is available + echo "Checking if jq is installed..." + if ! command -v jq &> /dev/null; then + echo "❌ **Error:** jq is not installed!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "jq is required for matrix generation but was not found in PATH." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "jq version: $(jq --version)" + + echo "Generating matrix from Makefile..." + # Capture both stdout and stderr separately to better diagnose issues + makefile_output=$(make -f IONOS/Makefile generate_external_apps_matrix_json 2>&1) + makefile_exit_code=$? + + echo "Makefile exit code: ${makefile_exit_code}" + echo "Makefile output length: ${#makefile_output}" + + # Debug: Check if GITHUB_STEP_SUMMARY is set + echo "GITHUB_STEP_SUMMARY: ${GITHUB_STEP_SUMMARY:-NOT SET}" + + # If the Makefile command failed, show the error + if [ ${makefile_exit_code} -ne 0 ]; then + echo "" + echo "=== MAKEFILE ERROR ===" + echo "Exit code: ${makefile_exit_code}" + echo "Output:" + echo "$makefile_output" + echo "=====================" + echo "" + + # Write to summary + echo "❌ **Error:** Makefile command failed with exit code ${makefile_exit_code}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "Makefile error output" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "$makefile_output" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + + echo "Error written to summary file: ${GITHUB_STEP_SUMMARY}" + exit 1 + fi + + # Filter out the info message to get just the JSON + # The Makefile outputs "[i] Generating..." to stderr, but we captured everything with 2>&1 + # So we need to extract just the JSON part + generated_matrix=$(echo "$makefile_output" | grep -v '^\[i\]' || echo "$makefile_output") + + workflow_matrix='${{ steps.set-matrix.outputs.matrix }}' + + # Debug output + echo "Generated matrix length: ${#generated_matrix}" + echo "Workflow matrix length: ${#workflow_matrix}" + + # Show first 200 chars of generated matrix for debugging + if [ -n "$generated_matrix" ]; then + echo "Generated matrix preview: ${generated_matrix:0:200}..." + fi + + # Validate that we got valid JSON + if ! echo "$generated_matrix" | jq empty 2>/dev/null; then + echo "❌ **Error:** Generated matrix is not valid JSON" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "Invalid JSON output" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "$generated_matrix" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + # Validate that we got data + if [ -z "$generated_matrix" ] || [ -z "$workflow_matrix" ]; then + echo "❌ **Error:** Failed to load matrices" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- Generated matrix empty: $([ -z "$generated_matrix" ] && echo "yes" || echo "no")" >> $GITHUB_STEP_SUMMARY + echo "- Workflow matrix empty: $([ -z "$workflow_matrix" ] && echo "yes" || echo "no")" >> $GITHUB_STEP_SUMMARY + echo "- Makefile exit code: ${makefile_exit_code}" >> $GITHUB_STEP_SUMMARY + + exit 1 + fi + + # Sort both matrices for comparison + generated_sorted=$(echo "$generated_matrix" | jq -S '.' 2>&1 || echo "ERROR") + workflow_sorted=$(echo "$workflow_matrix" | jq -S '.' 2>&1 || echo "ERROR") + + echo "Sorted matrix lengths - generated: ${#generated_sorted}, workflow: ${#workflow_sorted}" + + # Compare the two matrices + if [ "$generated_sorted" = "$workflow_sorted" ]; then + echo "✅ **Validation passed!** The workflow matrix matches the Makefile configuration." >> $GITHUB_STEP_SUMMARY + echo "" + echo "✅ Matrix validation passed!" + else + echo "❌ **Validation failed!** The workflow matrix does not match the Makefile configuration." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + echo "Starting detailed comparison..." + + # Extract app names from both matrices + generated_apps=$(echo "$generated_matrix" | jq -r '.[].name' 2>/dev/null | sort || echo "") + workflow_apps=$(echo "$workflow_matrix" | jq -r '.[].name' 2>/dev/null | sort || echo "") + + echo "Generated apps count: $(echo "$generated_apps" | wc -l)" + echo "Workflow apps count: $(echo "$workflow_apps" | wc -l)" + + # Find missing apps (in Makefile but not in workflow) + missing_apps=$(comm -23 <(echo "$generated_apps") <(echo "$workflow_apps")) + if [ $? -ne 0 ]; then + echo "Error: comm command failed when finding missing apps." >&2 + exit 1 + fi + # Find extra apps (in workflow but not in Makefile) + extra_apps=$(comm -13 <(echo "$generated_apps") <(echo "$workflow_apps")) + if [ $? -ne 0 ]; then + echo "Error: comm command failed when finding extra apps." >&2 + exit 1 + fi + + echo "Missing apps: ${missing_apps:-none}" + echo "Extra apps: ${extra_apps:-none}" + + if [ -n "$missing_apps" ]; then + echo "#### âš ī¸ Missing Apps" >> $GITHUB_STEP_SUMMARY + echo "The following apps are configured in the Makefile but missing from the workflow:" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "$missing_apps" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + fi + + if [ -n "$extra_apps" ]; then + echo "#### âš ī¸ Extra Apps" >> $GITHUB_STEP_SUMMARY + echo "The following apps are in the workflow but not configured in the Makefile:" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "$extra_apps" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + fi + + # Check for configuration mismatches in common apps + common_apps=$(comm -12 <(echo "$generated_apps") <(echo "$workflow_apps") 2>/dev/null || echo "") + + echo "Common apps count: $(echo "$common_apps" | wc -l)" + + if [ -n "$common_apps" ]; then + mismatched_apps="" + + while IFS= read -r app; do + [ -z "$app" ] && continue + gen_config=$(echo "$generated_matrix" | jq -c --arg app "$app" '.[] | select(.name == $app)' 2>/dev/null || echo "") + wf_config=$(echo "$workflow_matrix" | jq -c --arg app "$app" '.[] | select(.name == $app)' 2>/dev/null || echo "") + + if [ -n "$gen_config" ] && [ -n "$wf_config" ] && [ "$gen_config" != "$wf_config" ]; then + mismatched_apps="${mismatched_apps}${app}"$'\n' + fi + done <<< "$common_apps" + + echo "Mismatched apps: ${mismatched_apps:-none}" + + if [ -n "$mismatched_apps" ]; then + echo "#### âš ī¸ Configuration Mismatches" >> $GITHUB_STEP_SUMMARY + echo "The following apps have different configurations (has_npm, has_composer, etc.):" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "$mismatched_apps" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "📋 Detailed differences" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo '```diff' >> $GITHUB_STEP_SUMMARY + + while IFS= read -r app; do + [ -z "$app" ] && continue + echo "=== $app ===" >> $GITHUB_STEP_SUMMARY + diff -u --label "Workflow" --label "Makefile" \ + <(echo "$workflow_matrix" | jq --arg app "$app" '.[] | select(.name == $app)' 2>/dev/null || echo "{}") \ + <(echo "$generated_matrix" | jq --arg app "$app" '.[] | select(.name == $app)' 2>/dev/null || echo "{}") \ + >> $GITHUB_STEP_SUMMARY 2>&1 || true + done <<< "$mismatched_apps" + + echo '```' >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + fi + fi + + # Provide fix instructions + echo "#### 🔧 How to Fix" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Run this command locally to generate the correct matrix:" >> $GITHUB_STEP_SUMMARY + echo '```bash' >> $GITHUB_STEP_SUMMARY + echo "make -f IONOS/Makefile generate_external_apps_matrix_json" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Then update the \`matrix\` variable in \`.github/workflows/build-artifact.yml\` in the set-matrix step with the generated output." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Show full diff in expandable section + echo "
" >> $GITHUB_STEP_SUMMARY + echo "📄 Full matrix comparison" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Workflow Matrix:**" >> $GITHUB_STEP_SUMMARY + echo '```json' >> $GITHUB_STEP_SUMMARY + echo "$workflow_matrix" | jq '.' 2>/dev/null >> $GITHUB_STEP_SUMMARY || echo "$workflow_matrix" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Makefile Matrix:**" >> $GITHUB_STEP_SUMMARY + echo '```json' >> $GITHUB_STEP_SUMMARY + echo "$generated_matrix" | jq '.' 2>/dev/null >> $GITHUB_STEP_SUMMARY || echo "$generated_matrix" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + + echo "" + echo "❌ ERROR: Matrix validation failed!" + echo "See the job summary for details on what's wrong and how to fix it." + echo "Summary file size: $(wc -c < $GITHUB_STEP_SUMMARY || echo 0) bytes" + exit 1 + fi + build-external-apps: runs-on: ubuntu-latest needs: prepare-matrix From f990419e40c77bb020df18fabe4afe4baa0a567a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 12 Nov 2025 09:11:42 +0000 Subject: [PATCH 10/83] IONOS(config): update submodule 3dc76d7 (refactor(Makefile): refactored your Makefile to separate complex bash logic into standalone script.) https://github.com/IONOS-Productivity/ncw-config/releases/tag/3dc76d7 Signed-off-by: github-actions[bot] --- IONOS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IONOS b/IONOS index 945436e81cf68..3dc76d7819e43 160000 --- a/IONOS +++ b/IONOS @@ -1 +1 @@ -Subproject commit 945436e81cf68f8d4080641124ac9620adf46fe8 +Subproject commit 3dc76d7819e4385a51e948d9468b1bac340c48b7 From 44eb5dc4148f192891265de3fc1da58095924869 Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Tue, 11 Nov 2025 14:49:28 +0100 Subject: [PATCH 11/83] IONOS(build): add missing password_policy app to build configuration Signed-off-by: Misha M.-Kupriyanov --- .github/workflows/build-artifact.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/build-artifact.yml b/.github/workflows/build-artifact.yml index 91cafc4a410dd..a2cbfdb849649 100644 --- a/.github/workflows/build-artifact.yml +++ b/.github/workflows/build-artifact.yml @@ -183,6 +183,13 @@ jobs: "has_composer": true, "makefile_target": "build_notify_push_app" }, + { + "name": "password_policy", + "path": "apps-external/password_policy", + "has_npm": true, + "has_composer": true, + "makefile_target": "build_password_policy_app" + }, { "name": "richdocuments", "path": "apps-external/richdocuments", From 61b997c091cc98f64b3b34467534ff7cfd03eeaa Mon Sep 17 00:00:00 2001 From: Arsalan Ul Haq Sohni Date: Mon, 17 Nov 2025 15:19:31 +0100 Subject: [PATCH 12/83] IONOS(user_ldap): handle user mapping exceptions for user_limit reached Signed-off-by: Arsalan Ul Haq Sohni --- apps/user_ldap/lib/Access.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/apps/user_ldap/lib/Access.php b/apps/user_ldap/lib/Access.php index f97396c5a9a32..05eff159e0ac1 100644 --- a/apps/user_ldap/lib/Access.php +++ b/apps/user_ldap/lib/Access.php @@ -598,7 +598,21 @@ public function dn2ocname($fdn, $ldapName = null, $isUser = true, &$newlyMapped ) ) { $this->connection->setConfiguration(['ldapCacheTTL' => $originalTTL]); - $newlyMapped = $this->mapAndAnnounceIfApplicable($mapper, $fdn, $intName, $uuid, $isUser); + try { + $newlyMapped = $this->mapAndAnnounceIfApplicable($mapper, $fdn, $intName, $uuid, $isUser); + } catch (HintException $e) { + // User limit reached - log and return false + $this->logger->warning( + 'Could not map {dn} as {name}: {message}.', + [ + 'app' => 'user_ldap', + 'dn' => $fdn, + 'name' => $intName, + 'message' => $e->getMessage(), + ] + ); + return false; + } if ($newlyMapped) { $this->logger->debug('Mapped {fdn} as {name}', ['fdn' => $fdn,'name' => $intName]); return $intName; From 00a6ba43e0051d065b95e03dd55d23fc07fbdcf4 Mon Sep 17 00:00:00 2001 From: Tatjana Kaschperko Lindt Date: Wed, 19 Nov 2025 13:29:13 +0100 Subject: [PATCH 13/83] IONOS(core-notifier): reduce the user_limit_reached message Signed-off-by: Tatjana Kaschperko Lindt --- core/Notification/CoreNotifier.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/core/Notification/CoreNotifier.php b/core/Notification/CoreNotifier.php index 83a86513e03c1..3915ae5d6e1d3 100644 --- a/core/Notification/CoreNotifier.php +++ b/core/Notification/CoreNotifier.php @@ -59,16 +59,7 @@ public function prepare(INotification $notification, string $languageCode): INot if ($notification->getSubject() === 'user_limit_reached') { $notification->setParsedSubject($l->t('The account limit of this instance is reached.')); - $notification->setParsedMessage($l->t('Enter your subscription key in the support app in order to increase the account limit. This does also grant you all additional benefits that Nextcloud Enterprise offers and is highly recommended for the operation in companies.')); $notification->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'places/contacts.svg'))); - $action = $notification->createAction(); - $label = $l->t('Learn more ↗'); - $link = $this->config->getSystemValueString('one-click-instance.link', 'https://nextcloud.com/enterprise/'); - $action->setLabel($label) - ->setParsedLabel($label) - ->setLink($link, IAction::TYPE_WEB) - ->setPrimary(true); - $notification->addParsedAction($action); return $notification; } From fda334fed7e6eb58d29269b211286dc3bb468bd0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 12 Dec 2025 14:01:12 +0000 Subject: [PATCH 14/83] IONOS(config): update submodule c18550d (fix(Makefile): add support for apps requiring composer with --no-scripts and npm build) https://github.com/IONOS-Productivity/ncw-config/releases/tag/c18550d Signed-off-by: github-actions[bot] --- IONOS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IONOS b/IONOS index 3dc76d7819e43..c18550dbc23c9 160000 --- a/IONOS +++ b/IONOS @@ -1 +1 @@ -Subproject commit 3dc76d7819e4385a51e948d9468b1bac340c48b7 +Subproject commit c18550dbc23c91aaf20f15c62027cc1c812c9525 From 18ea03bc08f7fe5dcf343538bdb9598d9f0de569 Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Thu, 6 Nov 2025 10:04:23 +0100 Subject: [PATCH 15/83] IONOS(theming): fix user bubble color Signed-off-by: Kai Henseler --- apps/theming/css/ionos/files.css | 8 ++++++++ apps/theming/lib/Themes/IonosTheme.php | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/theming/css/ionos/files.css b/apps/theming/css/ionos/files.css index bb9cc032896a1..01d6f4eca6522 100644 --- a/apps/theming/css/ionos/files.css +++ b/apps/theming/css/ionos/files.css @@ -480,3 +480,11 @@ div.v-popper--theme-dropdown.v-popper__popper:has(.v-popper__wrapper), .header-m min-width: 0; } } + +/* Sidebar */ +aside#app-sidebar-vue { + div.user-bubble__content { + background-color: var(--ion-surface-secondary); + color: var(--ion-text); + } +} diff --git a/apps/theming/lib/Themes/IonosTheme.php b/apps/theming/lib/Themes/IonosTheme.php index cc325edd1d961..c62ca67d18025 100644 --- a/apps/theming/lib/Themes/IonosTheme.php +++ b/apps/theming/lib/Themes/IonosTheme.php @@ -214,7 +214,7 @@ public function getCSSVariables(): array { // used for different active/hover/focus/disabled states '--color-background-hover' => 'light-dark( var(--ion-color-blue-b1), var(--ion-color-blue-b8))', - '--color-background-dark' => 'light-dark( ' . $this->util->darken($colorMainBackground, 7) . ', var(--ion-color-blue-b6))', + '--color-background-dark' => $this->util->darken($colorMainBackground, 7), '--color-background-darker' => $this->util->darken($colorMainBackground, 14), '--color-placeholder-light' => $this->util->darken($colorMainBackground, 10), From 5ce432ca86aa86fc5e745ddd1d9cb11a33df92a2 Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Thu, 6 Nov 2025 10:04:50 +0100 Subject: [PATCH 16/83] IONOS(theming): adjust tasks app theming Signed-off-by: Kai Henseler --- apps/theming/css/ionos/tasks.css | 50 ++++++++++++++++++++++++++ apps/theming/lib/Themes/IonosTheme.php | 5 +-- 2 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 apps/theming/css/ionos/tasks.css diff --git a/apps/theming/css/ionos/tasks.css b/apps/theming/css/ionos/tasks.css new file mode 100644 index 0000000000000..1621839c9edaa --- /dev/null +++ b/apps/theming/css/ionos/tasks.css @@ -0,0 +1,50 @@ +div.app-tasks { + main#app-content-vue { + background-color: var(--color-main-background) !important; + + .header { + background-color: var(--color-main-background) !important; + } + + .task-item__body .task-body__icons .date { + color: var(--ion-text); + } + } + aside.app-sidebar { + ul.vs__dropdown-menu { + background-color: var(--ion-context-menu-background); + border: 1px solid var(--ion-context-menu-border); + + li.vs__dropdown-option { + &:hover { + background-color: var(--ion-context-menu-item-background-hover); + } + + &:active { + background-color: var(--ion-context-menu-item-background-active); + } + + div.multiselect-picker-option { + color: var(--ion-context-menu-item-text); + } + } + } + } + + .property__item .item__content .content__input input[type=number] { + background-color: var(--color-main-background); + color: var(--ion-text); + } + + nav#app-navigation-vue { + div#nc-vue-3 li select { + background-color: var(--ion-context-menu-background); + color: var(--ion-text); + + option { + background-color: var(--ion-context-menu-background); + color: var(--ion-text); + } + } + } +} diff --git a/apps/theming/lib/Themes/IonosTheme.php b/apps/theming/lib/Themes/IonosTheme.php index c62ca67d18025..b78c55c97c49e 100644 --- a/apps/theming/lib/Themes/IonosTheme.php +++ b/apps/theming/lib/Themes/IonosTheme.php @@ -16,7 +16,7 @@ class IonosTheme extends DefaultTheme implements ITheme { private const FONT_PATH_PREFIX = 'fonts/OpenSans/'; // CSS file paths for custom styling - private const CSS_FILES = [ + private const array CSS_FILES = [ 'variables.css', 'buttons.css', 'sidebar.css', @@ -25,7 +25,8 @@ class IonosTheme extends DefaultTheme implements ITheme { 'files.css', 'settings.css', '_layout.css', - 'tables.css' + 'tables.css', + 'tasks.css' ]; public function getId(): string { From 86d5b7e51448a4799ae0eb414f1a837d71f4f0f3 Mon Sep 17 00:00:00 2001 From: Aliza Held Date: Thu, 13 Nov 2025 15:06:53 +0100 Subject: [PATCH 17/83] IONOS(mail): update submodule to hide new mail creation for existing ionos email account Signed-off-by: Aliza Held Signed-off-by: Arsalan Ul Haq Sohni --- apps-external/mail | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps-external/mail b/apps-external/mail index 2addc199e43cf..71abf4ac19487 160000 --- a/apps-external/mail +++ b/apps-external/mail @@ -1 +1 @@ -Subproject commit 2addc199e43cf95ccb20ffe39b379786be9c5ffe +Subproject commit 71abf4ac194871f024f765167716cc490ab242e4 From 547ab71374dddb99346ede0910c3bfd0786864cc Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Thu, 6 Nov 2025 14:49:08 +0100 Subject: [PATCH 18/83] IONOS(theming): improve app manager colors Signed-off-by: Kai Henseler --- apps/theming/css/ionos/apps.css | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/apps/theming/css/ionos/apps.css b/apps/theming/css/ionos/apps.css index 393835d8f4e82..2208f1cc1e0dc 100644 --- a/apps/theming/css/ionos/apps.css +++ b/apps/theming/css/ionos/apps.css @@ -1,3 +1,21 @@ -#app-content { +.app-content { padding-bottom: 100px; + + tr.app-item.app-item--list-view { + background-color: var(--ion-files-list-background); + &:hover { + background-color: var(--ion-files-list-background-hover); + } + + td.app-image.app-image-icon { + svg { + fill: var(--ion-text); + } + image { + @media (prefers-color-scheme: dark) { + filter: none; + } + } + } + } } From ee2edc963da0ae6ab47985ac7187d89cea25af4d Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Thu, 6 Nov 2025 14:51:27 +0100 Subject: [PATCH 19/83] IONOS(theming): improve chat app colors Signed-off-by: Kai Henseler --- apps/theming/css/ionos/talk.css | 39 ++++++++++++++++++++++++++ apps/theming/lib/Themes/IonosTheme.php | 1 + 2 files changed, 40 insertions(+) create mode 100644 apps/theming/css/ionos/talk.css diff --git a/apps/theming/css/ionos/talk.css b/apps/theming/css/ionos/talk.css new file mode 100644 index 0000000000000..579dbc1fdd253 --- /dev/null +++ b/apps/theming/css/ionos/talk.css @@ -0,0 +1,39 @@ +div.app-talk.content { + .messages-group__date-text, li.message.message--hovered .message-body.normal-message-body { + background-color: var(--ion-surface-secondary); + color: var(--ion-text); + } + + .left-sidebar__settings-button-container { + button { + background-color: var(--ion-button-sidebar-background) !important; + color: var(--ion-button-sidebar-text); + + &:hover { + background-color: var(--ion-button-sidebar-background-hover) !important; + } + + &:active { + background-color: var(--ion-button-sidebar-background-active) !important; + } + } + } + + .rich-contenteditable__input--disabled { + background-color: light-dark(var(--ion-color-cool-grey-c2), var(--ion-color-cool-grey-c6)); + color: var(--ion-text); + cursor: not-allowed; + } +} + +.v-popper__inner { + .nc-emoji-picker-container { + button.emoji-mart-anchor { + color: var(--ion-button--icon-only-text); + + &.emoji-mart-anchor-selected, &:hover { + color: light-dark(var(--ion-color-blue-b7), var(--ion-text)) !important; + } + } + } +} diff --git a/apps/theming/lib/Themes/IonosTheme.php b/apps/theming/lib/Themes/IonosTheme.php index b78c55c97c49e..3d1f2f54d719c 100644 --- a/apps/theming/lib/Themes/IonosTheme.php +++ b/apps/theming/lib/Themes/IonosTheme.php @@ -26,6 +26,7 @@ class IonosTheme extends DefaultTheme implements ITheme { 'settings.css', '_layout.css', 'tables.css', + 'talk.css', 'tasks.css' ]; From 5835b28309495b9467cd348a34b9ba5f27b9cdd2 Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Mon, 10 Nov 2025 09:39:56 +0100 Subject: [PATCH 20/83] IONOS(theming): fix search button hover color Signed-off-by: Kai Henseler --- apps/theming/css/ionos/files.css | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/theming/css/ionos/files.css b/apps/theming/css/ionos/files.css index 01d6f4eca6522..2fc17c72cc282 100644 --- a/apps/theming/css/ionos/files.css +++ b/apps/theming/css/ionos/files.css @@ -307,13 +307,19 @@ div.v-popper--theme-dropdown.v-popper__popper:has(.v-popper__wrapper), .header-m background-color: var(--ion-dialog-filter-button-background); &:hover { background-color: var(--ion-button-tertiary-background-hover); + + .button-vue__wrapper { + span.button-vue__text, .button-vue__icon svg { + color: var(--ion-button-tertiary-text-hover); + } + } } &:active { background-color: var(--ion-button-tertiary-background-active); } span.button-vue__wrapper { span.button-vue__text, .button-vue__icon svg { - color: var(--ion-button-tertiary-text) !important; + color: var(--ion-button-tertiary-text); } } } From 146a6741e180b09fdb8de701a76acf6c8afdc2f8 Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Mon, 10 Nov 2025 09:40:43 +0100 Subject: [PATCH 21/83] IONOS(theming): fix search menu darkmode color Signed-off-by: Kai Henseler --- apps/theming/css/ionos/files.css | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/theming/css/ionos/files.css b/apps/theming/css/ionos/files.css index 2fc17c72cc282..b40a5e087ba41 100644 --- a/apps/theming/css/ionos/files.css +++ b/apps/theming/css/ionos/files.css @@ -444,13 +444,14 @@ div.v-popper--theme-dropdown.v-popper__popper:has(.v-popper__wrapper), .header-m .v-popper__popper { .v-popper__inner { background-color: var(--ion-surface-dialog) !important; + color: var(--ion-context-menu-item-text); border-radius: inherit; .searchable-list__list { - li button { + li[role=button] button { background-color: transparent !important; &:hover, &:active { - background-color: var(--ion-files-list-background-hover) !important; + background-color: var(--ion-context-menu-item-background-hover) !important; } } } From 28c2135733cbbe0a8c3ec42ea0402f1712429626 Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Mon, 10 Nov 2025 12:17:16 +0100 Subject: [PATCH 22/83] IONOS(theming): adjust colors for calendar app Signed-off-by: Kai Henseler --- apps/theming/css/ionos/calendar.css | 107 +++++++++++++++++++++++++ apps/theming/lib/Themes/IonosTheme.php | 3 + 2 files changed, 110 insertions(+) create mode 100644 apps/theming/css/ionos/calendar.css diff --git a/apps/theming/css/ionos/calendar.css b/apps/theming/css/ionos/calendar.css new file mode 100644 index 0000000000000..62bff44cdb036 --- /dev/null +++ b/apps/theming/css/ionos/calendar.css @@ -0,0 +1,107 @@ +.app-calendar.content { + .fc-daygrid-day.fc-day { + &.fc-day-other { + background-color: light-dark(var(--ion-color-cool-grey-c1), var(--ion-color-cool-grey-c8)) !important; + } + + &.fc-day-today { + background-color: light-dark(var(--ion-color-blue-b1), var(--ion-color-cool-grey-c7)) !important; + + .fc-event { + box-shadow: none !important; + } + } + + .fc-daygrid-week-number { + background-color: light-dark(var(--ion-color-cool-grey-c2), var(--ion-color-cool-grey-c8)); + color: var(--ion-text); + } + } + .fc-daygrid-dot-event:hover { + background-color: light-dark(var(--ion-color-cool-grey-c1), var(--ion-color-cool-grey-c8)); + } + + .fc-timeGridWeek-view { + .fc-col-header-cell.fc-day-today, .fc-timegrid-col.fc-day-today{ + background-color: light-dark(var(--ion-color-blue-b1), var(--ion-color-cool-grey-c7)) !important; + } + } + + .fc-multiMonthYear-view { + .fc-day-disabled { + background-color: light-dark(var(--ion-color-cool-grey-c1), var(--ion-color-cool-grey-c8)); + } + } + + .fc-list-table { + .fc-list-day-cushion.fc-cell-shaded { + background-color: light-dark(var(--ion-color-cool-grey-c2), var(--ion-color-cool-grey-c8)); + color: var(--ion-text); + } + } +} + +.modal-mask.calendar-edit-full { + .app-full__header__top, input, textarea, .vs__dropdown-toggle { + background-color: var(--ion-surface-dialog) !important; + } +} + +.appointment-config-modal, .modal-container__content { + input, textarea, .vs__dropdown-toggle, select { + background-color: var(--ion-surface-dialog) !important; + } +} + +.v-popper__wrapper { + .v-popper__inner .event-popover__inner { + .property-title .property-title__input input, textarea, #date-time-picker-input, .vs__dropdown-toggle { + background-color: var(--ion-surface-dialog); + } + } + + .v-popper__arrow-container { + border-right-color: var(--ion-surface-dialog) !important; + } +} + +ul.vs__dropdown-menu.vs__dropdown-menu--floating { + background-color: var(--ion-surface-dialog); +} + + +.app-calendar.nc-guest-content { + .mx-calendar-content { + td.cell.disabled { + background-color: light-dark(var(--ion-color-cool-grey-c1), var(--ion-color-cool-grey-c8)); + } + } + + .button-vue--vue-secondary { + background-color: var(--ion-button-secondary-background-default); + border: var(--ion-button-secondary-border-default); + color: var(--ion-button-secondary-text); + + &:hover { + background-color: var(--ion-button-secondary-background-hover); + color: var(--ion-button-secondary-text-hover); + } + + &:active { + background-color: var(--ion-button-secondary-background-active); + color: var(--ion-button-secondary-text-active); + } + + &:disabled { + background-color: var(--ion-button-secondary-background-disabled); + border: var(--ion-button-secondary-border-disabled); + color: var(--ion-button-secondary-text-disabled); + } + } +} + +header#header:has(+ .app-calendar.nc-guest-content) { + .header-info { + display: none; + } +} diff --git a/apps/theming/lib/Themes/IonosTheme.php b/apps/theming/lib/Themes/IonosTheme.php index 3d1f2f54d719c..9db2c5f4d2c4d 100644 --- a/apps/theming/lib/Themes/IonosTheme.php +++ b/apps/theming/lib/Themes/IonosTheme.php @@ -17,6 +17,7 @@ class IonosTheme extends DefaultTheme implements ITheme { // CSS file paths for custom styling private const array CSS_FILES = [ + 'calendar.css', 'variables.css', 'buttons.css', 'sidebar.css', @@ -219,6 +220,8 @@ public function getCSSVariables(): array { '--color-background-dark' => $this->util->darken($colorMainBackground, 7), '--color-background-darker' => $this->util->darken($colorMainBackground, 14), + '--color-background-plain' => $ionColorMainBackground, + '--color-placeholder-light' => $this->util->darken($colorMainBackground, 10), '--color-placeholder-dark' => $this->util->darken($colorMainBackground, 20), From e52b4ead0374175115645ef7b550f9f7352157c3 Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Mon, 10 Nov 2025 13:12:42 +0100 Subject: [PATCH 23/83] IONOS(theming): load all css files from theme folder Signed-off-by: Kai Henseler --- apps/theming/lib/Themes/IonosTheme.php | 32 +++++++++++++------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/apps/theming/lib/Themes/IonosTheme.php b/apps/theming/lib/Themes/IonosTheme.php index 9db2c5f4d2c4d..0f80dee28c595 100644 --- a/apps/theming/lib/Themes/IonosTheme.php +++ b/apps/theming/lib/Themes/IonosTheme.php @@ -15,21 +15,21 @@ class IonosTheme extends DefaultTheme implements ITheme { private const FONT_FAMILY = 'Open sans'; private const FONT_PATH_PREFIX = 'fonts/OpenSans/'; - // CSS file paths for custom styling - private const array CSS_FILES = [ - 'calendar.css', - 'variables.css', - 'buttons.css', - 'sidebar.css', - 'apps.css', - 'guest.css', - 'files.css', - 'settings.css', - '_layout.css', - 'tables.css', - 'talk.css', - 'tasks.css' - ]; + // CSS file paths for custom styling - dynamically loaded from ionos directory + private function getCssFiles(): array { + $cssDir = __DIR__ . '/../../css/' . self::THEME_ID . '/'; + $files = glob($cssDir . '*.css'); + $cssFiles = []; + + foreach ($files as $file) { + $cssFiles[] = basename($file); + } + + // Sort to ensure consistent loading order + sort($cssFiles); + + return $cssFiles; + } public function getId(): string { return self::THEME_ID; @@ -65,7 +65,7 @@ public function getCustomCss(): string { */ private function loadCustomCssFiles(): string { $customCss = ''; - foreach (self::CSS_FILES as $file) { + foreach ($this->getCssFiles() as $file) { $customCss .= file_get_contents(__DIR__ . '/../../css/' . self::THEME_ID . '/' . $file) . PHP_EOL; } From f5db9710127acc9848414386b1b4d0097e5b2953 Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Mon, 10 Nov 2025 13:13:04 +0100 Subject: [PATCH 24/83] IONOS(theming): adjust profile app colors Signed-off-by: Kai Henseler --- apps/theming/css/ionos/profile.css | 9 +++++++++ apps/theming/lib/Themes/IonosTheme.php | 6 ++++-- 2 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 apps/theming/css/ionos/profile.css diff --git a/apps/theming/css/ionos/profile.css b/apps/theming/css/ionos/profile.css new file mode 100644 index 0000000000000..b3930e94a4c8b --- /dev/null +++ b/apps/theming/css/ionos/profile.css @@ -0,0 +1,9 @@ +.app-profile.content { + .user-actions__other.action-items img { + filter: none; + @media (prefers-color-scheme: dark) { + filter: invert(100%); + } + + } +} diff --git a/apps/theming/lib/Themes/IonosTheme.php b/apps/theming/lib/Themes/IonosTheme.php index 0f80dee28c595..6e9881fd2da2f 100644 --- a/apps/theming/lib/Themes/IonosTheme.php +++ b/apps/theming/lib/Themes/IonosTheme.php @@ -205,9 +205,11 @@ public function getCSSVariables(): array { '--ion-shadow-header' => '0 4px 8px rgba(0, 0, 0, 0.12)', '--header-height' => '64px', '--color-main-background' => $ionColorMainBackground, - '--color-main-background-rgb' => $colorMainBackgroundRGB, + '--color-main-background-rbg-light' => join(',', $this->util->hexToRGB('#ffffff')), + '--color-main-backgrond-rgb-dark' => join(',', $this->util->hexToRGB($ionColorBlueB8)), + '--color-main-background-rgb' => 'light-dark(var(--color-main-background-rbg-light), var(--color-main-backgrond-rgb-dark))', '--color-main-background-translucent' => 'rgba(var(--color-main-background-rgb), .97)', - '--color-main-background-blur' => 'rgba(var(--color-main-background-rgb), .8)', + '--color-main-background-blur' => 'light-dark(#ffffffcc, #02102Bcc)', '--color-primary' => $colorPrimary, '--color-primary-element' => $colorPrimary, '--color-primary-element-light' => 'light-dark( #e5ebf3, var(--ion-color-blue-b5))', From 94eea8bc8b9b4609afca23826c56cef83609521b Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Mon, 10 Nov 2025 14:11:33 +0100 Subject: [PATCH 25/83] IONOS(theming): fix assistant navigation text color Signed-off-by: Kai Henseler --- apps/theming/css/ionos/assistant.css | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 apps/theming/css/ionos/assistant.css diff --git a/apps/theming/css/ionos/assistant.css b/apps/theming/css/ionos/assistant.css new file mode 100644 index 0000000000000..ef291ce618d31 --- /dev/null +++ b/apps/theming/css/ionos/assistant.css @@ -0,0 +1,7 @@ +div#assistantTextProcessingModal { + div.app-navigation__body { + div.empty-content.task-list--empty { + color: var(--ion-text); + } + } +} From 518351ea0ffdd98b0beb68149f8d3f7b0b2df798 Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Mon, 10 Nov 2025 14:11:53 +0100 Subject: [PATCH 26/83] IONOS(theming): fix notification settings colors Signed-off-by: Kai Henseler --- apps/theming/css/ionos/settings.css | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/apps/theming/css/ionos/settings.css b/apps/theming/css/ionos/settings.css index dcc8ce972ed2b..4a46220569c2f 100644 --- a/apps/theming/css/ionos/settings.css +++ b/apps/theming/css/ionos/settings.css @@ -3,4 +3,26 @@ div#content.app-settings { width: 100%; height: calc(100vh - var(--header-height)); border-radius: 0; + + + .notification-frequency__select { + background-color: var(--ion-context-menu-background); + color: var(--ion-text); + + option { + background-color: var(--ion-context-menu-background); + color: var(--ion-text); + } + } + table.grid.activitysettings { + tbody tr:not(.group-header) { + &:hover { + background-color: var(--ion-files-list-background-hover); + } + + .checkbox-radio-switch__icon { + color: var(--ion-color-blue-b4); + } + } + } } From 093e19f4f3f366f81ac8dbfc45d5abc61f1b7124 Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Wed, 12 Nov 2025 10:28:43 +0100 Subject: [PATCH 27/83] IONOS(theming): fix notes suggestion colors Signed-off-by: Kai Henseler --- apps/theming/css/ionos/notes.css | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 apps/theming/css/ionos/notes.css diff --git a/apps/theming/css/ionos/notes.css b/apps/theming/css/ionos/notes.css new file mode 100644 index 0000000000000..9b30e1f71d5de --- /dev/null +++ b/apps/theming/css/ionos/notes.css @@ -0,0 +1,18 @@ +div.suggestion-list { + .suggestion-list__item { + color: var(--ion-context-menu-item-text); + &:hover { + background-color: var(--ion-context-menu-item-background-hover); + } + &.is-selected { + background-color: var(--ion-context-menu-item-background-active); + } + + img { + filter: none; + @media (prefers-color-scheme: dark) { + filter: invert(100%); + } + } + } +} From c5c03c4a1a8065cec497491fa6fa496de264761c Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Wed, 12 Nov 2025 11:53:36 +0100 Subject: [PATCH 28/83] IONOS(theming): fix tables app colors Signed-off-by: Kai Henseler --- apps/theming/css/ionos/sidebar.css | 5 +++ apps/theming/css/ionos/tables.css | 56 ++++++++++++++++++++++++++++-- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/apps/theming/css/ionos/sidebar.css b/apps/theming/css/ionos/sidebar.css index 615ac435873b7..31b2fd852edcd 100644 --- a/apps/theming/css/ionos/sidebar.css +++ b/apps/theming/css/ionos/sidebar.css @@ -135,3 +135,8 @@ div.app-navigation, div#app-navigation { } } } + +.app-navigation-entry__counter-wrapper .counter-bubble__counter { + background-color: var(--ion-context-menu-background) !important; + color: var(--ion-text) !important; +} diff --git a/apps/theming/css/ionos/tables.css b/apps/theming/css/ionos/tables.css index dea47638e7995..603e8cf398b08 100644 --- a/apps/theming/css/ionos/tables.css +++ b/apps/theming/css/ionos/tables.css @@ -1,4 +1,54 @@ -.content.app-tables table thead tr th { - background: var(--ion-tables-header-background-color); - border: 1px solid #ccc; +.content.app-tables { + table { + thead tr th { + background: var(--ion-tables-header-background-color); + border: 1px solid #ccc; + } + + tbody tr { + &.selected { + background-color: var(--ion-files-list-background-hover) !important; + } + &:hover { + background-color: var(--ion-files-list-background-hover); + } + } + } + + ~div.dialog__modal { + div.user-bubble__content { + background-color: var(--ion-context-menu-background); + color: var(--ion-text); + } + + .text-editor__wrapper.is-rich-editor .editor { + background-color: var(--ion-context-menu-background); + color: var(--ion-text); + border: 1px solid var(--ion-context-menu-border); + } + + .dialog__content.app-settings__content { + .column-entry { + color: var(--ion-text); + &:hover { + background-color: var(--ion-files-list-background-hover); + } + } + + li.action:has(.action-checkbox) { + &:hover { + background-color: var(--ion-files-list-background-hover); + } + } + + .filter-section { + .filter-group { + border-left-color: var(--ion-color-cool-grey-c7) !important; + } + .row { + background-color: transparent; + } + } + } + } } From f66e5638cc326c7ebb273ea36f4be192d094c00d Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Wed, 12 Nov 2025 12:47:13 +0100 Subject: [PATCH 29/83] IONOS(theming): rename css variable to reflect broader usage Signed-off-by: Kai Henseler --- apps/theming/css/ionos/apps.css | 2 +- apps/theming/css/ionos/files.css | 6 +++--- apps/theming/css/ionos/settings.css | 2 +- apps/theming/css/ionos/tables.css | 8 ++++---- apps/theming/css/ionos/variables.css | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/theming/css/ionos/apps.css b/apps/theming/css/ionos/apps.css index 2208f1cc1e0dc..f2e642fdb66a4 100644 --- a/apps/theming/css/ionos/apps.css +++ b/apps/theming/css/ionos/apps.css @@ -4,7 +4,7 @@ tr.app-item.app-item--list-view { background-color: var(--ion-files-list-background); &:hover { - background-color: var(--ion-files-list-background-hover); + background-color: var(--ion-list-background-hover); } td.app-image.app-image-icon { diff --git a/apps/theming/css/ionos/files.css b/apps/theming/css/ionos/files.css index b40a5e087ba41..0f591bab4d80c 100644 --- a/apps/theming/css/ionos/files.css +++ b/apps/theming/css/ionos/files.css @@ -141,14 +141,14 @@ div.files-list__header:has(div.files-list__breadcrumbs), .file-picker { div[data-cy-files-list].files-list { .files-list__row { &:hover { - background-color: var(--ion-files-list-background-hover); + background-color: var(--ion-list-background-hover); .favorite-marker-icon svg path { stroke: var(--color-main-background); } } &:active, &.files-list__row--active, &:has(.checkbox-radio-switch--checked) { - background-color: var(--ion-files-list-background-active); + background-color: var(--ion-list-background-active); .favorite-marker-icon svg path { stroke: var(--color-main-background); @@ -381,7 +381,7 @@ div.v-popper--theme-dropdown.v-popper__popper:has(.v-popper__wrapper), .header-m tr.file-picker__row { &:hover, &.file-picker__row--selected { - background-color: var(--ion-files-list-background-hover); + background-color: var(--ion-list-background-hover); @media (prefers-color-scheme: dark) { background-color: var(--ion-color-cool-grey-c7); diff --git a/apps/theming/css/ionos/settings.css b/apps/theming/css/ionos/settings.css index 4a46220569c2f..fae279f518f7f 100644 --- a/apps/theming/css/ionos/settings.css +++ b/apps/theming/css/ionos/settings.css @@ -17,7 +17,7 @@ div#content.app-settings { table.grid.activitysettings { tbody tr:not(.group-header) { &:hover { - background-color: var(--ion-files-list-background-hover); + background-color: var(--ion-list-background-hover); } .checkbox-radio-switch__icon { diff --git a/apps/theming/css/ionos/tables.css b/apps/theming/css/ionos/tables.css index 603e8cf398b08..c80f01c852791 100644 --- a/apps/theming/css/ionos/tables.css +++ b/apps/theming/css/ionos/tables.css @@ -7,10 +7,10 @@ tbody tr { &.selected { - background-color: var(--ion-files-list-background-hover) !important; + background-color: var(--ion-list-background-hover) !important; } &:hover { - background-color: var(--ion-files-list-background-hover); + background-color: var(--ion-list-background-hover); } } } @@ -31,13 +31,13 @@ .column-entry { color: var(--ion-text); &:hover { - background-color: var(--ion-files-list-background-hover); + background-color: var(--ion-list-background-hover); } } li.action:has(.action-checkbox) { &:hover { - background-color: var(--ion-files-list-background-hover); + background-color: var(--ion-list-background-hover); } } diff --git a/apps/theming/css/ionos/variables.css b/apps/theming/css/ionos/variables.css index 2af74751d3f89..a9cdef54f1d95 100644 --- a/apps/theming/css/ionos/variables.css +++ b/apps/theming/css/ionos/variables.css @@ -13,8 +13,8 @@ --ion-breadcrumb-text-active: var(--ion-color-blue-b5); --ion-breadcrumb-text-disabled: var(--ion-color-cool-grey-c4); - --ion-files-list-background-hover: light-dark(var(--ion-color-blue-b1), var(--ion-color-cool-grey-c8)); - --ion-files-list-background-active: light-dark(var(--ion-color-blue-b2), var(--ion-color-cool-grey-c6)); + --ion-list-background-hover: light-dark(var(--ion-color-blue-b1), var(--ion-color-cool-grey-c8)); + --ion-list-background-active: light-dark(var(--ion-color-blue-b2), var(--ion-color-cool-grey-c6)); --ion-files-list-icon: var(--ion-color-blue-b4); --ion-context-menu-background: var(--color-main-background); From cc877dc4a3a0405dc9fe4cc8a840e9b1d0b8390f Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Thu, 20 Nov 2025 07:39:24 +0100 Subject: [PATCH 30/83] IONOS(theming): fix missing hover color for secondary button Signed-off-by: Kai Henseler --- apps/theming/css/ionos/buttons.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/theming/css/ionos/buttons.css b/apps/theming/css/ionos/buttons.css index 6c9891118850f..9a737c5ef1a97 100644 --- a/apps/theming/css/ionos/buttons.css +++ b/apps/theming/css/ionos/buttons.css @@ -37,12 +37,20 @@ &:hover:not(:disabled):not(.button-vue--disabled) { background-color: var(--ion-button-secondary-background-hover); border-color: var(--ion-button-secondary-background-hover); + color: var(--ion-button-secondary-text-hover); &:not(.action-item--single) { .button-vue__text, .button-vue__icon svg { color: var(--ion-button-secondary-text-hover); } } + + + &:not(.action-item__menutoggle) { + .button-vue__text, .button-vue__icon svg { + color: var(--ion-button-secondary-text-hover); + } + } } &:active:not(:disabled):not(.button-vue--disabled) { From 16fd978c6c33f357310bf67e69d454a7fa6082f2 Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Thu, 20 Nov 2025 07:40:00 +0100 Subject: [PATCH 31/83] IONOS(theming): make user bubble visible everywhere Signed-off-by: Kai Henseler --- apps/theming/css/ionos/_layout.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/theming/css/ionos/_layout.css b/apps/theming/css/ionos/_layout.css index 3120f27d8845a..844d2fc3e9631 100644 --- a/apps/theming/css/ionos/_layout.css +++ b/apps/theming/css/ionos/_layout.css @@ -139,3 +139,8 @@ background-color: var(--ion-surface-secondary); } } + +div.user-bubble__content:has(.user-bubble__name) { + color: var(--ion-text); + background-color: var(--ion-surface-secondary); +} From 39ef51f5a485cab30874635e2d932378602e61ee Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Thu, 20 Nov 2025 07:40:56 +0100 Subject: [PATCH 32/83] IONOS(theming): invert activity timeline icons in darkmode Signed-off-by: Kai Henseler --- apps/theming/css/ionos/activity.css | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 apps/theming/css/ionos/activity.css diff --git a/apps/theming/css/ionos/activity.css b/apps/theming/css/ionos/activity.css new file mode 100644 index 0000000000000..a2a7b52cecd78 --- /dev/null +++ b/apps/theming/css/ionos/activity.css @@ -0,0 +1,8 @@ +.content.app-activity { + img { + filter: none; + @media (prefers-color-scheme: dark) { + filter: invert(100%); + } + } +} From f84451f84fb41eff3bc5576a9f4e136ce3e8f4b5 Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Thu, 20 Nov 2025 07:57:40 +0100 Subject: [PATCH 33/83] IONOS(theming/deck): fix icon hover color Signed-off-by: Kai Henseler --- apps/theming/css/ionos/buttons.css | 4 ++-- apps/theming/css/ionos/variables.css | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/theming/css/ionos/buttons.css b/apps/theming/css/ionos/buttons.css index 9a737c5ef1a97..e2d38e8f60a69 100644 --- a/apps/theming/css/ionos/buttons.css +++ b/apps/theming/css/ionos/buttons.css @@ -204,8 +204,8 @@ &:hover:not(:disabled):not(.button-vue--disabled):not(&.modal-container__close):not(.icon-collapse):not(.files-list__header-grid-button) { background-color: var(--ion-button--icon-only-background-hover); - .button-vue__icon span[role=img]>svg { - color: var(--ion-button--icon-only-text-hover); + .button-vue__icon span[role=img]>svg, svg { + color: var(--ion-button--icon-only-text-hover) !important; } } diff --git a/apps/theming/css/ionos/variables.css b/apps/theming/css/ionos/variables.css index a9cdef54f1d95..79bdefdb4c1ff 100644 --- a/apps/theming/css/ionos/variables.css +++ b/apps/theming/css/ionos/variables.css @@ -52,7 +52,7 @@ --ion-button--icon-only-text: light-dark(var(--ion-color-blue-b4), var(--ion-color-cool-grey-c3)); --ion-button--icon-only-background-hover: light-dark(var(--ion-color-blue-b4), var(--ion-color-blue-b4)); --ion-button--icon-only-background-active: light-dark(var(--ion-color-blue-b6), var(--ion-color-blue-b3)); - --ion-button--icon-only-text-hover: #fff; + --ion-button--icon-only-text-hover: light-dark(#fff, var(--ion-color-secondary)); --ion-button-sidebar-background: transparent; --ion-button-sidebar-background-hover: light-dark(var(--ion-color-cool-grey-c2), var(--ion-color-cool-grey-c7)); From deecb25413386d3e615bb64e3b4b15474b0c9ab7 Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Mon, 10 Nov 2025 13:36:27 +0100 Subject: [PATCH 34/83] IONOS(build): sort build targets for external apps yq '.jobs.prepare-matrix.steps[].run' .github/workflows/build-artifact.yml | sed -n "/matrix='/,/'$/p" | sed "s/.*matrix='//;s/'$//" | jq '.' > matrix.original.json jq 'sort_by(.name)' matrix.original.json > matrix.sorted.json Signed-off-by: Misha M.-Kupriyanov --- .github/workflows/build-artifact.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-artifact.yml b/.github/workflows/build-artifact.yml index a2cbfdb849649..6ec8b263037d2 100644 --- a/.github/workflows/build-artifact.yml +++ b/.github/workflows/build-artifact.yml @@ -107,25 +107,25 @@ jobs: "makefile_target": "build_contacts_app" }, { - "name": "deck", - "path": "apps-external/deck", - "has_npm": true, + "name": "notify_push", + "path": "apps-external/notify_push", + "has_npm": false, "has_composer": true, - "makefile_target": "build_deck_app" + "makefile_target": "build_notify_push_app" }, { - "name": "end_to_end_encryption", - "path": "apps-external/end_to_end_encryption", + "name": "richdocuments", + "path": "apps-external/richdocuments", "has_npm": true, "has_composer": true, - "makefile_target": "build_end_to_end_encryption_app" + "makefile_target": "build_richdocuments_app" }, { - "name": "forms", - "path": "apps-external/forms", + "name": "spreed", + "path": "apps-external/spreed", "has_npm": true, "has_composer": true, - "makefile_target": "build_forms_app" + "makefile_target": "build_spreed_app" }, { "name": "groupfolders", From 583640a6bce0791a30723b3597e2fef857f9b91d Mon Sep 17 00:00:00 2001 From: Arsalan Ul Haq Sohni Date: Fri, 21 Nov 2025 08:31:01 +0100 Subject: [PATCH 35/83] IONOS(build): fixup build artifact file matrix for external apps Signed-off-by: Arsalan Ul Haq Sohni --- .github/workflows/build-artifact.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-artifact.yml b/.github/workflows/build-artifact.yml index 6ec8b263037d2..a2cbfdb849649 100644 --- a/.github/workflows/build-artifact.yml +++ b/.github/workflows/build-artifact.yml @@ -107,25 +107,25 @@ jobs: "makefile_target": "build_contacts_app" }, { - "name": "notify_push", - "path": "apps-external/notify_push", - "has_npm": false, + "name": "deck", + "path": "apps-external/deck", + "has_npm": true, "has_composer": true, - "makefile_target": "build_notify_push_app" + "makefile_target": "build_deck_app" }, { - "name": "richdocuments", - "path": "apps-external/richdocuments", + "name": "end_to_end_encryption", + "path": "apps-external/end_to_end_encryption", "has_npm": true, "has_composer": true, - "makefile_target": "build_richdocuments_app" + "makefile_target": "build_end_to_end_encryption_app" }, { - "name": "spreed", - "path": "apps-external/spreed", + "name": "forms", + "path": "apps-external/forms", "has_npm": true, "has_composer": true, - "makefile_target": "build_spreed_app" + "makefile_target": "build_forms_app" }, { "name": "groupfolders", From 820b8742b1b72b2ebac373d6ba68cd48ff9ba34a Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Mon, 10 Nov 2025 13:12:42 +0100 Subject: [PATCH 36/83] IONOS(theming): load all css files from theme folder Signed-off-by: Kai Henseler --- apps/theming/lib/Themes/IonosTheme.php | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/apps/theming/lib/Themes/IonosTheme.php b/apps/theming/lib/Themes/IonosTheme.php index 6e9881fd2da2f..5b5df0a46a46e 100644 --- a/apps/theming/lib/Themes/IonosTheme.php +++ b/apps/theming/lib/Themes/IonosTheme.php @@ -15,19 +15,28 @@ class IonosTheme extends DefaultTheme implements ITheme { private const FONT_FAMILY = 'Open sans'; private const FONT_PATH_PREFIX = 'fonts/OpenSans/'; - // CSS file paths for custom styling - dynamically loaded from ionos directory + private static ?array $cachedCssFiles = null; + private function getCssFiles(): array { + if (self::$cachedCssFiles !== null) { + return self::$cachedCssFiles; + } + $cssDir = __DIR__ . '/../../css/' . self::THEME_ID . '/'; $files = glob($cssDir . '*.css'); + if ($files === false) { + self::$cachedCssFiles = []; + return []; + } + $cssFiles = []; - foreach ($files as $file) { $cssFiles[] = basename($file); } - - // Sort to ensure consistent loading order + sort($cssFiles); - + self::$cachedCssFiles = $cssFiles; + return $cssFiles; } From d5ec50cb1ae99cea5f6b1ff36dba962ab16b2e63 Mon Sep 17 00:00:00 2001 From: Aliza Held Date: Thu, 13 Nov 2025 14:36:13 +0100 Subject: [PATCH 37/83] IONOS(theming): fix collabora vertical shift and copilot suggestion for css file content loading Signed-off-by: Aliza Held Signed-off-by: Arsalan Ul Haq Sohni --- apps/theming/css/ionos/collabora.css | 4 ++++ apps/theming/lib/Themes/IonosTheme.php | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 apps/theming/css/ionos/collabora.css diff --git a/apps/theming/css/ionos/collabora.css b/apps/theming/css/ionos/collabora.css new file mode 100644 index 0000000000000..1e46a1fb19012 --- /dev/null +++ b/apps/theming/css/ionos/collabora.css @@ -0,0 +1,4 @@ +/* More specific selector to override default Collabora viewer positioning */ +html body .viewer__content:not(.viewer--split) .office-viewer:not(.viewer__file--hidden):not(.widget-file) { + top: auto !important; +} diff --git a/apps/theming/lib/Themes/IonosTheme.php b/apps/theming/lib/Themes/IonosTheme.php index 5b5df0a46a46e..7139820873a31 100644 --- a/apps/theming/lib/Themes/IonosTheme.php +++ b/apps/theming/lib/Themes/IonosTheme.php @@ -75,7 +75,11 @@ public function getCustomCss(): string { private function loadCustomCssFiles(): string { $customCss = ''; foreach ($this->getCssFiles() as $file) { - $customCss .= file_get_contents(__DIR__ . '/../../css/' . self::THEME_ID . '/' . $file) . PHP_EOL; + $filePath = __DIR__ . '/../../css/' . self::THEME_ID . '/' . $file; + $content = @file_get_contents($filePath); + if ($content !== false) { + $customCss .= $content . PHP_EOL; + } } return rtrim($customCss, PHP_EOL); From 51f3493a05f24e16a30e7b56328cb99558f65c2f Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Wed, 3 Dec 2025 10:26:41 +0100 Subject: [PATCH 38/83] IONOS(ncw_mailtemplate): update submodule (fix broken mails) Signed-off-by: Kai Henseler --- apps-external/ncw_mailtemplate | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps-external/ncw_mailtemplate b/apps-external/ncw_mailtemplate index 2a98825d483a1..d3c66eb42a98b 160000 --- a/apps-external/ncw_mailtemplate +++ b/apps-external/ncw_mailtemplate @@ -1 +1 @@ -Subproject commit 2a98825d483a12f08e377a4da2191f2789af2caf +Subproject commit d3c66eb42a98b0a55063f6c36386091be47d9799 From 9b9c9c799f7d5885ac5860417d30f4ace6f9e4d6 Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Thu, 4 Dec 2025 16:32:40 +0100 Subject: [PATCH 39/83] IONOS(theming): fix text editor background color Signed-off-by: Kai Henseler --- apps/theming/css/ionos/files.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/theming/css/ionos/files.css b/apps/theming/css/ionos/files.css index 0f591bab4d80c..7ab7ab03028b1 100644 --- a/apps/theming/css/ionos/files.css +++ b/apps/theming/css/ionos/files.css @@ -290,6 +290,11 @@ div.v-popper--theme-dropdown.v-popper__popper:has(.v-popper__wrapper), .header-m &:has(.viewer__content) { background-color: transparent; + + .editor__content pre { + background-color: var(--ion-surface-secondary); + color: var(--ion-text); + } } &:has(.nc-generic-dialog) { From 0721b4f2f0d88e2ee3ed4ffdc5f305d8fd831410 Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Mon, 8 Dec 2025 13:19:10 +0100 Subject: [PATCH 40/83] IONOS(AppsMenu): update submodule (fixed styling) Signed-off-by: Kai Henseler --- apps-external/ncw_apps_menu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps-external/ncw_apps_menu b/apps-external/ncw_apps_menu index eb4a355cd3336..42cbd59ddda96 160000 --- a/apps-external/ncw_apps_menu +++ b/apps-external/ncw_apps_menu @@ -1 +1 @@ -Subproject commit eb4a355cd3336a4ab0aa9b949c0faa966a2ba967 +Subproject commit 42cbd59ddda9691b7ecee8da9c3697c46014662c From 1fe68671e6f56ed4991f28d954d252a8271f6951 Mon Sep 17 00:00:00 2001 From: Tatjana Kaschperko Lindt Date: Mon, 8 Dec 2025 10:33:23 +0100 Subject: [PATCH 41/83] IONOS(theming): use static favicon in all apps for consistent branding Signed-off-by: Tatjana Kaschperko Lindt --- apps/theming/img/favicon-touch.png | Bin 0 -> 723 bytes apps/theming/img/favicon-touch.svg | 2 + apps/theming/img/favicon.ico | Bin 0 -> 15374 bytes apps/theming/img/favicon.png | Bin 0 -> 723 bytes apps/theming/img/favicon.svg | 2 + .../theming/lib/Controller/IconController.php | 85 +++++-------- apps/theming/lib/ThemingDefaults.php | 15 ++- .../tests/Controller/IconControllerTest.php | 116 +++++------------- apps/theming/tests/ThemingDefaultsTest.php | 30 +++-- 9 files changed, 98 insertions(+), 152 deletions(-) create mode 100644 apps/theming/img/favicon-touch.png create mode 100644 apps/theming/img/favicon-touch.svg create mode 100644 apps/theming/img/favicon.ico create mode 100644 apps/theming/img/favicon.png create mode 100644 apps/theming/img/favicon.svg diff --git a/apps/theming/img/favicon-touch.png b/apps/theming/img/favicon-touch.png new file mode 100644 index 0000000000000000000000000000000000000000..c6d8d800c1260b28bca0591cd6110f6b1a7822db GIT binary patch literal 723 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K58911MRQ8&P5Fo`_;1OBOz`!j8!i<;h*8Kqr z8VC4VH|NsBLb?U~$r*16Sa8$}XSIf2hv3XT&*4 zf29jwS)oGc=I(d}({-NRV73v=?#*+Y6=ogCG)pyW1v<}!b4m%&K9HN+&-UCD0Xq0W zy;=0R$a9Jf4L7HN1ZMai{Wo`};@{{+{v0|lS^7$tYXA2=VP={uzrc3Jjo)?-)dq%B z3z+8aXSv0)%l?4SU+0PZ9A{K#ABgiZYt((f5T4JTuewlK`oPuh1Rd31O#M4u;uCDW z{no$e|G&G~r<(uI(% + \ No newline at end of file diff --git a/apps/theming/img/favicon.ico b/apps/theming/img/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..cccc4409280d7efb7227bfaf8dc6cf7d67913d67 GIT binary patch literal 15374 zcmeHNU1%It6h6DzY?>coebLk(7z8CIZOB_tVwjggPzXT~#F}}iN~Q3cfR-4*Q2=OecK9{qC$JfY%f|s;sR8KdaQLT_02yqi>GBN# zmzn^2dR+UP0J?7lNT*%?7Jz&1257+;DQpHdiW~HMqO}nQI&XvL4z$9=&{OdCt4CpG z>}ANG8iV=ix8UmP3S9f^PuSSl2y(GSo_NU-wVrmaLvOHa{A?e)Cf^}o0DC9d1s-$M zu{YC)tc$0{5SCuJ<~<9j0Rcl&5BF9oWL=X>{PxO*#5{_8eSUT=-YHk_@Di z4>X=1@ePvsoKLsqO`ksUamu6P`42o2lKB~*Zp){A`bggJ=y?7kk3{qLefoC6{2^{0 zdzYIh1oH;Lyq=pw?Q!4Z^(W{)8`w;2Vej*6hkMvNA-)fEuNS|(P^o*3Xaaso%iN^t zvT~dFacf%SYb=XDHmIn#F8YrJ*>zc;iRLhFeJb+UeNg1kE;EXRHe%AN#}_5&7DZ?u{kg8#Z@`#2HiUUez=`=ajoNwy@86_Tl@$ zd)B@ApXPq=S+C&ShN%9FU)}Pi2gDDVvDZ^RBM{5BZ3?VM*4lD;6Ut?N4%zma*H^q2 zZ2OAHyEM7aO626;kjN|KzA?akuWkP(i3(G;{j)@#KsPywyaDB=M2_x%mxy=T{e&ES z98u0m;Os$ppG4kn+gBxG7`E+IiF}9M#{$sPJ&~bD`MLxp#Fp(}WOC?QAC$@?VUz)u z0a|BFY*}n!*BM%4XzxttjO((eLahITUt)I2Pz|HMSUj)L$BV_{C53(u>VHt^_iOYg zQ2%{!e+pk*2-YV_pAFXEL3$%tKY;4RV0G*()a%0Mg8Q@Z9l?4EugD$?|7~!0HhM+) zCUjp^gzr~}hgsAs!q=j@s0iOkdQJGt!J}j0zYEs0@YBJ17QQc7-;e0F5x#~-rwHFb zdPVpLP^}989_kgNXWR4e8MpzJ8-1*XpiCl?0P;Q=QO#k~iJpS)};+()V?1G_N;w9o9pHi9keKGUmb zh)A*3!Dg+6GBdz3cqdO^-2F@`( z94t@ZPYtJzOezIOvH8QnIdIZ^m=cP?kyeVhG0Tp;?&2G99|6670fs?@;A5M*_BfW4uH zEOA^$el&9v&}{G%?;@qbERgWH{h_pTB#bf;WgyDH4V{4&M3=@khfR6TYiopOJ8y>P z58MT>AAS%8x!^+YE*sIHM?e9Oz`hE!^M87Bl zs7@&mGFKXXg^# zi9+yy$nkFoA|+X1_PXtv@4|(z+p78YF8rJ?&=4;C4vm?w-eLIc+@q@(_+aoOR44nU zAX1W3%wBU+<{oMs_p)y-xRF4ygNgRY$~(C5`?y3+RXO~L^0M#Lg>*#%p0Rr8ZfTCh z+`iqhS09hLIXLh&CwFe|!q*fPxP6twXB9eaL4pt0StoSoU2cEOxmyO8C@H6GCjYif z){&Lav=sy1(}f>;A+2)wnews^1!gb(vXAGGQ@d=LtMXOaUFN3w{Zmuj&cm}?mboc5 z+|Zj0cby2m$zb*4GBz3R%oaNPj(uqO;NS_p%z-mr#YY#V57RK^(=3$+^R$pEzj`R` bdeByX+g55H@lgh%3`7}-G7x2;Dl+g7-ImgV literal 0 HcmV?d00001 diff --git a/apps/theming/img/favicon.png b/apps/theming/img/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..c6d8d800c1260b28bca0591cd6110f6b1a7822db GIT binary patch literal 723 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K58911MRQ8&P5Fo`_;1OBOz`!j8!i<;h*8Kqr z8VC4VH|NsBLb?U~$r*16Sa8$}XSIf2hv3XT&*4 zf29jwS)oGc=I(d}({-NRV73v=?#*+Y6=ogCG)pyW1v<}!b4m%&K9HN+&-UCD0Xq0W zy;=0R$a9Jf4L7HN1ZMai{Wo`};@{{+{v0|lS^7$tYXA2=VP={uzrc3Jjo)?-)dq%B z3z+8aXSv0)%l?4SU+0PZ9A{K#ABgiZYt((f5T4JTuewlK`oPuh1Rd31O#M4u;uCDW z{no$e|G&G~r<(uI(% + \ No newline at end of file diff --git a/apps/theming/lib/Controller/IconController.php b/apps/theming/lib/Controller/IconController.php index f487663d8c81a..5565a08cb83b8 100644 --- a/apps/theming/lib/Controller/IconController.php +++ b/apps/theming/lib/Controller/IconController.php @@ -88,34 +88,25 @@ public function getThemedIcon(string $app, string $image): Response { #[NoCSRFRequired] #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)] public function getFavicon(string $app = 'core'): Response { - if ($app !== 'core' && !$this->appManager->isEnabledForUser($app)) { - $app = 'core'; + // Always serve the static favicon.ico from theming app for all apps + $staticFavicon = \OC::$SERVERROOT . '/apps/theming/img/favicon.ico'; + if (file_exists($staticFavicon)) { + $response = new DataDisplayResponse( + $this->fileAccessHelper->file_get_contents($staticFavicon), + Http::STATUS_OK, + ['Content-Type' => 'image/x-icon'] + ); + $response->cacheFor(86400); + return $response; } - $response = null; - $iconFile = null; - try { - $iconFile = $this->imageManager->getImage('favicon', false); - $response = new FileDisplayResponse($iconFile, Http::STATUS_OK, ['Content-Type' => 'image/x-icon']); - } catch (NotFoundException $e) { - } - if ($iconFile === null && $this->imageManager->shouldReplaceIcons()) { - $color = $this->themingDefaults->getColorPrimary(); - try { - $iconFile = $this->imageManager->getCachedImage('favIcon-' . $app . $color); - } catch (NotFoundException $exception) { - $icon = $this->iconBuilder->getFavicon($app); - if ($icon === false || $icon === '') { - return new NotFoundResponse(); - } - $iconFile = $this->imageManager->setCachedImage('favIcon-' . $app . $color, $icon); - } - $response = new FileDisplayResponse($iconFile, Http::STATUS_OK, ['Content-Type' => 'image/x-icon']); - } - if ($response === null) { - $fallbackLogo = \OC::$SERVERROOT . '/core/img/favicon.png'; - $response = new DataDisplayResponse($this->fileAccessHelper->file_get_contents($fallbackLogo), Http::STATUS_OK, ['Content-Type' => 'image/x-icon']); - } + // Fallback to core favicon if theming favicon doesn't exist + $fallbackLogo = \OC::$SERVERROOT . '/core/img/favicon.png'; + $response = new DataDisplayResponse( + $this->fileAccessHelper->file_get_contents($fallbackLogo), + Http::STATUS_OK, + ['Content-Type' => 'image/png'] + ); $response->cacheFor(86400); return $response; } @@ -134,33 +125,25 @@ public function getFavicon(string $app = 'core'): Response { #[NoCSRFRequired] #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)] public function getTouchIcon(string $app = 'core'): Response { - if ($app !== 'core' && !$this->appManager->isEnabledForUser($app)) { - $app = 'core'; + // Always serve the static favicon-touch.png from theming app for all apps + $staticTouchIcon = \OC::$SERVERROOT . '/apps/theming/img/favicon-touch.png'; + if (file_exists($staticTouchIcon)) { + $response = new DataDisplayResponse( + $this->fileAccessHelper->file_get_contents($staticTouchIcon), + Http::STATUS_OK, + ['Content-Type' => 'image/png'] + ); + $response->cacheFor(86400); + return $response; } - $response = null; - try { - $iconFile = $this->imageManager->getImage('favicon'); - $response = new FileDisplayResponse($iconFile, Http::STATUS_OK, ['Content-Type' => 'image/x-icon']); - } catch (NotFoundException $e) { - } - if ($this->imageManager->shouldReplaceIcons()) { - $color = $this->themingDefaults->getColorPrimary(); - try { - $iconFile = $this->imageManager->getCachedImage('touchIcon-' . $app . $color); - } catch (NotFoundException $exception) { - $icon = $this->iconBuilder->getTouchIcon($app); - if ($icon === false || $icon === '') { - return new NotFoundResponse(); - } - $iconFile = $this->imageManager->setCachedImage('touchIcon-' . $app . $color, $icon); - } - $response = new FileDisplayResponse($iconFile, Http::STATUS_OK, ['Content-Type' => 'image/png']); - } - if ($response === null) { - $fallbackLogo = \OC::$SERVERROOT . '/core/img/favicon-touch.png'; - $response = new DataDisplayResponse($this->fileAccessHelper->file_get_contents($fallbackLogo), Http::STATUS_OK, ['Content-Type' => 'image/png']); - } + // Fallback to core touch icon if theming icon doesn't exist + $fallbackLogo = \OC::$SERVERROOT . '/core/img/favicon-touch.png'; + $response = new DataDisplayResponse( + $this->fileAccessHelper->file_get_contents($fallbackLogo), + Http::STATUS_OK, + ['Content-Type' => 'image/png'] + ); $response->cacheFor(86400); return $response; } diff --git a/apps/theming/lib/ThemingDefaults.php b/apps/theming/lib/ThemingDefaults.php index c213077bd8998..64d5b14035179 100644 --- a/apps/theming/lib/ThemingDefaults.php +++ b/apps/theming/lib/ThemingDefaults.php @@ -380,12 +380,23 @@ public function replaceImagePath($app, $image) { $cacheBusterValue = $this->config->getAppValue('theming', 'cachebuster', '0'); $route = false; - if ($image === 'favicon.ico' && ($this->imageManager->shouldReplaceIcons() || $this->getCustomFavicon() !== null)) { + // Always use theming favicons for all apps to ensure consistent branding + if ($image === 'favicon.ico') { $route = $this->urlGenerator->linkToRoute('theming.Icon.getFavicon', ['app' => $app]); } - if (($image === 'favicon-touch.png' || $image === 'favicon-fb.png') && ($this->imageManager->shouldReplaceIcons() || $this->getCustomFavicon() !== null)) { + if ($image === 'favicon-touch.png' || $image === 'favicon-fb.png') { $route = $this->urlGenerator->linkToRoute('theming.Icon.getTouchIcon', ['app' => $app]); } + if ($image === 'favicon.svg' || $image === 'favicon.png') { + // Redirect to static theming favicon files + $route = $this->urlGenerator->linkTo('theming', 'img/' . $image); + } + if ($image === 'favicon-touch.svg') { + $route = $this->urlGenerator->linkTo('theming', 'img/favicon-touch.svg'); + } + if ($image === 'favicon-mask.svg') { + $route = $this->urlGenerator->linkTo('theming', 'img/favicon.svg'); + } if ($image === 'manifest.json') { try { $appPath = $this->appManager->getAppPath($app); diff --git a/apps/theming/tests/Controller/IconControllerTest.php b/apps/theming/tests/Controller/IconControllerTest.php index 42e841e9a0f9f..f0bb063aa3eac 100644 --- a/apps/theming/tests/Controller/IconControllerTest.php +++ b/apps/theming/tests/Controller/IconControllerTest.php @@ -92,102 +92,46 @@ public function testGetThemedIcon(): void { } public function testGetFaviconDefault(): void { - if (!extension_loaded('imagick')) { - $this->markTestSkipped('Imagemagick is required for dynamic icon generation.'); - } - $checkImagick = new \Imagick(); - if (count($checkImagick->queryFormats('SVG')) < 1) { - $this->markTestSkipped('No SVG provider present.'); - } - $file = $this->iconFileMock('filename', 'filecontent'); - $this->imageManager->expects($this->once()) - ->method('getImage', false) - ->with('favicon') - ->will($this->throwException(new NotFoundException())); - $this->imageManager->expects($this->any()) - ->method('shouldReplaceIcons') - ->willReturn(true); - $this->imageManager->expects($this->once()) - ->method('getCachedImage') - ->will($this->throwException(new NotFoundException())); - $this->iconBuilder->expects($this->once()) - ->method('getFavicon') - ->with('core') - ->willReturn('filecontent'); - $this->imageManager->expects($this->once()) - ->method('setCachedImage') - ->willReturn($file); + // Test that the controller serves the static favicon from theming app + $themingFavicon = \OC::$SERVERROOT . '/apps/theming/img/favicon.ico'; + $faviconContent = file_get_contents($themingFavicon); - $expected = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'image/x-icon']); - $expected->cacheFor(86400); - $this->assertEquals($expected, $this->iconController->getFavicon()); - } - - public function testGetFaviconFail(): void { - $this->imageManager->expects($this->once()) - ->method('getImage') - ->with('favicon', false) - ->will($this->throwException(new NotFoundException())); - $this->imageManager->expects($this->any()) - ->method('shouldReplaceIcons') - ->willReturn(false); - $fallbackLogo = \OC::$SERVERROOT . '/core/img/favicon.png'; $this->fileAccessHelper->expects($this->once()) ->method('file_get_contents') - ->with($fallbackLogo) - ->willReturn(file_get_contents($fallbackLogo)); - $expected = new DataDisplayResponse(file_get_contents($fallbackLogo), Http::STATUS_OK, ['Content-Type' => 'image/x-icon']); + ->with($themingFavicon) + ->willReturn($faviconContent); + + $expected = new DataDisplayResponse( + $faviconContent, + Http::STATUS_OK, + ['Content-Type' => 'image/x-icon'] + ); $expected->cacheFor(86400); - $this->assertEquals($expected, $this->iconController->getFavicon()); + + $result = $this->iconController->getFavicon(); + $this->assertEquals($expected->getStatus(), $result->getStatus()); + $this->assertEquals($expected->getHeaders(), $result->getHeaders()); } public function testGetTouchIconDefault(): void { - if (!extension_loaded('imagick')) { - $this->markTestSkipped('Imagemagick is required for dynamic icon generation.'); - } - $checkImagick = new \Imagick(); - if (count($checkImagick->queryFormats('SVG')) < 1) { - $this->markTestSkipped('No SVG provider present.'); - } + // Test that the controller serves the static touch icon from theming app + $themingTouchIcon = \OC::$SERVERROOT . '/apps/theming/img/favicon-touch.png'; + $touchIconContent = file_get_contents($themingTouchIcon); - $this->imageManager->expects($this->once()) - ->method('getImage') - ->will($this->throwException(new NotFoundException())); - $this->imageManager->expects($this->any()) - ->method('shouldReplaceIcons') - ->willReturn(true); - $this->iconBuilder->expects($this->once()) - ->method('getTouchIcon') - ->with('core') - ->willReturn('filecontent'); - $file = $this->iconFileMock('filename', 'filecontent'); - $this->imageManager->expects($this->once()) - ->method('getCachedImage') - ->will($this->throwException(new NotFoundException())); - $this->imageManager->expects($this->once()) - ->method('setCachedImage') - ->willReturn($file); - - $expected = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'image/png']); - $expected->cacheFor(86400); - $this->assertEquals($expected, $this->iconController->getTouchIcon()); - } - - public function testGetTouchIconFail(): void { - $this->imageManager->expects($this->once()) - ->method('getImage') - ->with('favicon') - ->will($this->throwException(new NotFoundException())); - $this->imageManager->expects($this->any()) - ->method('shouldReplaceIcons') - ->willReturn(false); - $fallbackLogo = \OC::$SERVERROOT . '/core/img/favicon-touch.png'; $this->fileAccessHelper->expects($this->once()) ->method('file_get_contents') - ->with($fallbackLogo) - ->willReturn(file_get_contents($fallbackLogo)); - $expected = new DataDisplayResponse(file_get_contents($fallbackLogo), Http::STATUS_OK, ['Content-Type' => 'image/png']); + ->with($themingTouchIcon) + ->willReturn($touchIconContent); + + $expected = new DataDisplayResponse( + $touchIconContent, + Http::STATUS_OK, + ['Content-Type' => 'image/png'] + ); $expected->cacheFor(86400); - $this->assertEquals($expected, $this->iconController->getTouchIcon()); + + $result = $this->iconController->getTouchIcon(); + $this->assertEquals($expected->getStatus(), $result->getStatus()); + $this->assertEquals($expected->getHeaders(), $result->getHeaders()); } } diff --git a/apps/theming/tests/ThemingDefaultsTest.php b/apps/theming/tests/ThemingDefaultsTest.php index 27b74c41caaf2..d3dfca0efa5b5 100644 --- a/apps/theming/tests/ThemingDefaultsTest.php +++ b/apps/theming/tests/ThemingDefaultsTest.php @@ -806,33 +806,37 @@ public function testGetCustomiTunesAppId(): void { public function dataReplaceImagePath() { return [ ['core', 'test.png', false], - ['core', 'manifest.json'], - ['core', 'favicon.ico'], - ['core', 'favicon-touch.png'] + ['core', 'manifest.json', 'themingRoute?v=1234abcd'], + ['core', 'favicon.ico', 'themingRoute?v=1234abcd'], + ['core', 'favicon-touch.png', 'themingRoute?v=1234abcd'], + ['core', 'favicon.svg', 'themingLink?v=1234abcd'], + ['core', 'favicon.png', 'themingLink?v=1234abcd'], + ['core', 'favicon-touch.svg', 'themingLink?v=1234abcd'], + ['core', 'favicon-mask.svg', 'themingLink?v=1234abcd'], + ['mail', 'favicon.ico', 'themingRoute?v=1234abcd'], + ['calendar', 'favicon-touch.png', 'themingRoute?v=1234abcd'], ]; } /** @dataProvider dataReplaceImagePath */ - public function testReplaceImagePath($app, $image, $result = 'themingRoute?v=1234abcd'): void { - $this->cache->expects($this->any()) - ->method('get') - ->with('shouldReplaceIcons') - ->willReturn(true); + public function testReplaceImagePath($app, $image, $result = false): void { $this->config ->expects($this->any()) ->method('getAppValue') ->with('theming', 'cachebuster', '0') ->willReturn('0'); - $this->urlGenerator - ->expects($this->any()) - ->method('linkToRoute') - ->willReturn('themingRoute'); - if ($result) { + + if (str_contains($result, '?v=')) { + $this->urlGenerator + ->expects($this->once()) + ->method(str_starts_with($result, 'themingRoute') ? 'linkToRoute' : 'linkTo') + ->willReturn(str_starts_with($result, 'themingRoute') ? 'themingRoute' : 'themingLink'); $this->util ->expects($this->once()) ->method('getCacheBuster') ->willReturn('1234abcd'); } + $this->assertEquals($result, $this->template->replaceImagePath($app, $image)); } From e75abe693bcfed5fd86ac24d7e09189ad5a18e50 Mon Sep 17 00:00:00 2001 From: Tatjana Kaschperko Lindt Date: Tue, 9 Dec 2025 13:29:35 +0100 Subject: [PATCH 42/83] IONOS(theming): fix file viewer background color Signed-off-by: Tatjana Kaschperko Lindt --- apps/theming/css/ionos/files.css | 2 +- apps/theming/css/ionos/variables.css | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/theming/css/ionos/files.css b/apps/theming/css/ionos/files.css index 7ab7ab03028b1..a959f9670b149 100644 --- a/apps/theming/css/ionos/files.css +++ b/apps/theming/css/ionos/files.css @@ -289,7 +289,7 @@ div.v-popper--theme-dropdown.v-popper__popper:has(.v-popper__wrapper), .header-m min-width: min-content; &:has(.viewer__content) { - background-color: transparent; + background-color: var(--ion-surface-viewer) !important; .editor__content pre { background-color: var(--ion-surface-secondary); diff --git a/apps/theming/css/ionos/variables.css b/apps/theming/css/ionos/variables.css index 79bdefdb4c1ff..44babf32849b7 100644 --- a/apps/theming/css/ionos/variables.css +++ b/apps/theming/css/ionos/variables.css @@ -76,6 +76,7 @@ --ion-surface-primary: var(--ion-color-main-background); --ion-surface-secondary: light-dark(var(--ion-color-cool-grey-c1), var(--ion-color-cool-grey-c8)); --ion-surface-dialog: light-dark(#fff, var(--ion-color-cool-grey-c8)); + --ion-surface-viewer: light-dark(#000, var(--ion-color-cool-grey-c8)); --ion-dialog-filter-button-background: light-dark(var(--ion-color-cool-grey-c1), var(--ion-color-blue-b9)); --ion-dialog-files-list-background: transparent; From aa5b5beeecdb7d9195315b24af8749c502e26e10 Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Wed, 10 Dec 2025 15:43:04 +0100 Subject: [PATCH 43/83] IONOS(ncw_mailtemplate) update submodule (adjust mail styling) Signed-off-by: Kai Henseler --- apps-external/ncw_mailtemplate | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps-external/ncw_mailtemplate b/apps-external/ncw_mailtemplate index d3c66eb42a98b..a4d2c8549e284 160000 --- a/apps-external/ncw_mailtemplate +++ b/apps-external/ncw_mailtemplate @@ -1 +1 @@ -Subproject commit d3c66eb42a98b0a55063f6c36386091be47d9799 +Subproject commit a4d2c8549e28493c91244f4ef15a9568868087c3 From 445083b34baa84d71b7512b09da413203ff40f15 Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Mon, 8 Dec 2025 12:41:18 +0100 Subject: [PATCH 44/83] IONOS(theming): fix breadcrumb button size Signed-off-by: Kai Henseler --- apps/theming/css/ionos/files.css | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/theming/css/ionos/files.css b/apps/theming/css/ionos/files.css index a959f9670b149..a53c1b02f5d7a 100644 --- a/apps/theming/css/ionos/files.css +++ b/apps/theming/css/ionos/files.css @@ -59,7 +59,6 @@ div.files-list__header:has(div.files-list__breadcrumbs), .file-picker { div.files-list__breadcrumbs, .file-picker__breadcrumbs { ul.breadcrumb__crumbs { a, a:hover, a:active, a:disabled, button { - padding: 2px 0; font-weight: 400; color: var(--ion-breadcrumb-text-default); background-color: transparent; @@ -83,7 +82,6 @@ div.files-list__header:has(div.files-list__breadcrumbs), .file-picker { button.button-vue--icon-only, a.button-vue--icon-only svg { background-color: transparent; color: var(--ion-breadcrumb-text); - padding: 2px 0; &:hover { color: var(--ion-breadcrumb-text-hover); background-color: inherit; From cdf1f132a4d6bce25caae10f01ed617e55ac75e6 Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Mon, 8 Dec 2025 12:41:52 +0100 Subject: [PATCH 45/83] IONOS(theming): change target dialog button colors by making the selectors more specific Signed-off-by: Kai Henseler --- apps/theming/css/ionos/buttons.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/theming/css/ionos/buttons.css b/apps/theming/css/ionos/buttons.css index e2d38e8f60a69..29fcd9575ba67 100644 --- a/apps/theming/css/ionos/buttons.css +++ b/apps/theming/css/ionos/buttons.css @@ -23,7 +23,7 @@ } } - &.button-vue--vue-primary[aria-label*="Copy"], + &.button-vue--vue-primary[aria-label*="Copy"][type="button"], &.button-vue--vue-secondary, &.button-vue--vue-tertiary[aria-label="Cancel"] { background-color: var(--ion-button-secondary-background-default); border: var(--ion-button-secondary-border-default); From a79215ce2e4d3d3f8ccc3283d6ae085579e61352 Mon Sep 17 00:00:00 2001 From: Arsalan Ul Haq Sohni Date: Tue, 2 Dec 2025 11:49:30 +0100 Subject: [PATCH 46/83] IONOS(tests): add tests for developer documentation link generation Signed-off-by: Arsalan Ul Haq Sohni --- .../Controller/AppSettingsControllerTest.php | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/apps/settings/tests/Controller/AppSettingsControllerTest.php b/apps/settings/tests/Controller/AppSettingsControllerTest.php index f72bd45a3d234..8adba2083296e 100644 --- a/apps/settings/tests/Controller/AppSettingsControllerTest.php +++ b/apps/settings/tests/Controller/AppSettingsControllerTest.php @@ -185,9 +185,21 @@ public function testViewApps(): void { ->method('setActiveEntry') ->with('core_apps'); + // Test that developer docs link is generated correctly + $this->urlGenerator + ->expects($this->once()) + ->method('linkToDocs') + ->with('developer-manual') + ->willReturn('https://docs.nextcloud.com/server/latest/developer_manual/'); + $this->initialState ->expects($this->exactly(4)) - ->method('provideInitialState'); + ->method('provideInitialState') + ->willReturnCallback(function ($key, $value) { + if ($key === 'appstoreDeveloperDocs') { + $this->assertEquals('https://docs.nextcloud.com/server/latest/developer_manual/', $value); + } + }); $policy = new ContentSecurityPolicy(); $policy->addAllowedImageDomain('https://usercontent.apps.nextcloud.com'); @@ -218,9 +230,21 @@ public function testViewAppsAppstoreNotEnabled(): void { ->method('setActiveEntry') ->with('core_apps'); + // Test that developer docs link is still generated even when appstore is disabled + $this->urlGenerator + ->expects($this->once()) + ->method('linkToDocs') + ->with('developer-manual') + ->willReturn('https://docs.nextcloud.com/server/latest/developer_manual/'); + $this->initialState ->expects($this->exactly(4)) - ->method('provideInitialState'); + ->method('provideInitialState') + ->willReturnCallback(function ($key, $value) { + if ($key === 'appstoreDeveloperDocs') { + $this->assertEquals('https://docs.nextcloud.com/server/latest/developer_manual/', $value); + } + }); $policy = new ContentSecurityPolicy(); $policy->addAllowedImageDomain('https://usercontent.apps.nextcloud.com'); From 8217b8a4e4acd4a6d24634abfb21daf71b7ea4aa Mon Sep 17 00:00:00 2001 From: Arsalan Ul Haq Sohni Date: Tue, 2 Dec 2025 11:59:33 +0100 Subject: [PATCH 47/83] IONOS(appsettings): conditionally display developer documentation link based on configuration Signed-off-by: Arsalan Ul Haq Sohni --- .../lib/Controller/AppSettingsController.php | 9 +- .../settings/src/views/AppStoreNavigation.vue | 3 +- .../Controller/AppSettingsControllerTest.php | 99 +++++++++++++++++++ 3 files changed, 109 insertions(+), 2 deletions(-) diff --git a/apps/settings/lib/Controller/AppSettingsController.php b/apps/settings/lib/Controller/AppSettingsController.php index c0ba0b556291b..d76408010da79 100644 --- a/apps/settings/lib/Controller/AppSettingsController.php +++ b/apps/settings/lib/Controller/AppSettingsController.php @@ -35,6 +35,7 @@ use OCP\Files\SimpleFS\ISimpleFile; use OCP\Files\SimpleFS\ISimpleFolder; use OCP\Http\Client\IClientService; +use OCP\IAppConfig; use OCP\IConfig; use OCP\IGroup; use OCP\IL10N; @@ -74,6 +75,7 @@ public function __construct( private IInitialState $initialState, private AppDiscoverFetcher $discoverFetcher, private IClientService $clientService, + private IAppConfig $appConfig, ) { parent::__construct($appName, $request); $this->appData = $appDataFactory->get('appstore'); @@ -90,7 +92,12 @@ public function viewApps(): TemplateResponse { $this->initialState->provideInitialState('appstoreEnabled', $this->config->getSystemValueBool('appstoreenabled', true)); $this->initialState->provideInitialState('appstoreBundles', $this->getBundles()); - $this->initialState->provideInitialState('appstoreDeveloperDocs', $this->urlGenerator->linkToDocs('developer-manual')); + + // Conditionally set developer docs link based on configuration + $displayDocumentationLink = $this->appConfig->getValueBool('settings', 'display_documentation_link', true); + $developerDocsUrl = $displayDocumentationLink ? $this->urlGenerator->linkToDocs('developer-manual') : ''; + $this->initialState->provideInitialState('appstoreDeveloperDocs', $developerDocsUrl); + $this->initialState->provideInitialState('appstoreUpdateCount', count($this->getAppsWithUpdates())); if ($this->appManager->isInstalled('app_api')) { diff --git a/apps/settings/src/views/AppStoreNavigation.vue b/apps/settings/src/views/AppStoreNavigation.vue index a35cd94da95fb..3507006c47d5b 100644 --- a/apps/settings/src/views/AppStoreNavigation.vue +++ b/apps/settings/src/views/AppStoreNavigation.vue @@ -91,7 +91,8 @@ - diff --git a/apps/settings/tests/Controller/AppSettingsControllerTest.php b/apps/settings/tests/Controller/AppSettingsControllerTest.php index 8adba2083296e..3c0df4532d79f 100644 --- a/apps/settings/tests/Controller/AppSettingsControllerTest.php +++ b/apps/settings/tests/Controller/AppSettingsControllerTest.php @@ -19,6 +19,7 @@ use OCP\AppFramework\Services\IInitialState; use OCP\Files\AppData\IAppDataFactory; use OCP\Http\Client\IClientService; +use OCP\IAppConfig; use OCP\IConfig; use OCP\IL10N; use OCP\INavigationManager; @@ -45,6 +46,8 @@ class AppSettingsControllerTest extends TestCase { private $l10n; /** @var IConfig|MockObject */ private $config; + /** @var IAppConfig|MockObject */ + private $appConfig; /** @var INavigationManager|MockObject */ private $navigationManager; private AppManager&MockObject $appManager; @@ -81,6 +84,7 @@ protected function setUp(): void { ->method('t') ->willReturnArgument(0); $this->config = $this->createMock(IConfig::class); + $this->appConfig = $this->createMock(IAppConfig::class); $this->navigationManager = $this->createMock(INavigationManager::class); $this->appManager = $this->createMock(AppManager::class); $this->categoryFetcher = $this->createMock(CategoryFetcher::class); @@ -112,6 +116,7 @@ protected function setUp(): void { $this->initialState, $this->discoverFetcher, $this->clientService, + $this->appConfig, ); } @@ -180,6 +185,11 @@ public function testViewApps(): void { ->method('getSystemValueBool') ->with('appstoreenabled', true) ->willReturn(true); + $this->appConfig + ->expects($this->once()) + ->method('getValueBool') + ->with('settings', 'display_documentation_link', true) + ->willReturn(true); $this->navigationManager ->expects($this->once()) ->method('setActiveEntry') @@ -225,6 +235,11 @@ public function testViewAppsAppstoreNotEnabled(): void { ->method('getSystemValueBool') ->with('appstoreenabled', true) ->willReturn(false); + $this->appConfig + ->expects($this->once()) + ->method('getValueBool') + ->with('settings', 'display_documentation_link', true) + ->willReturn(true); $this->navigationManager ->expects($this->once()) ->method('setActiveEntry') @@ -259,4 +274,88 @@ public function testViewAppsAppstoreNotEnabled(): void { $this->assertEquals($expected, $this->appSettingsController->viewApps()); } + + public function testDeveloperDocumentationLinkHiddenWhenConfigured(): void { + $this->installer->expects($this->any()) + ->method('isUpdateAvailable') + ->willReturn(false); + $this->bundleFetcher->expects($this->once())->method('getBundles')->willReturn([]); + $this->config + ->expects($this->once()) + ->method('getSystemValueBool') + ->with('appstoreenabled', true) + ->willReturn(true); + $this->appConfig + ->expects($this->once()) + ->method('getValueBool') + ->with('settings', 'display_documentation_link', true) + ->willReturn(false); + $this->navigationManager + ->expects($this->once()) + ->method('setActiveEntry') + ->with('core_apps'); + + // When display_documentation_link is false, linkToDocs should not be called + $this->urlGenerator + ->expects($this->never()) + ->method('linkToDocs'); + + $providedStates = []; + $this->initialState + ->expects($this->exactly(4)) + ->method('provideInitialState') + ->willReturnCallback(function ($key, $value) use (&$providedStates) { + $providedStates[$key] = $value; + }); + + $this->appSettingsController->viewApps(); + + // Assert that the developer docs state was provided with an empty string + $this->assertArrayHasKey('appstoreDeveloperDocs', $providedStates); + $this->assertEquals('', $providedStates['appstoreDeveloperDocs']); + } + + public function testDeveloperDocumentationLinkShownByDefault(): void { + $this->installer->expects($this->any()) + ->method('isUpdateAvailable') + ->willReturn(false); + $this->bundleFetcher->expects($this->once())->method('getBundles')->willReturn([]); + $this->config + ->expects($this->once()) + ->method('getSystemValueBool') + ->with('appstoreenabled', true) + ->willReturn(true); + $this->appConfig + ->expects($this->once()) + ->method('getValueBool') + ->with('settings', 'display_documentation_link', true) + ->willReturn(true); + $this->navigationManager + ->expects($this->once()) + ->method('setActiveEntry') + ->with('core_apps'); + + $developerDocsUrl = 'https://docs.nextcloud.com/server/latest/developer_manual/'; + + // When display_documentation_link is true (default), linkToDocs should be called + $this->urlGenerator + ->expects($this->once()) + ->method('linkToDocs') + ->with('developer-manual') + ->willReturn($developerDocsUrl); + + $providedStates = []; + $this->initialState + ->expects($this->exactly(4)) + ->method('provideInitialState') + ->willReturnCallback(function ($key, $value) use (&$providedStates) { + $providedStates[$key] = $value; + }); + + $this->appSettingsController->viewApps(); + + // Assert that the developer docs state was provided with the correct URL + $this->assertArrayHasKey('appstoreDeveloperDocs', $providedStates); + $this->assertEquals($developerDocsUrl, $providedStates['appstoreDeveloperDocs']); + } } From 0ab4f234660d5fc9902316b0558a6b3c3aaec136 Mon Sep 17 00:00:00 2001 From: Arsalan Ul Haq Sohni Date: Mon, 15 Dec 2025 11:05:13 +0100 Subject: [PATCH 48/83] IONOS(config): add developer_documentation_url boolean and build ncw core config Signed-off-by: Arsalan Ul Haq Sohni --- IONOS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IONOS b/IONOS index c18550dbc23c9..3d870c64bb27b 160000 --- a/IONOS +++ b/IONOS @@ -1 +1 @@ -Subproject commit c18550dbc23c91aaf20f15c62027cc1c812c9525 +Subproject commit 3d870c64bb27b778439108ddc3ddb524432f1be2 From 86635c13dc61d461e8bb523ca9da52d8618a13df Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Mon, 8 Dec 2025 15:59:45 +0100 Subject: [PATCH 49/83] IONOS(theming): fix table entry edit button colors Signed-off-by: Kai Henseler --- apps/theming/css/ionos/tables.css | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/apps/theming/css/ionos/tables.css b/apps/theming/css/ionos/tables.css index c80f01c852791..9f9dc477e8ce4 100644 --- a/apps/theming/css/ionos/tables.css +++ b/apps/theming/css/ionos/tables.css @@ -11,6 +11,30 @@ } &:hover { background-color: var(--ion-list-background-hover); + + /* Comment box */ + .text-editor__wrapper .editor { + background-color: transparent; + } + } + + td button[data-cy="editRowBtn"] { + background-color: var(--ion-button--icon-only-background); + .button-vue__icon span[role=img] { + color: var(--ion-button--icon-only-text); + } + + &:hover { + background-color: var(--ion-button--icon-only-background-hover); + + .button-vue__icon span[role=img] { + color: var(--ion-button--icon-only-text-hover) !important; + } + } + + &:active { + background-color: var(--ion-button--icon-only-background-active); + } } } } From 94e536de8e1d0a4d8229f8e0530ae0c522c4a42c Mon Sep 17 00:00:00 2001 From: Tatjana Kaschperko Lindt Date: Thu, 11 Dec 2025 09:59:56 +0100 Subject: [PATCH 50/83] IONOS(theming): add missing color definition for --ion-color-cool-grey-c9 Signed-off-by: Tatjana Kaschperko Lindt --- apps/theming/lib/Themes/IonosTheme.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/theming/lib/Themes/IonosTheme.php b/apps/theming/lib/Themes/IonosTheme.php index 7139820873a31..0a9433550e966 100644 --- a/apps/theming/lib/Themes/IonosTheme.php +++ b/apps/theming/lib/Themes/IonosTheme.php @@ -157,6 +157,7 @@ public function getCSSVariables(): array { $ionColorCoolGreyC6 = '#465A75'; $ionColorCoolGreyC7 = '#2E4360'; $ionColorCoolGreyC8 = '#1D2D42'; + $ionColorCoolGreyC9 = '#0A121C'; $ionColorTypoMild = 'light-dark(var(--ion-color-cool-grey-c7), var(--ion-color-cool-grey-c1))'; $ionColorLightGrey = '#d7d7d7'; $ionColorGreenG3 = '#12cf76'; @@ -188,6 +189,7 @@ public function getCSSVariables(): array { '--ion-color-cool-grey-c6' => $ionColorCoolGreyC6, '--ion-color-cool-grey-c7' => $ionColorCoolGreyC7, '--ion-color-cool-grey-c8' => $ionColorCoolGreyC8, + '--ion-color-cool-grey-c9' => $ionColorCoolGreyC9, '--ion-color-typo-mild' => $ionColorTypoMild, '--ion-color-light-grey' => $ionColorLightGrey, '--ion-color-green-g3' => $ionColorGreenG3, From d850137433c1c09b05a447c3ebd0c09b1b094165 Mon Sep 17 00:00:00 2001 From: Tatjana Kaschperko Lindt Date: Thu, 11 Dec 2025 10:00:47 +0100 Subject: [PATCH 51/83] IONOS(theming): tables app: refine table header and editor styles for consistency Signed-off-by: Tatjana Kaschperko Lindt --- apps/theming/css/ionos/tables.css | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/theming/css/ionos/tables.css b/apps/theming/css/ionos/tables.css index 9f9dc477e8ce4..bcb6a0b854fc2 100644 --- a/apps/theming/css/ionos/tables.css +++ b/apps/theming/css/ionos/tables.css @@ -1,7 +1,9 @@ .content.app-tables { table { - thead tr th { - background: var(--ion-tables-header-background-color); + thead tr th, + tr>th.sticky:first-child, + tr>th.sticky:last-child { + background-color: var(--ion-tables-header-background-color); border: 1px solid #ccc; } From 124e5cd2c754d3764194cc62a077436435c3a7ee Mon Sep 17 00:00:00 2001 From: Arsalan Ul Haq Sohni Date: Thu, 11 Dec 2025 16:26:48 +0100 Subject: [PATCH 52/83] IONOS(theming): adjust position of grid button in file list Signed-off-by: Arsalan Ul Haq Sohni --- apps/theming/css/ionos/files.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/theming/css/ionos/files.css b/apps/theming/css/ionos/files.css index a53c1b02f5d7a..06d701d2c0c81 100644 --- a/apps/theming/css/ionos/files.css +++ b/apps/theming/css/ionos/files.css @@ -34,7 +34,7 @@ tfoot.files-list__tfoot tr { #app-content-vue button.files-list__header-grid-button { position: absolute; - top: 88px; + top: 70px; right: 20px; z-index: 33; } From faf78a7474e3cbbfc4d7063fca9c83e76207805a Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Wed, 12 Nov 2025 10:58:00 +0100 Subject: [PATCH 53/83] IONOS(build): copy build-artifact.yml as build-artifact-original.yml for later performance comparison Signed-off-by: Misha M.-Kupriyanov --- .github/workflows/build-artifact-original.yml | 924 ++++++++++++++++++ 1 file changed, 924 insertions(+) create mode 100644 .github/workflows/build-artifact-original.yml diff --git a/.github/workflows/build-artifact-original.yml b/.github/workflows/build-artifact-original.yml new file mode 100644 index 0000000000000..a2cbfdb849649 --- /dev/null +++ b/.github/workflows/build-artifact-original.yml @@ -0,0 +1,924 @@ +name: Build Nextcloud Workspace artifact + +# SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors +# SPDX-FileCopyrightText: 2025 STRATO AG +# SPDX-License-Identifier: AGPL-3.0-or-later + +# The Nextcloud Workspace source is packaged as a container image. +# This is a workaround because releases cannot be created without tags, +# and we want to be able to create snapshots from branches. + +on: + pull_request: + paths: + - '.github/workflows/**' + - 'src/**' + - 'apps/**' + - 'apps/**/appinfo/info.xml' + - 'apps-external/**' + - 'IONOS' + - 'package.json' + - 'package-lock.json' + - 'themes/**' + - 'lib/**' + - 'tsconfig.json' + - '**.js' + - '**.ts' + - '**.vue' + - '.gitmodules' + push: + branches: + - ionos-dev + - ionos-stable + +concurrency: + group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/ionos-dev' && github.run_id || github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + TARGET_PACKAGE_NAME: nc-workspace.zip + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + ARTIFACTORY_REPOSITORY_SNAPSHOT: ionos-productivity-ncwserver-snapshot + +permissions: + contents: read + +jobs: + prepare-matrix: + runs-on: ubuntu-latest + outputs: + external-apps-matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + submodules: true + fetch-depth: '1' + + - name: Install dependencies + run: sudo apt-get update && sudo apt-get install -y make jq + + - name: Set matrix + id: set-matrix + run: | + # Create matrix configuration as a compact JSON string + matrix='[ + { + "name": "activity", + "path": "apps-external/activity", + "has_npm": true, + "has_composer": true, + "makefile_target": "build_activity_app" + }, + { + "name": "assistant", + "path": "apps-external/assistant", + "has_npm": true, + "has_composer": true, + "makefile_target": "build_assistant_app" + }, + { + "name": "calendar", + "path": "apps-external/calendar", + "has_npm": true, + "has_composer": true, + "makefile_target": "build_calendar_app" + }, + { + "name": "circles", + "path": "apps-external/circles", + "has_npm": false, + "has_composer": true, + "makefile_target": "build_circles_app" + }, + { + "name": "collectives", + "path": "apps-external/collectives", + "has_npm": true, + "has_composer": true, + "makefile_target": "build_collectives_app" + }, + { + "name": "contacts", + "path": "apps-external/contacts", + "has_npm": true, + "has_composer": true, + "makefile_target": "build_contacts_app" + }, + { + "name": "deck", + "path": "apps-external/deck", + "has_npm": true, + "has_composer": true, + "makefile_target": "build_deck_app" + }, + { + "name": "end_to_end_encryption", + "path": "apps-external/end_to_end_encryption", + "has_npm": true, + "has_composer": true, + "makefile_target": "build_end_to_end_encryption_app" + }, + { + "name": "forms", + "path": "apps-external/forms", + "has_npm": true, + "has_composer": true, + "makefile_target": "build_forms_app" + }, + { + "name": "groupfolders", + "path": "apps-external/groupfolders", + "has_npm": true, + "has_composer": true, + "makefile_target": "build_groupfolders_app" + }, + { + "name": "integration_openai", + "path": "apps-external/integration_openai", + "has_npm": true, + "has_composer": true, + "makefile_target": "build_integration_openai_app" + }, + { + "name": "mail", + "path": "apps-external/mail", + "has_npm": true, + "has_composer": true, + "makefile_target": "build_mail_app" + }, + { + "name": "ncw_apps_menu", + "path": "apps-external/ncw_apps_menu", + "has_npm": true, + "has_composer": true, + "makefile_target": "build_ncw_apps_menu_app" + }, + { + "name": "ncw_mailtemplate", + "path": "apps-external/ncw_mailtemplate", + "has_npm": true, + "has_composer": true, + "makefile_target": "build_ncw_mailtemplate_app" + }, + { + "name": "notes", + "path": "apps-external/notes", + "has_npm": true, + "has_composer": true, + "makefile_target": "build_notes_app" + }, + { + "name": "notifications", + "path": "apps-external/notifications", + "has_npm": true, + "has_composer": true, + "makefile_target": "build_notifications_app" + }, + { + "name": "notify_push", + "path": "apps-external/notify_push", + "has_npm": false, + "has_composer": true, + "makefile_target": "build_notify_push_app" + }, + { + "name": "password_policy", + "path": "apps-external/password_policy", + "has_npm": true, + "has_composer": true, + "makefile_target": "build_password_policy_app" + }, + { + "name": "richdocuments", + "path": "apps-external/richdocuments", + "has_npm": true, + "has_composer": true, + "makefile_target": "build_richdocuments_app" + }, + { + "name": "spreed", + "path": "apps-external/spreed", + "has_npm": true, + "has_composer": true, + "makefile_target": "build_spreed_app" + }, + { + "name": "tables", + "path": "apps-external/tables", + "has_npm": true, + "has_composer": true, + "makefile_target": "build_tables_app" + }, + { + "name": "tasks", + "path": "apps-external/tasks", + "has_npm": true, + "has_composer": true, + "makefile_target": "build_tasks_app" + }, + { + "name": "text", + "path": "apps-external/text", + "has_npm": true, + "has_composer": true, + "makefile_target": "build_text_app" + }, + { + "name": "twofactor_totp", + "path": "apps-external/twofactor_totp", + "has_npm": true, + "has_composer": true, + "makefile_target": "build_twofactor_totp_app" + }, + { + "name": "user_oidc", + "path": "apps-external/user_oidc", + "has_npm": true, + "has_composer": true, + "makefile_target": "build_user_oidc_app" + }, + { + "name": "viewer", + "path": "apps-external/viewer", + "has_npm": true, + "has_composer": true, + "makefile_target": "build_viewer_app" + }, + { + "name": "whiteboard", + "path": "apps-external/whiteboard", + "has_npm": true, + "has_composer": true, + "makefile_target": "build_whiteboard_app" + } + ]' + + # Validate JSON and output as compact format + if echo "$matrix" | jq empty 2>/dev/null; then + echo "matrix=$(echo "$matrix" | jq -c '.')" >> $GITHUB_OUTPUT + echo "Matrix configuration set successfully" + else + echo "Error: Invalid JSON in matrix configuration" + exit 1 + fi + + - name: Validate matrix against Makefile + run: | + set +e # Intentionally allow script to continue on error for custom error handling and reporting to GITHUB_STEP_SUMMARY + set -u # Exit on undefined variable + + echo "### 🔍 Matrix Validation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Debug: Check if apps-external exists + echo "Checking apps-external directory..." + if [ ! -d "apps-external" ]; then + echo "❌ **Error:** apps-external directory does not exist!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Directory listing:" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + ls -la >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + echo "Apps-external directory exists. Listing contents:" + ls -la apps-external/ | head -10 + + # Check if jq is available + echo "Checking if jq is installed..." + if ! command -v jq &> /dev/null; then + echo "❌ **Error:** jq is not installed!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "jq is required for matrix generation but was not found in PATH." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "jq version: $(jq --version)" + + echo "Generating matrix from Makefile..." + # Capture both stdout and stderr separately to better diagnose issues + makefile_output=$(make -f IONOS/Makefile generate_external_apps_matrix_json 2>&1) + makefile_exit_code=$? + + echo "Makefile exit code: ${makefile_exit_code}" + echo "Makefile output length: ${#makefile_output}" + + # Debug: Check if GITHUB_STEP_SUMMARY is set + echo "GITHUB_STEP_SUMMARY: ${GITHUB_STEP_SUMMARY:-NOT SET}" + + # If the Makefile command failed, show the error + if [ ${makefile_exit_code} -ne 0 ]; then + echo "" + echo "=== MAKEFILE ERROR ===" + echo "Exit code: ${makefile_exit_code}" + echo "Output:" + echo "$makefile_output" + echo "=====================" + echo "" + + # Write to summary + echo "❌ **Error:** Makefile command failed with exit code ${makefile_exit_code}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "Makefile error output" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "$makefile_output" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + + echo "Error written to summary file: ${GITHUB_STEP_SUMMARY}" + exit 1 + fi + + # Filter out the info message to get just the JSON + # The Makefile outputs "[i] Generating..." to stderr, but we captured everything with 2>&1 + # So we need to extract just the JSON part + generated_matrix=$(echo "$makefile_output" | grep -v '^\[i\]' || echo "$makefile_output") + + workflow_matrix='${{ steps.set-matrix.outputs.matrix }}' + + # Debug output + echo "Generated matrix length: ${#generated_matrix}" + echo "Workflow matrix length: ${#workflow_matrix}" + + # Show first 200 chars of generated matrix for debugging + if [ -n "$generated_matrix" ]; then + echo "Generated matrix preview: ${generated_matrix:0:200}..." + fi + + # Validate that we got valid JSON + if ! echo "$generated_matrix" | jq empty 2>/dev/null; then + echo "❌ **Error:** Generated matrix is not valid JSON" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "Invalid JSON output" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "$generated_matrix" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + # Validate that we got data + if [ -z "$generated_matrix" ] || [ -z "$workflow_matrix" ]; then + echo "❌ **Error:** Failed to load matrices" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- Generated matrix empty: $([ -z "$generated_matrix" ] && echo "yes" || echo "no")" >> $GITHUB_STEP_SUMMARY + echo "- Workflow matrix empty: $([ -z "$workflow_matrix" ] && echo "yes" || echo "no")" >> $GITHUB_STEP_SUMMARY + echo "- Makefile exit code: ${makefile_exit_code}" >> $GITHUB_STEP_SUMMARY + + exit 1 + fi + + # Sort both matrices for comparison + generated_sorted=$(echo "$generated_matrix" | jq -S '.' 2>&1 || echo "ERROR") + workflow_sorted=$(echo "$workflow_matrix" | jq -S '.' 2>&1 || echo "ERROR") + + echo "Sorted matrix lengths - generated: ${#generated_sorted}, workflow: ${#workflow_sorted}" + + # Compare the two matrices + if [ "$generated_sorted" = "$workflow_sorted" ]; then + echo "✅ **Validation passed!** The workflow matrix matches the Makefile configuration." >> $GITHUB_STEP_SUMMARY + echo "" + echo "✅ Matrix validation passed!" + else + echo "❌ **Validation failed!** The workflow matrix does not match the Makefile configuration." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + echo "Starting detailed comparison..." + + # Extract app names from both matrices + generated_apps=$(echo "$generated_matrix" | jq -r '.[].name' 2>/dev/null | sort || echo "") + workflow_apps=$(echo "$workflow_matrix" | jq -r '.[].name' 2>/dev/null | sort || echo "") + + echo "Generated apps count: $(echo "$generated_apps" | wc -l)" + echo "Workflow apps count: $(echo "$workflow_apps" | wc -l)" + + # Find missing apps (in Makefile but not in workflow) + missing_apps=$(comm -23 <(echo "$generated_apps") <(echo "$workflow_apps")) + if [ $? -ne 0 ]; then + echo "Error: comm command failed when finding missing apps." >&2 + exit 1 + fi + # Find extra apps (in workflow but not in Makefile) + extra_apps=$(comm -13 <(echo "$generated_apps") <(echo "$workflow_apps")) + if [ $? -ne 0 ]; then + echo "Error: comm command failed when finding extra apps." >&2 + exit 1 + fi + + echo "Missing apps: ${missing_apps:-none}" + echo "Extra apps: ${extra_apps:-none}" + + if [ -n "$missing_apps" ]; then + echo "#### âš ī¸ Missing Apps" >> $GITHUB_STEP_SUMMARY + echo "The following apps are configured in the Makefile but missing from the workflow:" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "$missing_apps" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + fi + + if [ -n "$extra_apps" ]; then + echo "#### âš ī¸ Extra Apps" >> $GITHUB_STEP_SUMMARY + echo "The following apps are in the workflow but not configured in the Makefile:" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "$extra_apps" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + fi + + # Check for configuration mismatches in common apps + common_apps=$(comm -12 <(echo "$generated_apps") <(echo "$workflow_apps") 2>/dev/null || echo "") + + echo "Common apps count: $(echo "$common_apps" | wc -l)" + + if [ -n "$common_apps" ]; then + mismatched_apps="" + + while IFS= read -r app; do + [ -z "$app" ] && continue + gen_config=$(echo "$generated_matrix" | jq -c --arg app "$app" '.[] | select(.name == $app)' 2>/dev/null || echo "") + wf_config=$(echo "$workflow_matrix" | jq -c --arg app "$app" '.[] | select(.name == $app)' 2>/dev/null || echo "") + + if [ -n "$gen_config" ] && [ -n "$wf_config" ] && [ "$gen_config" != "$wf_config" ]; then + mismatched_apps="${mismatched_apps}${app}"$'\n' + fi + done <<< "$common_apps" + + echo "Mismatched apps: ${mismatched_apps:-none}" + + if [ -n "$mismatched_apps" ]; then + echo "#### âš ī¸ Configuration Mismatches" >> $GITHUB_STEP_SUMMARY + echo "The following apps have different configurations (has_npm, has_composer, etc.):" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "$mismatched_apps" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "📋 Detailed differences" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo '```diff' >> $GITHUB_STEP_SUMMARY + + while IFS= read -r app; do + [ -z "$app" ] && continue + echo "=== $app ===" >> $GITHUB_STEP_SUMMARY + diff -u --label "Workflow" --label "Makefile" \ + <(echo "$workflow_matrix" | jq --arg app "$app" '.[] | select(.name == $app)' 2>/dev/null || echo "{}") \ + <(echo "$generated_matrix" | jq --arg app "$app" '.[] | select(.name == $app)' 2>/dev/null || echo "{}") \ + >> $GITHUB_STEP_SUMMARY 2>&1 || true + done <<< "$mismatched_apps" + + echo '```' >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + fi + fi + + # Provide fix instructions + echo "#### 🔧 How to Fix" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Run this command locally to generate the correct matrix:" >> $GITHUB_STEP_SUMMARY + echo '```bash' >> $GITHUB_STEP_SUMMARY + echo "make -f IONOS/Makefile generate_external_apps_matrix_json" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Then update the \`matrix\` variable in \`.github/workflows/build-artifact.yml\` in the set-matrix step with the generated output." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Show full diff in expandable section + echo "
" >> $GITHUB_STEP_SUMMARY + echo "📄 Full matrix comparison" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Workflow Matrix:**" >> $GITHUB_STEP_SUMMARY + echo '```json' >> $GITHUB_STEP_SUMMARY + echo "$workflow_matrix" | jq '.' 2>/dev/null >> $GITHUB_STEP_SUMMARY || echo "$workflow_matrix" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Makefile Matrix:**" >> $GITHUB_STEP_SUMMARY + echo '```json' >> $GITHUB_STEP_SUMMARY + echo "$generated_matrix" | jq '.' 2>/dev/null >> $GITHUB_STEP_SUMMARY || echo "$generated_matrix" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + + echo "" + echo "❌ ERROR: Matrix validation failed!" + echo "See the job summary for details on what's wrong and how to fix it." + echo "Summary file size: $(wc -c < $GITHUB_STEP_SUMMARY || echo 0) bytes" + exit 1 + fi + + build-external-apps: + runs-on: ubuntu-latest + needs: prepare-matrix + + permissions: + contents: read + + name: build-external-apps + strategy: + max-parallel: 20 + matrix: + app: ${{ fromJson(needs.prepare-matrix.outputs.external-apps-matrix) }} + + steps: + - name: Checkout server + uses: actions/checkout@v5 + with: + submodules: true + fetch-depth: '1' + + - name: Set up node with version from package.json's engines + if: matrix.app.has_npm + uses: actions/setup-node@v5 + with: + node-version-file: "package.json" + cache: 'npm' + cache-dependency-path: ${{ matrix.app.path }}/package-lock.json + + - name: Setup PHP with PECL extension + if: matrix.app.has_composer + uses: shivammathur/setup-php@c541c155eee45413f5b09a52248675b1a2575231 #v2.31.1 + with: + tools: composer:v2 + extensions: gd, zip, curl, xml, xmlrpc, mbstring, sqlite, xdebug, pgsql, intl, imagick, gmp, apcu, bcmath, redis, soap, imap, opcache + env: + runner: ubuntu-latest + + - name: Cache Composer dependencies for ${{ matrix.app.name }} + if: matrix.app.has_composer + uses: actions/cache@v4 + with: + path: ${{ matrix.app.path }}/vendor + key: ${{ runner.os }}-composer-${{ matrix.app.name }}-${{ hashFiles(format('{0}/composer.lock', matrix.app.path)) }} + restore-keys: | + ${{ runner.os }}-composer-${{ matrix.app.name }}- + + - name: Build ${{ matrix.app.name }} app + run: make -f IONOS/Makefile ${{ matrix.app.makefile_target }} + + - name: Upload ${{ matrix.app.name }} build artifacts + uses: actions/upload-artifact@v4 + with: + retention-days: 1 + name: external-app-build-${{ matrix.app.name }} + path: | + ${{ matrix.app.path }} + !${{ matrix.app.path }}/node_modules + + build-artifact: + runs-on: ubuntu-latest + needs: [prepare-matrix, build-external-apps] + + permissions: + contents: read + + outputs: + NC_VERSION: ${{ steps.get_nc_version.outputs.NC_VERSION }} + + name: build-artifact + steps: + - name: Checkout server + uses: actions/checkout@v5 + with: + submodules: true + fetch-depth: '1' + + - name: Download build external apps + uses: actions/download-artifact@v5 + with: + pattern: external-app-build-* + path: apps-external/ + + - name: Reorganize downloaded apps-external artifacts + run: | + cd apps-external/ + + echo "Initial structure:" + ls -la + + # Move contents from external-app-build-* directories to their target directories + for artifact_dir in external-app-build-*; do + if [ -d "$artifact_dir" ]; then + # Extract app name from artifact directory name + app_name=${artifact_dir#external-app-build-} + + echo "Processing artifact: $artifact_dir -> $app_name" + + # If target directory exists, merge the contents from the artifact directory containing build artifacts + if [ -d "$app_name" ]; then + echo "Target directory $app_name exists, merging contents from $artifact_dir" + # Copy contents from artifact directory to target directory + cp -r "$artifact_dir"/* "$app_name"/ + # Remove the now-empty artifact directory + rm -rf "$artifact_dir" + else + # Move the artifact directory to the proper app name + echo "Moving $artifact_dir to $app_name" + mv "$artifact_dir" "$app_name" + fi + fi + done + + echo "Reorganization complete. Final structure:" + ls -la + + - name: Verify downloaded artifacts structure + run: | + echo "External apps structure:" + ls -la apps-external/ + for app_dir in apps-external/*/; do + if [ -d "$app_dir" ]; then + echo "Contents of $app_dir:" + ls -la "$app_dir" + fi + done + + - name: Set up node with version from package.json's engines + uses: actions/setup-node@v5 + with: + node-version-file: "package.json" + cache: 'npm' + + - name: Install Dependencies + run: sudo apt-get update && sudo apt-get install -y make zip unzip + + - name: Print dependencies versions + run: make --version && node --version && npm --version + + - name: Setup PHP with PECL extension + uses: shivammathur/setup-php@c541c155eee45413f5b09a52248675b1a2575231 #v2.31.1 + with: + tools: composer:v2 + extensions: gd, zip, curl, xml, xmlrpc, mbstring, sqlite, xdebug, pgsql, intl, imagick, gmp, apcu, bcmath, redis, soap, imap, opcache + env: + runner: ubuntu-latest + + - name: Cache Composer dependencies + uses: actions/cache@v4 + with: + path: vendor + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + + - name: Print PHP install + run: php -i && php -m + + - name: Build Nextcloud + run: make -f IONOS/Makefile build_ncw + + - name: Add config partials + run: make -f IONOS/Makefile add_config_partials + + - name: Zip dependencies + run: make -f IONOS/Makefile zip_dependencies TARGET_PACKAGE_NAME=${{ env.TARGET_PACKAGE_NAME }} + + - name: Get NC version + id: get_nc_version + continue-on-error: false + run: | + NC_VERSION=$(jq -r '.ncVersion' version.json) + echo "NC_VERSION: $NC_VERSION" + + if [ -z "$NC_VERSION" ]; then + echo "NC_VERSION is empty" + exit 1 + fi + + echo "NC_VERSION=$NC_VERSION" >> $GITHUB_OUTPUT + + - name: Upload artifact result for job build-artifact + uses: actions/upload-artifact@v4 + with: + retention-days: 30 + name: nextcloud_workspace_build_artifact + path: ${{ env.TARGET_PACKAGE_NAME }} + + - name: Show changes on failure + if: failure() + run: | + git status + git --no-pager diff + exit 1 # make it red to grab attention + + upload-to-artifactory: + runs-on: self-hosted + # Upload the artifact to the Artifactory repository on PR *OR* on "ionos-dev|ionos-stable" branch push defined in the on:push:branches + if: github.event_name == 'pull_request' || github.ref_name == 'ionos-dev' || github.ref_name == 'ionos-stable' + + name: Push to artifactory + needs: [prepare-matrix, build-external-apps, build-artifact] + + outputs: + ARTIFACTORY_LAST_BUILD_PATH: ${{ steps.artifactory_upload.outputs.ARTIFACTORY_LAST_BUILD_PATH }} + + env: + BUILD_NAME: "nextcloud-workspace-snapshot" + + steps: + - name: Check prerequisites + run: | + # count the number of secrets that are set + echo "Checking if required secrets are set..." + error_count=0 + + if [ -z "${{ secrets.JF_ARTIFACTORY_URL }}" ]; then + # output error to github actions log + echo "::error::JF_ARTIFACTORY_URL secret is not set" + error_count=$((error_count + 1)) + fi + + if [ -z "${{ secrets.JF_ARTIFACTORY_USER }}" ]; then + echo "::error::JF_ARTIFACTORY_USER secret is not set" + error_count=$((error_count + 1)) + fi + + if [ -z "${{ secrets.JF_ACCESS_TOKEN }}" ]; then + echo "::error::JF_ACCESS_TOKEN secret is not set" + error_count=$((error_count + 1)) + fi + + # abort if any of the required secrets are not set + if [ $error_count -ne 0 ]; then + echo "::error::Required secrets are not set. Aborting." + exit 1 + fi + + - name: Download artifact zip + uses: actions/download-artifact@v5 + with: + name: nextcloud_workspace_build_artifact + + # This action sets up the JFrog CLI with the Artifactory URL and access token + - uses: jfrog/setup-jfrog-cli@7c95feb32008765e1b4e626b078dfd897c4340ad # v4.4.1 + env: + JF_URL: ${{ secrets.JF_ARTIFACTORY_URL }} + JF_USER: ${{ secrets.JF_ARTIFACTORY_USER }} + JF_ACCESS_TOKEN: ${{ secrets.JF_ACCESS_TOKEN }} + + - name: Ping the JF server + run: | + # Ping the server + jf rt ping + + - name: Upload build to artifactory + id: artifactory_upload + run: | + # PR builds are stored in a separate directory as "dev/pr/nextcloud-workspace-pr-.zip" + # Push to "ionos-dev" branch is stored as "dev/nextcloud-workspace-.zip" + + ARTIFACTORY_STAGE_PREFIX="dev" + + # set ARTIFACTORY_STAGE_PREFIX=stable on ionos-stable branch + if [ "${{ github.ref_name }}" == "ionos-stable" ]; then + ARTIFACTORY_STAGE_PREFIX="stable" + fi + + export PATH_TO_DIRECTORY="${{ env.ARTIFACTORY_REPOSITORY_SNAPSHOT }}/${ARTIFACTORY_STAGE_PREFIX}" + PATH_TO_FILE="pr/nextcloud-workspace-pr-${{ github.event.pull_request.number }}.zip" + + if [ -z "${{ github.event.pull_request.number }}" ]; then + PATH_TO_FILE="nextcloud-workspace-${{ needs.build-artifact.outputs.NC_VERSION }}.zip" + fi + + export PATH_TO_LATEST_ARTIFACT="${PATH_TO_DIRECTORY}/${PATH_TO_FILE}" + + # Promote current build to the "latest" dev build + jf rt upload "${{ env.TARGET_PACKAGE_NAME }}" \ + --build-name "${{ env.BUILD_NAME }}" \ + --build-number ${{ github.run_number }} \ + --target-props "build.nc_version=${{ needs.build-artifact.outputs.NC_VERSION }};vcs.branch=${{ github.ref }};vcs.revision=${{ github.sha }}" \ + $PATH_TO_LATEST_ARTIFACT + + echo "ARTIFACTORY_LAST_BUILD_PATH=${PATH_TO_LATEST_ARTIFACT}" >> $GITHUB_OUTPUT + + - name: Show changes on failure + if: failure() + run: | + git status + git --no-pager diff + exit 1 # make it red to grab attention + + nextcloud-workspace-artifact-to-ghcr_io: + runs-on: ubuntu-latest + + permissions: + contents: read + packages: write + + name: Push artifact to ghcr.io + needs: [prepare-matrix, build-external-apps, build-artifact] + + steps: + - name: Download artifact zip + uses: actions/download-artifact@v5 + with: + name: nextcloud_workspace_build_artifact + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" + + - name: Create Dockerfile + run: | + cat >Dockerfile << EOF + FROM busybox as builder + COPY ./${{ env.TARGET_PACKAGE_NAME }} / + WORKDIR /builder + RUN unzip /${{ env.TARGET_PACKAGE_NAME }} -d /builder + + FROM scratch + WORKDIR /app + VOLUME /app + COPY --from=builder /builder /app + EOF + + - name: Build and push Docker image + uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Show changes on failure + if: failure() + run: | + echo "Git status:" + git status + echo "Git diff:" + git diff + exit 1 # make it red to grab attention + + trigger-remote-dev-worflow: + runs-on: self-hosted + + name: Trigger remote workflow + needs: [ build-artifact, upload-to-artifactory ] + # Trigger remote build on "ionos-dev|ionos-stable" branch *push* defined in the on:push:branches + if: github.event_name == 'push' && ( github.ref_name == 'ionos-dev' || github.ref_name == 'ionos-stable' ) + steps: + - name: Trigger remote workflow + run: | + # Enable command echo for debugging purposes + set -x + + # Determine build type based on branch: + # - 'ionos-dev' branch triggers 'dev' build type + # - 'ionos-stable' branch triggers 'stable' build type + BUILD_TYPE="dev" + + # Override build type for stable branch + if [ "${{ github.ref_name }}" == "ionos-stable" ]; then + BUILD_TYPE="stable" + fi + + # Trigger GitLab pipeline via webhook with build artifacts and metadata + # Passes GitHub context variables to remote GitLab workflow + curl \ + --silent \ + --insecure \ + --request POST \ + --fail-with-body \ + -o response.json \ + --form token=${{ secrets.GITLAB_TOKEN }} \ + --form ref="stable" \ + --form "variables[GITHUB_SHA]=${{ github.sha }}" \ + --form "variables[ARTIFACTORY_LAST_BUILD_PATH]=${{ needs.upload-to-artifactory.outputs.ARTIFACTORY_LAST_BUILD_PATH }}" \ + --form "variables[NC_VERSION]=${{ needs.build-artifact.outputs.NC_VERSION }}" \ + --form "variables[BUILD_ID]=${{ github.run_id }}" \ + --form "variables[BUILD_TYPE]=${BUILD_TYPE}" \ + "${{ secrets.GITLAB_TRIGGER_URL }}" || ( RETCODE="$?"; jq . response.json; exit "$RETCODE" ) + + # Disable command echo + set +x + + # Print and parse json + # jq . response.json + echo "json<> $GITHUB_OUTPUT + cat response.json >> $GITHUB_OUTPUT + echo "END" >> $GITHUB_OUTPUT + echo "web_url<> $GITHUB_OUTPUT + cat response.json | jq --raw-output '.web_url' >> $GITHUB_OUTPUT + echo "END" >> $GITHUB_OUTPUT + + - name: Show changes on failure + if: failure() + run: | + git status + git --no-pager diff + exit 1 # make it red to grab attention From 4ed5ba78a40fca453b8f0fadbecd37583a5df374 Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Wed, 12 Nov 2025 12:14:41 +0100 Subject: [PATCH 54/83] IONOS(build): update artifact filename to include '-original' suffix in order not to overwrite optimized artifact Signed-off-by: Misha M.-Kupriyanov --- .github/workflows/build-artifact-original.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-artifact-original.yml b/.github/workflows/build-artifact-original.yml index a2cbfdb849649..8d76945d5b7d4 100644 --- a/.github/workflows/build-artifact-original.yml +++ b/.github/workflows/build-artifact-original.yml @@ -780,10 +780,10 @@ jobs: fi export PATH_TO_DIRECTORY="${{ env.ARTIFACTORY_REPOSITORY_SNAPSHOT }}/${ARTIFACTORY_STAGE_PREFIX}" - PATH_TO_FILE="pr/nextcloud-workspace-pr-${{ github.event.pull_request.number }}.zip" + PATH_TO_FILE="pr/nextcloud-workspace-pr-${{ github.event.pull_request.number }}-original.zip" if [ -z "${{ github.event.pull_request.number }}" ]; then - PATH_TO_FILE="nextcloud-workspace-${{ needs.build-artifact.outputs.NC_VERSION }}.zip" + PATH_TO_FILE="nextcloud-workspace-${{ needs.build-artifact.outputs.NC_VERSION }}-original.zip" fi export PATH_TO_LATEST_ARTIFACT="${PATH_TO_DIRECTORY}/${PATH_TO_FILE}" From 04239d3a079189df3d4591f85d0821e87ab3268b Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Wed, 12 Nov 2025 15:55:13 +0100 Subject: [PATCH 55/83] IONOS(build): update fetch-depth in build-artifact.yml to integer format Signed-off-by: Misha M.-Kupriyanov --- .github/workflows/build-artifact.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-artifact.yml b/.github/workflows/build-artifact.yml index a2cbfdb849649..c2d1e43de61a6 100644 --- a/.github/workflows/build-artifact.yml +++ b/.github/workflows/build-artifact.yml @@ -54,7 +54,7 @@ jobs: uses: actions/checkout@v5 with: submodules: true - fetch-depth: '1' + fetch-depth: 1 # Shallow clone - only need current submodule SHAs for cache detection - name: Install dependencies run: sudo apt-get update && sudo apt-get install -y make jq @@ -530,7 +530,7 @@ jobs: uses: actions/checkout@v5 with: submodules: true - fetch-depth: '1' + fetch-depth: 1 - name: Set up node with version from package.json's engines if: matrix.app.has_npm @@ -586,7 +586,7 @@ jobs: uses: actions/checkout@v5 with: submodules: true - fetch-depth: '1' + fetch-depth: 1 - name: Download build external apps uses: actions/download-artifact@v5 From dcc7e3ac26803e7490220e86ff612fb2da708966 Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Wed, 12 Nov 2025 16:01:31 +0100 Subject: [PATCH 56/83] IONOS(build): update action versions for improved functionality and compatibility Signed-off-by: Misha M.-Kupriyanov --- .github/workflows/build-artifact.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-artifact.yml b/.github/workflows/build-artifact.yml index c2d1e43de61a6..5c3ae2a7995a7 100644 --- a/.github/workflows/build-artifact.yml +++ b/.github/workflows/build-artifact.yml @@ -534,7 +534,7 @@ jobs: - name: Set up node with version from package.json's engines if: matrix.app.has_npm - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version-file: "package.json" cache: 'npm' @@ -562,7 +562,7 @@ jobs: run: make -f IONOS/Makefile ${{ matrix.app.makefile_target }} - name: Upload ${{ matrix.app.name }} build artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: retention-days: 1 name: external-app-build-${{ matrix.app.name }} @@ -589,7 +589,7 @@ jobs: fetch-depth: 1 - name: Download build external apps - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v6 with: pattern: external-app-build-* path: apps-external/ @@ -639,7 +639,7 @@ jobs: done - name: Set up node with version from package.json's engines - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version-file: "package.json" cache: 'npm' @@ -693,7 +693,7 @@ jobs: echo "NC_VERSION=$NC_VERSION" >> $GITHUB_OUTPUT - name: Upload artifact result for job build-artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: retention-days: 30 name: nextcloud_workspace_build_artifact @@ -750,7 +750,7 @@ jobs: fi - name: Download artifact zip - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v6 with: name: nextcloud_workspace_build_artifact @@ -816,7 +816,7 @@ jobs: steps: - name: Download artifact zip - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v6 with: name: nextcloud_workspace_build_artifact From e074c0b7f00e0b8fcbdb4e02bbd55a7efe88604c Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Wed, 12 Nov 2025 16:12:06 +0100 Subject: [PATCH 57/83] IONOS(build): add retry logic for GitLab pipeline trigger Signed-off-by: Misha M.-Kupriyanov --- .github/workflows/build-artifact.yml | 65 +++++++++++++++++++++------- 1 file changed, 49 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build-artifact.yml b/.github/workflows/build-artifact.yml index 5c3ae2a7995a7..e5735f20c58e5 100644 --- a/.github/workflows/build-artifact.yml +++ b/.github/workflows/build-artifact.yml @@ -887,22 +887,55 @@ jobs: BUILD_TYPE="stable" fi - # Trigger GitLab pipeline via webhook with build artifacts and metadata - # Passes GitHub context variables to remote GitLab workflow - curl \ - --silent \ - --insecure \ - --request POST \ - --fail-with-body \ - -o response.json \ - --form token=${{ secrets.GITLAB_TOKEN }} \ - --form ref="stable" \ - --form "variables[GITHUB_SHA]=${{ github.sha }}" \ - --form "variables[ARTIFACTORY_LAST_BUILD_PATH]=${{ needs.upload-to-artifactory.outputs.ARTIFACTORY_LAST_BUILD_PATH }}" \ - --form "variables[NC_VERSION]=${{ needs.build-artifact.outputs.NC_VERSION }}" \ - --form "variables[BUILD_ID]=${{ github.run_id }}" \ - --form "variables[BUILD_TYPE]=${BUILD_TYPE}" \ - "${{ secrets.GITLAB_TRIGGER_URL }}" || ( RETCODE="$?"; jq . response.json; exit "$RETCODE" ) + # Trigger GitLab pipeline via webhook with retry logic (3 attempts with 30s delay) + MAX_ATTEMPTS=3 + ATTEMPT=1 + TRIGGER_SUCCESS=false + DELAY_SEC=5 + + while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do + echo "Trigger attempt $ATTEMPT of $MAX_ATTEMPTS..." + + if curl \ + --silent \ + --insecure \ + --request POST \ + --fail-with-body \ + -o response.json \ + --form token=${{ secrets.GITLAB_TOKEN }} \ + --form ref="stable" \ + --form "variables[GITHUB_SHA]=${{ github.sha }}" \ + --form "variables[ARTIFACTORY_LAST_BUILD_PATH]=${{ needs.upload-to-artifactory.outputs.ARTIFACTORY_LAST_BUILD_PATH }}" \ + --form "variables[NC_VERSION]=${{ needs.build-artifact.outputs.NC_VERSION }}" \ + --form "variables[BUILD_ID]=${{ github.run_id }}" \ + --form "variables[BUILD_TYPE]=${BUILD_TYPE}" \ + "${{ secrets.GITLAB_TRIGGER_URL }}"; then + TRIGGER_SUCCESS=true + echo "✅ Trigger successful on attempt $ATTEMPT" + break + else + RETCODE="$?" + echo "âš ī¸ Trigger attempt $ATTEMPT failed with code $RETCODE" + if [ -f response.json ]; then + jq . response.json || cat response.json + fi + if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then + echo "Waiting ${DELAY_SEC} seconds before retry..." + sleep $DELAY_SEC + DELAY_SEC=$((DELAY_SEC * 2)) # Exponential backoff: 5s, 10s, 20s + fi + fi + + ATTEMPT=$((ATTEMPT + 1)) + done + + if [ "$TRIGGER_SUCCESS" != "true" ]; then + echo "❌ Trigger failed after $MAX_ATTEMPTS attempts" + if [ -f response.json ]; then + jq . response.json || cat response.json + fi + exit 1 + fi # Disable command echo set +x From 58b1c95d5c9c00f428a9e893ed5d6df455aef7e0 Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Wed, 12 Nov 2025 16:12:38 +0100 Subject: [PATCH 58/83] IONOS(build): add retry logic for artifact upload in build-artifact.yml Signed-off-by: Misha M.-Kupriyanov --- .github/workflows/build-artifact.yml | 39 +++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-artifact.yml b/.github/workflows/build-artifact.yml index e5735f20c58e5..e603a517e1b7a 100644 --- a/.github/workflows/build-artifact.yml +++ b/.github/workflows/build-artifact.yml @@ -788,12 +788,39 @@ jobs: export PATH_TO_LATEST_ARTIFACT="${PATH_TO_DIRECTORY}/${PATH_TO_FILE}" - # Promote current build to the "latest" dev build - jf rt upload "${{ env.TARGET_PACKAGE_NAME }}" \ - --build-name "${{ env.BUILD_NAME }}" \ - --build-number ${{ github.run_number }} \ - --target-props "build.nc_version=${{ needs.build-artifact.outputs.NC_VERSION }};vcs.branch=${{ github.ref }};vcs.revision=${{ github.sha }}" \ - $PATH_TO_LATEST_ARTIFACT + # Upload with retry logic (3 attempts with 30s delay) + MAX_ATTEMPTS=3 + ATTEMPT=1 + UPLOAD_SUCCESS=false + DELAY_SEC=10 + + while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do + echo "Upload attempt $ATTEMPT of $MAX_ATTEMPTS..." + + if jf rt upload "${{ env.TARGET_PACKAGE_NAME }}" \ + --build-name "${{ env.BUILD_NAME }}" \ + --build-number ${{ github.run_number }} \ + --target-props "build.nc_version=${{ needs.build-artifact.outputs.NC_VERSION }};vcs.branch=${{ github.ref }};vcs.revision=${{ github.sha }}" \ + $PATH_TO_LATEST_ARTIFACT; then + UPLOAD_SUCCESS=true + echo "✅ Upload successful on attempt $ATTEMPT" + break + else + echo "âš ī¸ Upload attempt $ATTEMPT failed" + if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then + echo "Waiting $DELAY_SEC seconds before retry..." + sleep $DELAY_SEC + DELAY_SEC=$((DELAY_SEC * 2)) # Exponential backoff: delays are 10s, then 20s (sleep occurs after failed attempts) + fi + fi + + ATTEMPT=$((ATTEMPT + 1)) + done + + if [ "$UPLOAD_SUCCESS" != "true" ]; then + echo "❌ Upload failed after $MAX_ATTEMPTS attempts" + exit 1 + fi echo "ARTIFACTORY_LAST_BUILD_PATH=${PATH_TO_LATEST_ARTIFACT}" >> $GITHUB_OUTPUT From 5994f43d636e8f593396d45f3911d40731d0af7e Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Wed, 12 Nov 2025 16:29:14 +0100 Subject: [PATCH 59/83] IONOS(build): fix step ID naming in build-artifact.yml for consistency lets use underscore Signed-off-by: Misha M.-Kupriyanov --- .github/workflows/build-artifact.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-artifact.yml b/.github/workflows/build-artifact.yml index e603a517e1b7a..c17c69dd12e01 100644 --- a/.github/workflows/build-artifact.yml +++ b/.github/workflows/build-artifact.yml @@ -48,7 +48,7 @@ jobs: prepare-matrix: runs-on: ubuntu-latest outputs: - external-apps-matrix: ${{ steps.set-matrix.outputs.matrix }} + external-apps-matrix: ${{ steps.set_matrix.outputs.matrix }} steps: - name: Checkout repository uses: actions/checkout@v5 @@ -60,7 +60,7 @@ jobs: run: sudo apt-get update && sudo apt-get install -y make jq - name: Set matrix - id: set-matrix + id: set_matrix run: | # Create matrix configuration as a compact JSON string matrix='[ @@ -338,7 +338,7 @@ jobs: # So we need to extract just the JSON part generated_matrix=$(echo "$makefile_output" | grep -v '^\[i\]' || echo "$makefile_output") - workflow_matrix='${{ steps.set-matrix.outputs.matrix }}' + workflow_matrix='${{ steps.set_matrix.outputs.matrix }}' # Debug output echo "Generated matrix length: ${#generated_matrix}" @@ -487,7 +487,7 @@ jobs: echo "make -f IONOS/Makefile generate_external_apps_matrix_json" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "Then update the \`matrix\` variable in \`.github/workflows/build-artifact.yml\` in the set-matrix step with the generated output." >> $GITHUB_STEP_SUMMARY + echo "Then update the \`matrix\` variable in \`.github/workflows/build-artifact.yml\` in the set_matrix step with the generated output." >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY # Show full diff in expandable section From 28be12f873228c3e4455ee062de168fa06b18921 Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Wed, 12 Nov 2025 16:33:27 +0100 Subject: [PATCH 60/83] IONOS(build): generate apps matrix dynamically from Makefile drop validation since it is now Redundant (by design) Signed-off-by: Misha M.-Kupriyanov --- .github/workflows/build-artifact.yml | 459 +-------------------------- 1 file changed, 16 insertions(+), 443 deletions(-) diff --git a/.github/workflows/build-artifact.yml b/.github/workflows/build-artifact.yml index c17c69dd12e01..4900aa4329ba2 100644 --- a/.github/workflows/build-artifact.yml +++ b/.github/workflows/build-artifact.yml @@ -59,458 +59,31 @@ jobs: - name: Install dependencies run: sudo apt-get update && sudo apt-get install -y make jq - - name: Set matrix + - name: Generate apps matrix dynamically from Makefile id: set_matrix run: | - # Create matrix configuration as a compact JSON string - matrix='[ - { - "name": "activity", - "path": "apps-external/activity", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_activity_app" - }, - { - "name": "assistant", - "path": "apps-external/assistant", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_assistant_app" - }, - { - "name": "calendar", - "path": "apps-external/calendar", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_calendar_app" - }, - { - "name": "circles", - "path": "apps-external/circles", - "has_npm": false, - "has_composer": true, - "makefile_target": "build_circles_app" - }, - { - "name": "collectives", - "path": "apps-external/collectives", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_collectives_app" - }, - { - "name": "contacts", - "path": "apps-external/contacts", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_contacts_app" - }, - { - "name": "deck", - "path": "apps-external/deck", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_deck_app" - }, - { - "name": "end_to_end_encryption", - "path": "apps-external/end_to_end_encryption", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_end_to_end_encryption_app" - }, - { - "name": "forms", - "path": "apps-external/forms", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_forms_app" - }, - { - "name": "groupfolders", - "path": "apps-external/groupfolders", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_groupfolders_app" - }, - { - "name": "integration_openai", - "path": "apps-external/integration_openai", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_integration_openai_app" - }, - { - "name": "mail", - "path": "apps-external/mail", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_mail_app" - }, - { - "name": "ncw_apps_menu", - "path": "apps-external/ncw_apps_menu", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_ncw_apps_menu_app" - }, - { - "name": "ncw_mailtemplate", - "path": "apps-external/ncw_mailtemplate", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_ncw_mailtemplate_app" - }, - { - "name": "notes", - "path": "apps-external/notes", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_notes_app" - }, - { - "name": "notifications", - "path": "apps-external/notifications", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_notifications_app" - }, - { - "name": "notify_push", - "path": "apps-external/notify_push", - "has_npm": false, - "has_composer": true, - "makefile_target": "build_notify_push_app" - }, - { - "name": "password_policy", - "path": "apps-external/password_policy", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_password_policy_app" - }, - { - "name": "richdocuments", - "path": "apps-external/richdocuments", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_richdocuments_app" - }, - { - "name": "spreed", - "path": "apps-external/spreed", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_spreed_app" - }, - { - "name": "tables", - "path": "apps-external/tables", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_tables_app" - }, - { - "name": "tasks", - "path": "apps-external/tasks", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_tasks_app" - }, - { - "name": "text", - "path": "apps-external/text", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_text_app" - }, - { - "name": "twofactor_totp", - "path": "apps-external/twofactor_totp", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_twofactor_totp_app" - }, - { - "name": "user_oidc", - "path": "apps-external/user_oidc", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_user_oidc_app" - }, - { - "name": "viewer", - "path": "apps-external/viewer", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_viewer_app" - }, - { - "name": "whiteboard", - "path": "apps-external/whiteboard", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_whiteboard_app" - } - ]' - - # Validate JSON and output as compact format - if echo "$matrix" | jq empty 2>/dev/null; then - echo "matrix=$(echo "$matrix" | jq -c '.')" >> $GITHUB_OUTPUT - echo "Matrix configuration set successfully" - else - echo "Error: Invalid JSON in matrix configuration" - exit 1 - fi - - - name: Validate matrix against Makefile - run: | - set +e # Intentionally allow script to continue on error for custom error handling and reporting to GITHUB_STEP_SUMMARY - set -u # Exit on undefined variable - - echo "### 🔍 Matrix Validation" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Debug: Check if apps-external exists - echo "Checking apps-external directory..." - if [ ! -d "apps-external" ]; then - echo "❌ **Error:** apps-external directory does not exist!" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Directory listing:" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - ls -la >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - echo "Apps-external directory exists. Listing contents:" - ls -la apps-external/ | head -10 - - # Check if jq is available - echo "Checking if jq is installed..." - if ! command -v jq &> /dev/null; then - echo "❌ **Error:** jq is not installed!" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "jq is required for matrix generation but was not found in PATH." >> $GITHUB_STEP_SUMMARY - exit 1 - fi - echo "jq version: $(jq --version)" - + # Generate matrix from Makefile - single source of truth echo "Generating matrix from Makefile..." - # Capture both stdout and stderr separately to better diagnose issues - makefile_output=$(make -f IONOS/Makefile generate_external_apps_matrix_json 2>&1) - makefile_exit_code=$? - - echo "Makefile exit code: ${makefile_exit_code}" - echo "Makefile output length: ${#makefile_output}" - - # Debug: Check if GITHUB_STEP_SUMMARY is set - echo "GITHUB_STEP_SUMMARY: ${GITHUB_STEP_SUMMARY:-NOT SET}" - - # If the Makefile command failed, show the error - if [ ${makefile_exit_code} -ne 0 ]; then - echo "" - echo "=== MAKEFILE ERROR ===" - echo "Exit code: ${makefile_exit_code}" - echo "Output:" - echo "$makefile_output" - echo "=====================" - echo "" - - # Write to summary - echo "❌ **Error:** Makefile command failed with exit code ${makefile_exit_code}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "Makefile error output" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "$makefile_output" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - - echo "Error written to summary file: ${GITHUB_STEP_SUMMARY}" - exit 1 - fi - - # Filter out the info message to get just the JSON - # The Makefile outputs "[i] Generating..." to stderr, but we captured everything with 2>&1 - # So we need to extract just the JSON part - generated_matrix=$(echo "$makefile_output" | grep -v '^\[i\]' || echo "$makefile_output") - - workflow_matrix='${{ steps.set_matrix.outputs.matrix }}' - - # Debug output - echo "Generated matrix length: ${#generated_matrix}" - echo "Workflow matrix length: ${#workflow_matrix}" + matrix_output=$(make -f IONOS/Makefile generate_external_apps_matrix_json 2>&1) - # Show first 200 chars of generated matrix for debugging - if [ -n "$generated_matrix" ]; then - echo "Generated matrix preview: ${generated_matrix:0:200}..." - fi - - # Validate that we got valid JSON - if ! echo "$generated_matrix" | jq empty 2>/dev/null; then - echo "❌ **Error:** Generated matrix is not valid JSON" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "Invalid JSON output" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "$generated_matrix" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - exit 1 + # Filter out info messages to get just the JSON + # Note: Use same filtering logic as validation script to ensure consistency + if echo "$matrix_output" | grep -q '^\[i\]'; then + matrix=$(echo "$matrix_output" | grep -v '^\[i\]') + else + matrix="$matrix_output" fi - # Validate that we got data - if [ -z "$generated_matrix" ] || [ -z "$workflow_matrix" ]; then - echo "❌ **Error:** Failed to load matrices" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "- Generated matrix empty: $([ -z "$generated_matrix" ] && echo "yes" || echo "no")" >> $GITHUB_STEP_SUMMARY - echo "- Workflow matrix empty: $([ -z "$workflow_matrix" ] && echo "yes" || echo "no")" >> $GITHUB_STEP_SUMMARY - echo "- Makefile exit code: ${makefile_exit_code}" >> $GITHUB_STEP_SUMMARY - + # Validate JSON + if ! echo "$matrix" | jq empty 2>/dev/null; then + echo "Error: Generated matrix is not valid JSON" + echo "Output: $matrix_output" exit 1 fi - # Sort both matrices for comparison - generated_sorted=$(echo "$generated_matrix" | jq -S '.' 2>&1 || echo "ERROR") - workflow_sorted=$(echo "$workflow_matrix" | jq -S '.' 2>&1 || echo "ERROR") - - echo "Sorted matrix lengths - generated: ${#generated_sorted}, workflow: ${#workflow_sorted}" - - # Compare the two matrices - if [ "$generated_sorted" = "$workflow_sorted" ]; then - echo "✅ **Validation passed!** The workflow matrix matches the Makefile configuration." >> $GITHUB_STEP_SUMMARY - echo "" - echo "✅ Matrix validation passed!" - else - echo "❌ **Validation failed!** The workflow matrix does not match the Makefile configuration." >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - echo "Starting detailed comparison..." - - # Extract app names from both matrices - generated_apps=$(echo "$generated_matrix" | jq -r '.[].name' 2>/dev/null | sort || echo "") - workflow_apps=$(echo "$workflow_matrix" | jq -r '.[].name' 2>/dev/null | sort || echo "") - - echo "Generated apps count: $(echo "$generated_apps" | wc -l)" - echo "Workflow apps count: $(echo "$workflow_apps" | wc -l)" - - # Find missing apps (in Makefile but not in workflow) - missing_apps=$(comm -23 <(echo "$generated_apps") <(echo "$workflow_apps")) - if [ $? -ne 0 ]; then - echo "Error: comm command failed when finding missing apps." >&2 - exit 1 - fi - # Find extra apps (in workflow but not in Makefile) - extra_apps=$(comm -13 <(echo "$generated_apps") <(echo "$workflow_apps")) - if [ $? -ne 0 ]; then - echo "Error: comm command failed when finding extra apps." >&2 - exit 1 - fi - - echo "Missing apps: ${missing_apps:-none}" - echo "Extra apps: ${extra_apps:-none}" - - if [ -n "$missing_apps" ]; then - echo "#### âš ī¸ Missing Apps" >> $GITHUB_STEP_SUMMARY - echo "The following apps are configured in the Makefile but missing from the workflow:" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "$missing_apps" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - fi - - if [ -n "$extra_apps" ]; then - echo "#### âš ī¸ Extra Apps" >> $GITHUB_STEP_SUMMARY - echo "The following apps are in the workflow but not configured in the Makefile:" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "$extra_apps" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - fi - - # Check for configuration mismatches in common apps - common_apps=$(comm -12 <(echo "$generated_apps") <(echo "$workflow_apps") 2>/dev/null || echo "") - - echo "Common apps count: $(echo "$common_apps" | wc -l)" - - if [ -n "$common_apps" ]; then - mismatched_apps="" - - while IFS= read -r app; do - [ -z "$app" ] && continue - gen_config=$(echo "$generated_matrix" | jq -c --arg app "$app" '.[] | select(.name == $app)' 2>/dev/null || echo "") - wf_config=$(echo "$workflow_matrix" | jq -c --arg app "$app" '.[] | select(.name == $app)' 2>/dev/null || echo "") - - if [ -n "$gen_config" ] && [ -n "$wf_config" ] && [ "$gen_config" != "$wf_config" ]; then - mismatched_apps="${mismatched_apps}${app}"$'\n' - fi - done <<< "$common_apps" - - echo "Mismatched apps: ${mismatched_apps:-none}" - - if [ -n "$mismatched_apps" ]; then - echo "#### âš ī¸ Configuration Mismatches" >> $GITHUB_STEP_SUMMARY - echo "The following apps have different configurations (has_npm, has_composer, etc.):" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "$mismatched_apps" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "📋 Detailed differences" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo '```diff' >> $GITHUB_STEP_SUMMARY - - while IFS= read -r app; do - [ -z "$app" ] && continue - echo "=== $app ===" >> $GITHUB_STEP_SUMMARY - diff -u --label "Workflow" --label "Makefile" \ - <(echo "$workflow_matrix" | jq --arg app "$app" '.[] | select(.name == $app)' 2>/dev/null || echo "{}") \ - <(echo "$generated_matrix" | jq --arg app "$app" '.[] | select(.name == $app)' 2>/dev/null || echo "{}") \ - >> $GITHUB_STEP_SUMMARY 2>&1 || true - done <<< "$mismatched_apps" - - echo '```' >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - fi - fi - - # Provide fix instructions - echo "#### 🔧 How to Fix" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Run this command locally to generate the correct matrix:" >> $GITHUB_STEP_SUMMARY - echo '```bash' >> $GITHUB_STEP_SUMMARY - echo "make -f IONOS/Makefile generate_external_apps_matrix_json" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Then update the \`matrix\` variable in \`.github/workflows/build-artifact.yml\` in the set_matrix step with the generated output." >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Show full diff in expandable section - echo "
" >> $GITHUB_STEP_SUMMARY - echo "📄 Full matrix comparison" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Workflow Matrix:**" >> $GITHUB_STEP_SUMMARY - echo '```json' >> $GITHUB_STEP_SUMMARY - echo "$workflow_matrix" | jq '.' 2>/dev/null >> $GITHUB_STEP_SUMMARY || echo "$workflow_matrix" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Makefile Matrix:**" >> $GITHUB_STEP_SUMMARY - echo '```json' >> $GITHUB_STEP_SUMMARY - echo "$generated_matrix" | jq '.' 2>/dev/null >> $GITHUB_STEP_SUMMARY || echo "$generated_matrix" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - - echo "" - echo "❌ ERROR: Matrix validation failed!" - echo "See the job summary for details on what's wrong and how to fix it." - echo "Summary file size: $(wc -c < $GITHUB_STEP_SUMMARY || echo 0) bytes" - exit 1 - fi + # Output as compact format + echo "matrix=$(echo "$matrix" | jq -c '.')" >> $GITHUB_OUTPUT + echo "Matrix generated successfully with $(echo "$matrix" | jq 'length') apps" build-external-apps: runs-on: ubuntu-latest From 81e45a579429247396c0a7915fff19423bfd2ee5 Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Tue, 11 Nov 2025 15:33:09 +0100 Subject: [PATCH 61/83] IONOS(build): optimize build workflow - only rebuild changed apps Optimized build workflow that uses cache-based detection - Checks cache for each app's current SHA - Only builds apps with no cached build - Significantly reduces build time through smart caching Signed-off-by: Misha M.-Kupriyanov --- .github/workflows/build-artifact.yml | 327 +++++++++++++++++++++++++-- 1 file changed, 304 insertions(+), 23 deletions(-) diff --git a/.github/workflows/build-artifact.yml b/.github/workflows/build-artifact.yml index 4900aa4329ba2..ff9a48391a938 100644 --- a/.github/workflows/build-artifact.yml +++ b/.github/workflows/build-artifact.yml @@ -1,12 +1,13 @@ -name: Build Nextcloud Workspace artifact +name: Build Nextcloud Workspace artifact (Optimized) # SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors # SPDX-FileCopyrightText: 2025 STRATO AG # SPDX-License-Identifier: AGPL-3.0-or-later -# The Nextcloud Workspace source is packaged as a container image. -# This is a workaround because releases cannot be created without tags, -# and we want to be able to create snapshots from branches. +# Optimized build workflow using GitHub Actions cache +# - Checks GitHub Actions cache for each app's current SHA +# - Only builds apps without cached artifacts +# - Significantly reduces build time by reusing cached builds via GitHub Actions cache on: pull_request: @@ -40,6 +41,10 @@ env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} ARTIFACTORY_REPOSITORY_SNAPSHOT: ionos-productivity-ncwserver-snapshot + # Cache version - increment this to invalidate all caches when build process changes + # Update when: Node.js version changes, PHP version changes, build scripts modified, etc. + # Format: v. (e.g., v1.0, v1.1, v2.0) + CACHE_VERSION: v1.0 permissions: contents: read @@ -48,7 +53,11 @@ jobs: prepare-matrix: runs-on: ubuntu-latest outputs: - external-apps-matrix: ${{ steps.set_matrix.outputs.matrix }} + apps_to_build: ${{ steps.detect.outputs.apps_to_build }} + apps_to_restore: ${{ steps.detect.outputs.apps_to_restore }} + external_apps_matrix: ${{ steps.set_matrix.outputs.matrix }} + has_cached_apps: ${{ steps.detect.outputs.has_cached_apps }} + apps_sha_map: ${{ steps.detect.outputs.apps_sha_map }} steps: - name: Checkout repository uses: actions/checkout@v5 @@ -85,9 +94,165 @@ jobs: echo "matrix=$(echo "$matrix" | jq -c '.')" >> $GITHUB_OUTPUT echo "Matrix generated successfully with $(echo "$matrix" | jq 'length') apps" + - name: Collect apps and their SHAs for cache-based building + id: detect + env: + GH_TOKEN: ${{ github.token }} + CACHE_VERSION: ${{ env.CACHE_VERSION }} + run: | + set -e # Exit on error + set -u # Exit on undefined variable + set -o pipefail # Exit if any command in pipeline fails + + echo "Collecting app SHAs and checking cache status..." + echo "" + + # Get the matrix from previous step + MATRIX='${{ steps.set_matrix.outputs.matrix }}' + + # Build JSON array for apps that actually need building + APPS_TO_BUILD="[]" + # Build JSON array for apps that are cached and need restoring + APPS_TO_RESTORE="[]" + APPS_CHECKED=0 + APPS_CACHED=0 + APPS_TO_BUILD_COUNT=0 + APPS_SHA_MAP="{}" + + echo "### đŸ“Ļ Cache Status Report" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| App | SHA | Cache Key | Status |" >> $GITHUB_STEP_SUMMARY + echo "|-----|-----|-----------|--------|" >> $GITHUB_STEP_SUMMARY + + # Iterate through each app in the matrix + while IFS= read -r app_json; do + APP_NAME=$(echo "$app_json" | jq -r '.name') + APP_PATH=$(echo "$app_json" | jq -r '.path') + + APPS_CHECKED=$((APPS_CHECKED + 1)) + + # Get current submodule SHA + if [ -d "$APP_PATH" ]; then + CURRENT_SHA=$(git -C "$APP_PATH" rev-parse HEAD 2>/dev/null || echo "") + else + echo "⊘ $APP_NAME - directory not found, will build" + echo "| $APP_NAME | N/A | N/A | ⊘ Directory not found |" >> $GITHUB_STEP_SUMMARY + APPS_TO_BUILD=$(echo "$APPS_TO_BUILD" | jq -c --arg app "$APP_NAME" --arg sha "unknown" '. + [{name: $app, sha: $sha}]') + APPS_TO_BUILD_COUNT=$((APPS_TO_BUILD_COUNT + 1)) + continue + fi + + if [ -z "$CURRENT_SHA" ]; then + echo "⊘ $APP_NAME - not a git repo, will build" + echo "| $APP_NAME | N/A | N/A | ⊘ Not a git repo |" >> $GITHUB_STEP_SUMMARY + APPS_TO_BUILD=$(echo "$APPS_TO_BUILD" | jq -c --arg app "$APP_NAME" --arg sha "unknown" '. + [{name: $app, sha: $sha}]') + APPS_TO_BUILD_COUNT=$((APPS_TO_BUILD_COUNT + 1)) + continue + fi + + # Add SHA to the map for all apps (regardless of cache status) + APPS_SHA_MAP=$(echo "$APPS_SHA_MAP" | jq -c --arg app "$APP_NAME" --arg sha "$CURRENT_SHA" '.[$app] = $sha') + + # Cache key that would be used for this app + # Format: -app-build-- + CACHE_KEY="${CACHE_VERSION}-app-build-${APP_NAME}-${CURRENT_SHA}" + SHORT_SHA="${CURRENT_SHA:0:8}" + + echo -n " Checking $APP_NAME (SHA: $SHORT_SHA)... " + + # Check if cache exists using GitHub CLI + CACHE_EXISTS="false" + if ! CACHE_LIST=$(gh cache list --key "$CACHE_KEY" --json key --jq ".[].key" 2>&1); then + echo "âš ī¸ Warning: Failed to query cache for $APP_NAME: $CACHE_LIST" + echo "| $APP_NAME | \`$SHORT_SHA\` | \`$CACHE_KEY\` | âš ī¸ Cache check failed - will build |" >> $GITHUB_STEP_SUMMARY + APPS_TO_BUILD=$(echo "$APPS_TO_BUILD" | jq -c --arg app "$APP_NAME" --arg sha "$CURRENT_SHA" '. + [{name: $app, sha: $sha}]') + APPS_TO_BUILD_COUNT=$((APPS_TO_BUILD_COUNT + 1)) + continue + fi + if echo "$CACHE_LIST" | grep -q "^${CACHE_KEY}$"; then + CACHE_EXISTS="true" + APPS_CACHED=$((APPS_CACHED + 1)) + echo "✓ cached" + echo "| $APP_NAME | \`$SHORT_SHA\` | \`$CACHE_KEY\` | ✅ Cached |" >> $GITHUB_STEP_SUMMARY + # Add to restore list with SHA + APPS_TO_RESTORE=$(echo "$APPS_TO_RESTORE" | jq -c --argjson app "$app_json" --arg sha "$CURRENT_SHA" '. + [($app + {sha: $sha})]') + else + echo "⚡ needs build" + echo "| $APP_NAME | \`$SHORT_SHA\` | \`$CACHE_KEY\` | 🔨 Needs build |" >> $GITHUB_STEP_SUMMARY + APPS_TO_BUILD=$(echo "$APPS_TO_BUILD" | jq -c --arg app "$APP_NAME" --arg sha "$CURRENT_SHA" '. + [{name: $app, sha: $sha}]') + APPS_TO_BUILD_COUNT=$((APPS_TO_BUILD_COUNT + 1)) + fi + + done < <(echo "$MATRIX" | jq -c '.[]') + + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Summary:**" >> $GITHUB_STEP_SUMMARY + echo "- Total apps checked: $APPS_CHECKED" >> $GITHUB_STEP_SUMMARY + echo "- ✅ Apps with cached builds: $APPS_CACHED" >> $GITHUB_STEP_SUMMARY + echo "- 🔨 Apps needing build: $APPS_TO_BUILD_COUNT" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ $APPS_CACHED -gt 0 ] && [ $APPS_CHECKED -gt 0 ]; then + CACHE_HIT_PERCENT=$((APPS_CACHED * 100 / APPS_CHECKED)) + echo "**Cache hit rate: ${CACHE_HIT_PERCENT}%** đŸŽ¯" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + fi + + echo "" + echo "Summary:" + echo " Total apps: $APPS_CHECKED" + echo " Cached: $APPS_CACHED" + echo " To build: $APPS_TO_BUILD_COUNT" + + # Validate that we built valid JSON + if ! echo "$APPS_TO_BUILD" | jq empty 2>/dev/null; then + echo "ERROR: Failed to build valid JSON for apps_to_build" + echo "Content: $APPS_TO_BUILD" + exit 1 + fi + + if ! echo "$APPS_TO_RESTORE" | jq empty 2>/dev/null; then + echo "ERROR: Failed to build valid JSON for apps_to_restore" + echo "Content: $APPS_TO_RESTORE" + exit 1 + fi + + # Output app list with SHAs for the build job to use + # Use proper multiline output format for GitHub Actions + echo "apps_to_build<> $GITHUB_OUTPUT + echo "$APPS_TO_BUILD" >> $GITHUB_OUTPUT + echo "APPS_TO_BUILD_JSON_EOF" >> $GITHUB_OUTPUT + + # Output the list of apps to restore from cache + echo "apps_to_restore<> $GITHUB_OUTPUT + echo "$APPS_TO_RESTORE" >> $GITHUB_OUTPUT + echo "APPS_TO_RESTORE_JSON_EOF" >> $GITHUB_OUTPUT + + # Output the SHA map for all apps + echo "apps_sha_map<> $GITHUB_OUTPUT + echo "$APPS_SHA_MAP" >> $GITHUB_OUTPUT + echo "APPS_SHA_MAP_JSON_EOF" >> $GITHUB_OUTPUT + + # Determine if there are cached apps by comparing counts + # If apps_to_build count is less than total apps, some are cached + if [ $APPS_TO_BUILD_COUNT -lt $APPS_CHECKED ]; then + echo "has_cached_apps=true" >> $GITHUB_OUTPUT + else + echo "has_cached_apps=false" >> $GITHUB_OUTPUT + fi + + echo "" + if [ $APPS_TO_BUILD_COUNT -eq 0 ]; then + echo "🎉 All apps are cached! No builds needed." + else + echo "✓ Will build $APPS_TO_BUILD_COUNT app(s)" + fi + build-external-apps: runs-on: ubuntu-latest needs: prepare-matrix + # Only run if there are apps to build + if: needs.prepare-matrix.outputs.apps_to_build != '[]' permissions: contents: read @@ -96,9 +261,38 @@ jobs: strategy: max-parallel: 20 matrix: - app: ${{ fromJson(needs.prepare-matrix.outputs.external-apps-matrix) }} + # Use the filtered list of apps that need building (not in cache) + app_info: ${{ fromJson(needs.prepare-matrix.outputs.apps_to_build) }} steps: + - name: Get app configuration from full matrix + id: app-config + run: | + # Get the full matrix to look up app configuration + FULL_MATRIX='${{ needs.prepare-matrix.outputs.external_apps_matrix }}' + APP_NAME='${{ matrix.app_info.name }}' + + # Find the app configuration in the full matrix + APP_CONFIG=$(echo "$FULL_MATRIX" | jq -c --arg name "$APP_NAME" '.[] | select(.name == $name)') + + if [ -z "$APP_CONFIG" ]; then + echo "ERROR: Could not find configuration for $APP_NAME" + exit 1 + fi + + # Extract configuration values + APP_PATH=$(echo "$APP_CONFIG" | jq -r '.path') + HAS_NPM=$(echo "$APP_CONFIG" | jq -r '.has_npm') + HAS_COMPOSER=$(echo "$APP_CONFIG" | jq -r '.has_composer') + MAKEFILE_TARGET=$(echo "$APP_CONFIG" | jq -r '.makefile_target') + + # Set outputs + echo "path=$APP_PATH" >> $GITHUB_OUTPUT + echo "has-npm=$HAS_NPM" >> $GITHUB_OUTPUT + echo "has-composer=$HAS_COMPOSER" >> $GITHUB_OUTPUT + echo "makefile-target=$MAKEFILE_TARGET" >> $GITHUB_OUTPUT + echo "Building $APP_NAME from $APP_PATH (SHA: ${{ matrix.app_info.sha }})" + - name: Checkout server uses: actions/checkout@v5 with: @@ -106,15 +300,15 @@ jobs: fetch-depth: 1 - name: Set up node with version from package.json's engines - if: matrix.app.has_npm + if: steps.app-config.outputs.has-npm == 'true' uses: actions/setup-node@v6 with: node-version-file: "package.json" cache: 'npm' - cache-dependency-path: ${{ matrix.app.path }}/package-lock.json + cache-dependency-path: ${{ steps.app-config.outputs.path }}/package-lock.json - name: Setup PHP with PECL extension - if: matrix.app.has_composer + if: steps.app-config.outputs.has-composer == 'true' uses: shivammathur/setup-php@c541c155eee45413f5b09a52248675b1a2575231 #v2.31.1 with: tools: composer:v2 @@ -122,19 +316,86 @@ jobs: env: runner: ubuntu-latest - - name: Cache Composer dependencies for ${{ matrix.app.name }} - if: matrix.app.has_composer + - name: Cache Composer dependencies for ${{ matrix.app_info.name }} + if: steps.app-config.outputs.has-composer == 'true' uses: actions/cache@v4 with: - path: ${{ matrix.app.path }}/vendor - key: ${{ runner.os }}-composer-${{ matrix.app.name }}-${{ hashFiles(format('{0}/composer.lock', matrix.app.path)) }} + path: ${{ steps.app-config.outputs.path }}/vendor + key: ${{ runner.os }}-composer-${{ matrix.app_info.name }}-${{ hashFiles(format('{0}/composer.lock', steps.app-config.outputs.path)) }} restore-keys: | - ${{ runner.os }}-composer-${{ matrix.app.name }}- + ${{ runner.os }}-composer-${{ matrix.app_info.name }}- + + - name: Build ${{ matrix.app_info.name }} app + run: make -f IONOS/Makefile ${{ steps.app-config.outputs.makefile-target }} + + - name: Report build completion + if: success() + run: | + echo "### ✅ Built ${{ matrix.app_info.name }}" >> $GITHUB_STEP_SUMMARY + echo "- **SHA:** \`${{ matrix.app_info.sha }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Path:** ${{ steps.app-config.outputs.path }}" >> $GITHUB_STEP_SUMMARY + echo "- **Status:** Success" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Save built app to cache for future runs + - name: Save build artifacts to cache + uses: actions/cache/save@v4 + with: + path: ${{ steps.app-config.outputs.path }} + key: ${{ env.CACHE_VERSION }}-app-build-${{ matrix.app_info.name }}-${{ matrix.app_info.sha }} + + - name: Upload ${{ matrix.app_info.name }} build artifacts + uses: actions/upload-artifact@v5 + with: + retention-days: 1 + name: external-app-build-${{ matrix.app_info.name }} + path: | + ${{ steps.app-config.outputs.path }} + !${{ steps.app-config.outputs.path }}/node_modules + + restore-cached-apps: + runs-on: ubuntu-latest + needs: prepare-matrix + # Only run if there are cached apps to restore + if: needs.prepare-matrix.outputs.apps_to_restore != '[]' + + permissions: + contents: read + + name: restore-cached-apps + strategy: + max-parallel: 20 + matrix: + # Use the filtered list of apps that need restoring from cache + app: ${{ fromJson(needs.prepare-matrix.outputs.apps_to_restore) }} + + steps: + - name: Restore cached build from cache + uses: actions/cache/restore@v4 + with: + path: ${{ matrix.app.path }} + key: ${{ env.CACHE_VERSION }}-app-build-${{ matrix.app.name }}-${{ matrix.app.sha }} + fail-on-cache-miss: true + + - name: Validate cached build + run: | + APP_PATH="${{ matrix.app.path }}" + + # Check that the directory exists and is not empty + if [ ! -d "$APP_PATH" ] || [ -z "$(ls -A $APP_PATH)" ]; then + echo "❌ Cache validation failed: Directory is empty or missing" + exit 1 + fi + + # Check for appinfo/info.xml (required for all Nextcloud apps) + if [ ! -f "$APP_PATH/appinfo/info.xml" ]; then + echo "❌ Cache validation failed: Missing appinfo/info.xml" + exit 1 + fi - - name: Build ${{ matrix.app.name }} app - run: make -f IONOS/Makefile ${{ matrix.app.makefile_target }} + echo "✅ Cache validation passed for ${{ matrix.app.name }}" - - name: Upload ${{ matrix.app.name }} build artifacts + - name: Upload cached ${{ matrix.app.name }} build artifacts uses: actions/upload-artifact@v5 with: retention-days: 1 @@ -145,7 +406,12 @@ jobs: build-artifact: runs-on: ubuntu-latest - needs: [prepare-matrix, build-external-apps] + needs: [prepare-matrix, build-external-apps, restore-cached-apps] + # Always run this job, even if restore-cached-apps is skipped + if: | + always() && + (needs.build-external-apps.result == 'success' || needs.build-external-apps.result == 'skipped') && + (needs.restore-cached-apps.result == 'success' || needs.restore-cached-apps.result == 'skipped') permissions: contents: read @@ -282,10 +548,15 @@ jobs: upload-to-artifactory: runs-on: self-hosted # Upload the artifact to the Artifactory repository on PR *OR* on "ionos-dev|ionos-stable" branch push defined in the on:push:branches - if: github.event_name == 'pull_request' || github.ref_name == 'ionos-dev' || github.ref_name == 'ionos-stable' + if: | + always() && + (github.event_name == 'pull_request' || github.ref_name == 'ionos-dev' || github.ref_name == 'ionos-stable') && + (needs.build-external-apps.result == 'success' || needs.build-external-apps.result == 'skipped') && + (needs.restore-cached-apps.result == 'success' || needs.restore-cached-apps.result == 'skipped') && + needs.build-artifact.result == 'success' name: Push to artifactory - needs: [prepare-matrix, build-external-apps, build-artifact] + needs: [prepare-matrix, build-external-apps, restore-cached-apps, build-artifact] outputs: ARTIFACTORY_LAST_BUILD_PATH: ${{ steps.artifactory_upload.outputs.ARTIFACTORY_LAST_BUILD_PATH }} @@ -406,13 +677,19 @@ jobs: nextcloud-workspace-artifact-to-ghcr_io: runs-on: ubuntu-latest + # Only run if build-artifact succeeded + if: | + always() && + (needs.build-external-apps.result == 'success' || needs.build-external-apps.result == 'skipped') && + (needs.restore-cached-apps.result == 'success' || needs.restore-cached-apps.result == 'skipped') && + needs.build-artifact.result == 'success' permissions: contents: read packages: write name: Push artifact to ghcr.io - needs: [prepare-matrix, build-external-apps, build-artifact] + needs: [prepare-matrix, build-external-apps, restore-cached-apps, build-artifact] steps: - name: Download artifact zip @@ -468,9 +745,13 @@ jobs: runs-on: self-hosted name: Trigger remote workflow - needs: [ build-artifact, upload-to-artifactory ] + needs: [build-artifact, upload-to-artifactory] # Trigger remote build on "ionos-dev|ionos-stable" branch *push* defined in the on:push:branches - if: github.event_name == 'push' && ( github.ref_name == 'ionos-dev' || github.ref_name == 'ionos-stable' ) + if: | + github.event_name == 'push' && + (github.ref_name == 'ionos-dev' || github.ref_name == 'ionos-stable') && + needs.build-artifact.result == 'success' && + needs.upload-to-artifactory.result == 'success' steps: - name: Trigger remote workflow run: | From ad04de67d2c39f4a8f4298a3abbeb3fd71c7a7e2 Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Wed, 12 Nov 2025 19:51:54 +0100 Subject: [PATCH 62/83] IONOS(build): ensure jobs depend on successful matrix preparation Signed-off-by: Misha M.-Kupriyanov --- .github/workflows/build-artifact.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-artifact.yml b/.github/workflows/build-artifact.yml index ff9a48391a938..e89c0fab15513 100644 --- a/.github/workflows/build-artifact.yml +++ b/.github/workflows/build-artifact.yml @@ -410,6 +410,7 @@ jobs: # Always run this job, even if restore-cached-apps is skipped if: | always() && + needs.prepare-matrix.result == 'success' && (needs.build-external-apps.result == 'success' || needs.build-external-apps.result == 'skipped') && (needs.restore-cached-apps.result == 'success' || needs.restore-cached-apps.result == 'skipped') @@ -551,6 +552,7 @@ jobs: if: | always() && (github.event_name == 'pull_request' || github.ref_name == 'ionos-dev' || github.ref_name == 'ionos-stable') && + needs.prepare-matrix.result == 'success' && (needs.build-external-apps.result == 'success' || needs.build-external-apps.result == 'skipped') && (needs.restore-cached-apps.result == 'success' || needs.restore-cached-apps.result == 'skipped') && needs.build-artifact.result == 'success' @@ -680,6 +682,7 @@ jobs: # Only run if build-artifact succeeded if: | always() && + needs.prepare-matrix.result == 'success' && (needs.build-external-apps.result == 'success' || needs.build-external-apps.result == 'skipped') && (needs.restore-cached-apps.result == 'success' || needs.restore-cached-apps.result == 'skipped') && needs.build-artifact.result == 'success' @@ -745,11 +748,12 @@ jobs: runs-on: self-hosted name: Trigger remote workflow - needs: [build-artifact, upload-to-artifactory] + needs: [prepare-matrix, build-artifact, upload-to-artifactory] # Trigger remote build on "ionos-dev|ionos-stable" branch *push* defined in the on:push:branches if: | github.event_name == 'push' && (github.ref_name == 'ionos-dev' || github.ref_name == 'ionos-stable') && + needs.prepare-matrix.result == 'success' && needs.build-artifact.result == 'success' && needs.upload-to-artifactory.result == 'success' steps: From c7ce243dce523fe0792b9733a5e1f41df2543a93 Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Thu, 13 Nov 2025 14:42:11 +0100 Subject: [PATCH 63/83] IONOS(build): add manual trigger for comparison testing in build-artifact.yml Signed-off-by: Misha M.-Kupriyanov --- .github/workflows/build-artifact.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/build-artifact.yml b/.github/workflows/build-artifact.yml index e89c0fab15513..a110ac203da71 100644 --- a/.github/workflows/build-artifact.yml +++ b/.github/workflows/build-artifact.yml @@ -31,6 +31,13 @@ on: branches: - ionos-dev - ionos-stable + workflow_dispatch: # Manual trigger only for comparison testing + inputs: + force_rebuild: + description: 'Force rebuild all apps (ignore cache)' + required: false + type: boolean + default: false concurrency: group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/ionos-dev' && github.run_id || github.event.pull_request.number || github.ref }} From 12d3b7a7ffba798ee4c23513f866f4055ee43bc2 Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Mon, 15 Dec 2025 11:47:58 +0100 Subject: [PATCH 64/83] IONOS(ci): Add app cache detection for JFrog build pipeline Implement intelligent cache detection for Nextcloud apps in the build artifact workflow. This enhancement improves build performance by detecting which apps need rebuilding based on git changes. Key features: - Detect changed apps using git diff against base branch - Add verbose logging for debugging cache behavior - Optimize performance with parallel cache operations - Remove strict PR requirement to enable testing flexibility Signed-off-by: Kai Henseler --- .github/workflows/build-artifact-original.yml | 924 ------------------ .github/workflows/build-artifact.yml | 442 +++++++-- 2 files changed, 361 insertions(+), 1005 deletions(-) delete mode 100644 .github/workflows/build-artifact-original.yml diff --git a/.github/workflows/build-artifact-original.yml b/.github/workflows/build-artifact-original.yml deleted file mode 100644 index 8d76945d5b7d4..0000000000000 --- a/.github/workflows/build-artifact-original.yml +++ /dev/null @@ -1,924 +0,0 @@ -name: Build Nextcloud Workspace artifact - -# SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors -# SPDX-FileCopyrightText: 2025 STRATO AG -# SPDX-License-Identifier: AGPL-3.0-or-later - -# The Nextcloud Workspace source is packaged as a container image. -# This is a workaround because releases cannot be created without tags, -# and we want to be able to create snapshots from branches. - -on: - pull_request: - paths: - - '.github/workflows/**' - - 'src/**' - - 'apps/**' - - 'apps/**/appinfo/info.xml' - - 'apps-external/**' - - 'IONOS' - - 'package.json' - - 'package-lock.json' - - 'themes/**' - - 'lib/**' - - 'tsconfig.json' - - '**.js' - - '**.ts' - - '**.vue' - - '.gitmodules' - push: - branches: - - ionos-dev - - ionos-stable - -concurrency: - group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/ionos-dev' && github.run_id || github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -env: - TARGET_PACKAGE_NAME: nc-workspace.zip - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - ARTIFACTORY_REPOSITORY_SNAPSHOT: ionos-productivity-ncwserver-snapshot - -permissions: - contents: read - -jobs: - prepare-matrix: - runs-on: ubuntu-latest - outputs: - external-apps-matrix: ${{ steps.set-matrix.outputs.matrix }} - steps: - - name: Checkout repository - uses: actions/checkout@v5 - with: - submodules: true - fetch-depth: '1' - - - name: Install dependencies - run: sudo apt-get update && sudo apt-get install -y make jq - - - name: Set matrix - id: set-matrix - run: | - # Create matrix configuration as a compact JSON string - matrix='[ - { - "name": "activity", - "path": "apps-external/activity", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_activity_app" - }, - { - "name": "assistant", - "path": "apps-external/assistant", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_assistant_app" - }, - { - "name": "calendar", - "path": "apps-external/calendar", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_calendar_app" - }, - { - "name": "circles", - "path": "apps-external/circles", - "has_npm": false, - "has_composer": true, - "makefile_target": "build_circles_app" - }, - { - "name": "collectives", - "path": "apps-external/collectives", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_collectives_app" - }, - { - "name": "contacts", - "path": "apps-external/contacts", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_contacts_app" - }, - { - "name": "deck", - "path": "apps-external/deck", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_deck_app" - }, - { - "name": "end_to_end_encryption", - "path": "apps-external/end_to_end_encryption", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_end_to_end_encryption_app" - }, - { - "name": "forms", - "path": "apps-external/forms", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_forms_app" - }, - { - "name": "groupfolders", - "path": "apps-external/groupfolders", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_groupfolders_app" - }, - { - "name": "integration_openai", - "path": "apps-external/integration_openai", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_integration_openai_app" - }, - { - "name": "mail", - "path": "apps-external/mail", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_mail_app" - }, - { - "name": "ncw_apps_menu", - "path": "apps-external/ncw_apps_menu", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_ncw_apps_menu_app" - }, - { - "name": "ncw_mailtemplate", - "path": "apps-external/ncw_mailtemplate", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_ncw_mailtemplate_app" - }, - { - "name": "notes", - "path": "apps-external/notes", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_notes_app" - }, - { - "name": "notifications", - "path": "apps-external/notifications", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_notifications_app" - }, - { - "name": "notify_push", - "path": "apps-external/notify_push", - "has_npm": false, - "has_composer": true, - "makefile_target": "build_notify_push_app" - }, - { - "name": "password_policy", - "path": "apps-external/password_policy", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_password_policy_app" - }, - { - "name": "richdocuments", - "path": "apps-external/richdocuments", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_richdocuments_app" - }, - { - "name": "spreed", - "path": "apps-external/spreed", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_spreed_app" - }, - { - "name": "tables", - "path": "apps-external/tables", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_tables_app" - }, - { - "name": "tasks", - "path": "apps-external/tasks", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_tasks_app" - }, - { - "name": "text", - "path": "apps-external/text", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_text_app" - }, - { - "name": "twofactor_totp", - "path": "apps-external/twofactor_totp", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_twofactor_totp_app" - }, - { - "name": "user_oidc", - "path": "apps-external/user_oidc", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_user_oidc_app" - }, - { - "name": "viewer", - "path": "apps-external/viewer", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_viewer_app" - }, - { - "name": "whiteboard", - "path": "apps-external/whiteboard", - "has_npm": true, - "has_composer": true, - "makefile_target": "build_whiteboard_app" - } - ]' - - # Validate JSON and output as compact format - if echo "$matrix" | jq empty 2>/dev/null; then - echo "matrix=$(echo "$matrix" | jq -c '.')" >> $GITHUB_OUTPUT - echo "Matrix configuration set successfully" - else - echo "Error: Invalid JSON in matrix configuration" - exit 1 - fi - - - name: Validate matrix against Makefile - run: | - set +e # Intentionally allow script to continue on error for custom error handling and reporting to GITHUB_STEP_SUMMARY - set -u # Exit on undefined variable - - echo "### 🔍 Matrix Validation" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Debug: Check if apps-external exists - echo "Checking apps-external directory..." - if [ ! -d "apps-external" ]; then - echo "❌ **Error:** apps-external directory does not exist!" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Directory listing:" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - ls -la >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - echo "Apps-external directory exists. Listing contents:" - ls -la apps-external/ | head -10 - - # Check if jq is available - echo "Checking if jq is installed..." - if ! command -v jq &> /dev/null; then - echo "❌ **Error:** jq is not installed!" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "jq is required for matrix generation but was not found in PATH." >> $GITHUB_STEP_SUMMARY - exit 1 - fi - echo "jq version: $(jq --version)" - - echo "Generating matrix from Makefile..." - # Capture both stdout and stderr separately to better diagnose issues - makefile_output=$(make -f IONOS/Makefile generate_external_apps_matrix_json 2>&1) - makefile_exit_code=$? - - echo "Makefile exit code: ${makefile_exit_code}" - echo "Makefile output length: ${#makefile_output}" - - # Debug: Check if GITHUB_STEP_SUMMARY is set - echo "GITHUB_STEP_SUMMARY: ${GITHUB_STEP_SUMMARY:-NOT SET}" - - # If the Makefile command failed, show the error - if [ ${makefile_exit_code} -ne 0 ]; then - echo "" - echo "=== MAKEFILE ERROR ===" - echo "Exit code: ${makefile_exit_code}" - echo "Output:" - echo "$makefile_output" - echo "=====================" - echo "" - - # Write to summary - echo "❌ **Error:** Makefile command failed with exit code ${makefile_exit_code}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "Makefile error output" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "$makefile_output" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - - echo "Error written to summary file: ${GITHUB_STEP_SUMMARY}" - exit 1 - fi - - # Filter out the info message to get just the JSON - # The Makefile outputs "[i] Generating..." to stderr, but we captured everything with 2>&1 - # So we need to extract just the JSON part - generated_matrix=$(echo "$makefile_output" | grep -v '^\[i\]' || echo "$makefile_output") - - workflow_matrix='${{ steps.set-matrix.outputs.matrix }}' - - # Debug output - echo "Generated matrix length: ${#generated_matrix}" - echo "Workflow matrix length: ${#workflow_matrix}" - - # Show first 200 chars of generated matrix for debugging - if [ -n "$generated_matrix" ]; then - echo "Generated matrix preview: ${generated_matrix:0:200}..." - fi - - # Validate that we got valid JSON - if ! echo "$generated_matrix" | jq empty 2>/dev/null; then - echo "❌ **Error:** Generated matrix is not valid JSON" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "Invalid JSON output" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "$generated_matrix" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - # Validate that we got data - if [ -z "$generated_matrix" ] || [ -z "$workflow_matrix" ]; then - echo "❌ **Error:** Failed to load matrices" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "- Generated matrix empty: $([ -z "$generated_matrix" ] && echo "yes" || echo "no")" >> $GITHUB_STEP_SUMMARY - echo "- Workflow matrix empty: $([ -z "$workflow_matrix" ] && echo "yes" || echo "no")" >> $GITHUB_STEP_SUMMARY - echo "- Makefile exit code: ${makefile_exit_code}" >> $GITHUB_STEP_SUMMARY - - exit 1 - fi - - # Sort both matrices for comparison - generated_sorted=$(echo "$generated_matrix" | jq -S '.' 2>&1 || echo "ERROR") - workflow_sorted=$(echo "$workflow_matrix" | jq -S '.' 2>&1 || echo "ERROR") - - echo "Sorted matrix lengths - generated: ${#generated_sorted}, workflow: ${#workflow_sorted}" - - # Compare the two matrices - if [ "$generated_sorted" = "$workflow_sorted" ]; then - echo "✅ **Validation passed!** The workflow matrix matches the Makefile configuration." >> $GITHUB_STEP_SUMMARY - echo "" - echo "✅ Matrix validation passed!" - else - echo "❌ **Validation failed!** The workflow matrix does not match the Makefile configuration." >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - echo "Starting detailed comparison..." - - # Extract app names from both matrices - generated_apps=$(echo "$generated_matrix" | jq -r '.[].name' 2>/dev/null | sort || echo "") - workflow_apps=$(echo "$workflow_matrix" | jq -r '.[].name' 2>/dev/null | sort || echo "") - - echo "Generated apps count: $(echo "$generated_apps" | wc -l)" - echo "Workflow apps count: $(echo "$workflow_apps" | wc -l)" - - # Find missing apps (in Makefile but not in workflow) - missing_apps=$(comm -23 <(echo "$generated_apps") <(echo "$workflow_apps")) - if [ $? -ne 0 ]; then - echo "Error: comm command failed when finding missing apps." >&2 - exit 1 - fi - # Find extra apps (in workflow but not in Makefile) - extra_apps=$(comm -13 <(echo "$generated_apps") <(echo "$workflow_apps")) - if [ $? -ne 0 ]; then - echo "Error: comm command failed when finding extra apps." >&2 - exit 1 - fi - - echo "Missing apps: ${missing_apps:-none}" - echo "Extra apps: ${extra_apps:-none}" - - if [ -n "$missing_apps" ]; then - echo "#### âš ī¸ Missing Apps" >> $GITHUB_STEP_SUMMARY - echo "The following apps are configured in the Makefile but missing from the workflow:" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "$missing_apps" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - fi - - if [ -n "$extra_apps" ]; then - echo "#### âš ī¸ Extra Apps" >> $GITHUB_STEP_SUMMARY - echo "The following apps are in the workflow but not configured in the Makefile:" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "$extra_apps" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - fi - - # Check for configuration mismatches in common apps - common_apps=$(comm -12 <(echo "$generated_apps") <(echo "$workflow_apps") 2>/dev/null || echo "") - - echo "Common apps count: $(echo "$common_apps" | wc -l)" - - if [ -n "$common_apps" ]; then - mismatched_apps="" - - while IFS= read -r app; do - [ -z "$app" ] && continue - gen_config=$(echo "$generated_matrix" | jq -c --arg app "$app" '.[] | select(.name == $app)' 2>/dev/null || echo "") - wf_config=$(echo "$workflow_matrix" | jq -c --arg app "$app" '.[] | select(.name == $app)' 2>/dev/null || echo "") - - if [ -n "$gen_config" ] && [ -n "$wf_config" ] && [ "$gen_config" != "$wf_config" ]; then - mismatched_apps="${mismatched_apps}${app}"$'\n' - fi - done <<< "$common_apps" - - echo "Mismatched apps: ${mismatched_apps:-none}" - - if [ -n "$mismatched_apps" ]; then - echo "#### âš ī¸ Configuration Mismatches" >> $GITHUB_STEP_SUMMARY - echo "The following apps have different configurations (has_npm, has_composer, etc.):" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "$mismatched_apps" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "📋 Detailed differences" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo '```diff' >> $GITHUB_STEP_SUMMARY - - while IFS= read -r app; do - [ -z "$app" ] && continue - echo "=== $app ===" >> $GITHUB_STEP_SUMMARY - diff -u --label "Workflow" --label "Makefile" \ - <(echo "$workflow_matrix" | jq --arg app "$app" '.[] | select(.name == $app)' 2>/dev/null || echo "{}") \ - <(echo "$generated_matrix" | jq --arg app "$app" '.[] | select(.name == $app)' 2>/dev/null || echo "{}") \ - >> $GITHUB_STEP_SUMMARY 2>&1 || true - done <<< "$mismatched_apps" - - echo '```' >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - fi - fi - - # Provide fix instructions - echo "#### 🔧 How to Fix" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Run this command locally to generate the correct matrix:" >> $GITHUB_STEP_SUMMARY - echo '```bash' >> $GITHUB_STEP_SUMMARY - echo "make -f IONOS/Makefile generate_external_apps_matrix_json" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Then update the \`matrix\` variable in \`.github/workflows/build-artifact.yml\` in the set-matrix step with the generated output." >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Show full diff in expandable section - echo "
" >> $GITHUB_STEP_SUMMARY - echo "📄 Full matrix comparison" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Workflow Matrix:**" >> $GITHUB_STEP_SUMMARY - echo '```json' >> $GITHUB_STEP_SUMMARY - echo "$workflow_matrix" | jq '.' 2>/dev/null >> $GITHUB_STEP_SUMMARY || echo "$workflow_matrix" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Makefile Matrix:**" >> $GITHUB_STEP_SUMMARY - echo '```json' >> $GITHUB_STEP_SUMMARY - echo "$generated_matrix" | jq '.' 2>/dev/null >> $GITHUB_STEP_SUMMARY || echo "$generated_matrix" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - - echo "" - echo "❌ ERROR: Matrix validation failed!" - echo "See the job summary for details on what's wrong and how to fix it." - echo "Summary file size: $(wc -c < $GITHUB_STEP_SUMMARY || echo 0) bytes" - exit 1 - fi - - build-external-apps: - runs-on: ubuntu-latest - needs: prepare-matrix - - permissions: - contents: read - - name: build-external-apps - strategy: - max-parallel: 20 - matrix: - app: ${{ fromJson(needs.prepare-matrix.outputs.external-apps-matrix) }} - - steps: - - name: Checkout server - uses: actions/checkout@v5 - with: - submodules: true - fetch-depth: '1' - - - name: Set up node with version from package.json's engines - if: matrix.app.has_npm - uses: actions/setup-node@v5 - with: - node-version-file: "package.json" - cache: 'npm' - cache-dependency-path: ${{ matrix.app.path }}/package-lock.json - - - name: Setup PHP with PECL extension - if: matrix.app.has_composer - uses: shivammathur/setup-php@c541c155eee45413f5b09a52248675b1a2575231 #v2.31.1 - with: - tools: composer:v2 - extensions: gd, zip, curl, xml, xmlrpc, mbstring, sqlite, xdebug, pgsql, intl, imagick, gmp, apcu, bcmath, redis, soap, imap, opcache - env: - runner: ubuntu-latest - - - name: Cache Composer dependencies for ${{ matrix.app.name }} - if: matrix.app.has_composer - uses: actions/cache@v4 - with: - path: ${{ matrix.app.path }}/vendor - key: ${{ runner.os }}-composer-${{ matrix.app.name }}-${{ hashFiles(format('{0}/composer.lock', matrix.app.path)) }} - restore-keys: | - ${{ runner.os }}-composer-${{ matrix.app.name }}- - - - name: Build ${{ matrix.app.name }} app - run: make -f IONOS/Makefile ${{ matrix.app.makefile_target }} - - - name: Upload ${{ matrix.app.name }} build artifacts - uses: actions/upload-artifact@v4 - with: - retention-days: 1 - name: external-app-build-${{ matrix.app.name }} - path: | - ${{ matrix.app.path }} - !${{ matrix.app.path }}/node_modules - - build-artifact: - runs-on: ubuntu-latest - needs: [prepare-matrix, build-external-apps] - - permissions: - contents: read - - outputs: - NC_VERSION: ${{ steps.get_nc_version.outputs.NC_VERSION }} - - name: build-artifact - steps: - - name: Checkout server - uses: actions/checkout@v5 - with: - submodules: true - fetch-depth: '1' - - - name: Download build external apps - uses: actions/download-artifact@v5 - with: - pattern: external-app-build-* - path: apps-external/ - - - name: Reorganize downloaded apps-external artifacts - run: | - cd apps-external/ - - echo "Initial structure:" - ls -la - - # Move contents from external-app-build-* directories to their target directories - for artifact_dir in external-app-build-*; do - if [ -d "$artifact_dir" ]; then - # Extract app name from artifact directory name - app_name=${artifact_dir#external-app-build-} - - echo "Processing artifact: $artifact_dir -> $app_name" - - # If target directory exists, merge the contents from the artifact directory containing build artifacts - if [ -d "$app_name" ]; then - echo "Target directory $app_name exists, merging contents from $artifact_dir" - # Copy contents from artifact directory to target directory - cp -r "$artifact_dir"/* "$app_name"/ - # Remove the now-empty artifact directory - rm -rf "$artifact_dir" - else - # Move the artifact directory to the proper app name - echo "Moving $artifact_dir to $app_name" - mv "$artifact_dir" "$app_name" - fi - fi - done - - echo "Reorganization complete. Final structure:" - ls -la - - - name: Verify downloaded artifacts structure - run: | - echo "External apps structure:" - ls -la apps-external/ - for app_dir in apps-external/*/; do - if [ -d "$app_dir" ]; then - echo "Contents of $app_dir:" - ls -la "$app_dir" - fi - done - - - name: Set up node with version from package.json's engines - uses: actions/setup-node@v5 - with: - node-version-file: "package.json" - cache: 'npm' - - - name: Install Dependencies - run: sudo apt-get update && sudo apt-get install -y make zip unzip - - - name: Print dependencies versions - run: make --version && node --version && npm --version - - - name: Setup PHP with PECL extension - uses: shivammathur/setup-php@c541c155eee45413f5b09a52248675b1a2575231 #v2.31.1 - with: - tools: composer:v2 - extensions: gd, zip, curl, xml, xmlrpc, mbstring, sqlite, xdebug, pgsql, intl, imagick, gmp, apcu, bcmath, redis, soap, imap, opcache - env: - runner: ubuntu-latest - - - name: Cache Composer dependencies - uses: actions/cache@v4 - with: - path: vendor - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-composer- - - - name: Print PHP install - run: php -i && php -m - - - name: Build Nextcloud - run: make -f IONOS/Makefile build_ncw - - - name: Add config partials - run: make -f IONOS/Makefile add_config_partials - - - name: Zip dependencies - run: make -f IONOS/Makefile zip_dependencies TARGET_PACKAGE_NAME=${{ env.TARGET_PACKAGE_NAME }} - - - name: Get NC version - id: get_nc_version - continue-on-error: false - run: | - NC_VERSION=$(jq -r '.ncVersion' version.json) - echo "NC_VERSION: $NC_VERSION" - - if [ -z "$NC_VERSION" ]; then - echo "NC_VERSION is empty" - exit 1 - fi - - echo "NC_VERSION=$NC_VERSION" >> $GITHUB_OUTPUT - - - name: Upload artifact result for job build-artifact - uses: actions/upload-artifact@v4 - with: - retention-days: 30 - name: nextcloud_workspace_build_artifact - path: ${{ env.TARGET_PACKAGE_NAME }} - - - name: Show changes on failure - if: failure() - run: | - git status - git --no-pager diff - exit 1 # make it red to grab attention - - upload-to-artifactory: - runs-on: self-hosted - # Upload the artifact to the Artifactory repository on PR *OR* on "ionos-dev|ionos-stable" branch push defined in the on:push:branches - if: github.event_name == 'pull_request' || github.ref_name == 'ionos-dev' || github.ref_name == 'ionos-stable' - - name: Push to artifactory - needs: [prepare-matrix, build-external-apps, build-artifact] - - outputs: - ARTIFACTORY_LAST_BUILD_PATH: ${{ steps.artifactory_upload.outputs.ARTIFACTORY_LAST_BUILD_PATH }} - - env: - BUILD_NAME: "nextcloud-workspace-snapshot" - - steps: - - name: Check prerequisites - run: | - # count the number of secrets that are set - echo "Checking if required secrets are set..." - error_count=0 - - if [ -z "${{ secrets.JF_ARTIFACTORY_URL }}" ]; then - # output error to github actions log - echo "::error::JF_ARTIFACTORY_URL secret is not set" - error_count=$((error_count + 1)) - fi - - if [ -z "${{ secrets.JF_ARTIFACTORY_USER }}" ]; then - echo "::error::JF_ARTIFACTORY_USER secret is not set" - error_count=$((error_count + 1)) - fi - - if [ -z "${{ secrets.JF_ACCESS_TOKEN }}" ]; then - echo "::error::JF_ACCESS_TOKEN secret is not set" - error_count=$((error_count + 1)) - fi - - # abort if any of the required secrets are not set - if [ $error_count -ne 0 ]; then - echo "::error::Required secrets are not set. Aborting." - exit 1 - fi - - - name: Download artifact zip - uses: actions/download-artifact@v5 - with: - name: nextcloud_workspace_build_artifact - - # This action sets up the JFrog CLI with the Artifactory URL and access token - - uses: jfrog/setup-jfrog-cli@7c95feb32008765e1b4e626b078dfd897c4340ad # v4.4.1 - env: - JF_URL: ${{ secrets.JF_ARTIFACTORY_URL }} - JF_USER: ${{ secrets.JF_ARTIFACTORY_USER }} - JF_ACCESS_TOKEN: ${{ secrets.JF_ACCESS_TOKEN }} - - - name: Ping the JF server - run: | - # Ping the server - jf rt ping - - - name: Upload build to artifactory - id: artifactory_upload - run: | - # PR builds are stored in a separate directory as "dev/pr/nextcloud-workspace-pr-.zip" - # Push to "ionos-dev" branch is stored as "dev/nextcloud-workspace-.zip" - - ARTIFACTORY_STAGE_PREFIX="dev" - - # set ARTIFACTORY_STAGE_PREFIX=stable on ionos-stable branch - if [ "${{ github.ref_name }}" == "ionos-stable" ]; then - ARTIFACTORY_STAGE_PREFIX="stable" - fi - - export PATH_TO_DIRECTORY="${{ env.ARTIFACTORY_REPOSITORY_SNAPSHOT }}/${ARTIFACTORY_STAGE_PREFIX}" - PATH_TO_FILE="pr/nextcloud-workspace-pr-${{ github.event.pull_request.number }}-original.zip" - - if [ -z "${{ github.event.pull_request.number }}" ]; then - PATH_TO_FILE="nextcloud-workspace-${{ needs.build-artifact.outputs.NC_VERSION }}-original.zip" - fi - - export PATH_TO_LATEST_ARTIFACT="${PATH_TO_DIRECTORY}/${PATH_TO_FILE}" - - # Promote current build to the "latest" dev build - jf rt upload "${{ env.TARGET_PACKAGE_NAME }}" \ - --build-name "${{ env.BUILD_NAME }}" \ - --build-number ${{ github.run_number }} \ - --target-props "build.nc_version=${{ needs.build-artifact.outputs.NC_VERSION }};vcs.branch=${{ github.ref }};vcs.revision=${{ github.sha }}" \ - $PATH_TO_LATEST_ARTIFACT - - echo "ARTIFACTORY_LAST_BUILD_PATH=${PATH_TO_LATEST_ARTIFACT}" >> $GITHUB_OUTPUT - - - name: Show changes on failure - if: failure() - run: | - git status - git --no-pager diff - exit 1 # make it red to grab attention - - nextcloud-workspace-artifact-to-ghcr_io: - runs-on: ubuntu-latest - - permissions: - contents: read - packages: write - - name: Push artifact to ghcr.io - needs: [prepare-matrix, build-external-apps, build-artifact] - - steps: - - name: Download artifact zip - uses: actions/download-artifact@v5 - with: - name: nextcloud_workspace_build_artifact - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" - - - name: Create Dockerfile - run: | - cat >Dockerfile << EOF - FROM busybox as builder - COPY ./${{ env.TARGET_PACKAGE_NAME }} / - WORKDIR /builder - RUN unzip /${{ env.TARGET_PACKAGE_NAME }} -d /builder - - FROM scratch - WORKDIR /app - VOLUME /app - COPY --from=builder /builder /app - EOF - - - name: Build and push Docker image - uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 - with: - context: . - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - - name: Show changes on failure - if: failure() - run: | - echo "Git status:" - git status - echo "Git diff:" - git diff - exit 1 # make it red to grab attention - - trigger-remote-dev-worflow: - runs-on: self-hosted - - name: Trigger remote workflow - needs: [ build-artifact, upload-to-artifactory ] - # Trigger remote build on "ionos-dev|ionos-stable" branch *push* defined in the on:push:branches - if: github.event_name == 'push' && ( github.ref_name == 'ionos-dev' || github.ref_name == 'ionos-stable' ) - steps: - - name: Trigger remote workflow - run: | - # Enable command echo for debugging purposes - set -x - - # Determine build type based on branch: - # - 'ionos-dev' branch triggers 'dev' build type - # - 'ionos-stable' branch triggers 'stable' build type - BUILD_TYPE="dev" - - # Override build type for stable branch - if [ "${{ github.ref_name }}" == "ionos-stable" ]; then - BUILD_TYPE="stable" - fi - - # Trigger GitLab pipeline via webhook with build artifacts and metadata - # Passes GitHub context variables to remote GitLab workflow - curl \ - --silent \ - --insecure \ - --request POST \ - --fail-with-body \ - -o response.json \ - --form token=${{ secrets.GITLAB_TOKEN }} \ - --form ref="stable" \ - --form "variables[GITHUB_SHA]=${{ github.sha }}" \ - --form "variables[ARTIFACTORY_LAST_BUILD_PATH]=${{ needs.upload-to-artifactory.outputs.ARTIFACTORY_LAST_BUILD_PATH }}" \ - --form "variables[NC_VERSION]=${{ needs.build-artifact.outputs.NC_VERSION }}" \ - --form "variables[BUILD_ID]=${{ github.run_id }}" \ - --form "variables[BUILD_TYPE]=${BUILD_TYPE}" \ - "${{ secrets.GITLAB_TRIGGER_URL }}" || ( RETCODE="$?"; jq . response.json; exit "$RETCODE" ) - - # Disable command echo - set +x - - # Print and parse json - # jq . response.json - echo "json<> $GITHUB_OUTPUT - cat response.json >> $GITHUB_OUTPUT - echo "END" >> $GITHUB_OUTPUT - echo "web_url<> $GITHUB_OUTPUT - cat response.json | jq --raw-output '.web_url' >> $GITHUB_OUTPUT - echo "END" >> $GITHUB_OUTPUT - - - name: Show changes on failure - if: failure() - run: | - git status - git --no-pager diff - exit 1 # make it red to grab attention diff --git a/.github/workflows/build-artifact.yml b/.github/workflows/build-artifact.yml index a110ac203da71..47c91c8f4e12c 100644 --- a/.github/workflows/build-artifact.yml +++ b/.github/workflows/build-artifact.yml @@ -31,10 +31,10 @@ on: branches: - ionos-dev - ionos-stable - workflow_dispatch: # Manual trigger only for comparison testing + workflow_dispatch: # Manual trigger to bypass all caches inputs: force_rebuild: - description: 'Force rebuild all apps (ignore cache)' + description: 'Force rebuild all apps and dependencies (bypass ALL caches)' required: false type: boolean default: false @@ -63,8 +63,9 @@ jobs: apps_to_build: ${{ steps.detect.outputs.apps_to_build }} apps_to_restore: ${{ steps.detect.outputs.apps_to_restore }} external_apps_matrix: ${{ steps.set_matrix.outputs.matrix }} - has_cached_apps: ${{ steps.detect.outputs.has_cached_apps }} apps_sha_map: ${{ steps.detect.outputs.apps_sha_map }} + has_apps_to_build: ${{ steps.detect.outputs.has_apps_to_build }} + has_apps_to_restore: ${{ steps.detect.outputs.has_apps_to_restore }} steps: - name: Checkout repository uses: actions/checkout@v5 @@ -75,6 +76,43 @@ jobs: - name: Install dependencies run: sudo apt-get update && sudo apt-get install -y make jq + - name: Check configuration + run: | + echo "" + echo "### 🔧 Remote Trigger Configuration" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**DISABLE_REMOTE_TRIGGER value:** \`${{ vars.DISABLE_REMOTE_TRIGGER }}\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "🔧 Remote Trigger Configuration" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "DISABLE_REMOTE_TRIGGER = '${{ vars.DISABLE_REMOTE_TRIGGER }}'" + echo "" + + if [ "${{ vars.DISABLE_REMOTE_TRIGGER }}" == "true" ]; then + echo "âš ī¸ Remote trigger is DISABLED" + echo " The 'trigger-remote-dev-worflow' job will be SKIPPED" + echo "**Status:** âš ī¸ Remote trigger is **DISABLED**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "The \`trigger-remote-dev-worflow\` job will be skipped." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "To enable, delete the variable or set it to a value other than 'true' at:" >> $GITHUB_STEP_SUMMARY + echo "https://github.com/${{ github.repository }}/settings/variables/actions" >> $GITHUB_STEP_SUMMARY + else + echo "✅ Remote trigger is ENABLED" + echo " The 'trigger-remote-dev-worflow' job will proceed" + echo "**Status:** ✅ Remote trigger is **ENABLED**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "The \`trigger-remote-dev-worflow\` job will proceed to trigger the remote GitLab workflow." >> $GITHUB_STEP_SUMMARY + fi + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + + - name: List caches before restore + run: gh cache list + env: + GH_TOKEN: ${{ github.token }} + - name: Generate apps matrix dynamically from Makefile id: set_matrix run: | @@ -106,12 +144,57 @@ jobs: env: GH_TOKEN: ${{ github.token }} CACHE_VERSION: ${{ env.CACHE_VERSION }} + FORCE_REBUILD: ${{ github.event.inputs.force_rebuild || 'false' }} + JF_URL: ${{ secrets.JF_ARTIFACTORY_URL }} + JF_USER: ${{ secrets.JF_ARTIFACTORY_USER }} + JF_ACCESS_TOKEN: ${{ secrets.JF_ACCESS_TOKEN }} + ARTIFACTORY_REPOSITORY_SNAPSHOT: ${{ env.ARTIFACTORY_REPOSITORY_SNAPSHOT }} run: | set -e # Exit on error set -u # Exit on undefined variable set -o pipefail # Exit if any command in pipeline fails echo "Collecting app SHAs and checking cache status..." + echo "Force rebuild mode: $FORCE_REBUILD" + echo "" + + # Setup JFrog CLI if credentials are available + JFROG_AVAILABLE="false" + echo "=== JFrog Setup ===" + echo "JF_URL present: $([ -n "$JF_URL" ] && echo 'YES' || echo 'NO')" + echo "JF_USER present: $([ -n "$JF_USER" ] && echo 'YES' || echo 'NO')" + echo "JF_ACCESS_TOKEN present: $([ -n "$JF_ACCESS_TOKEN" ] && echo 'YES' || echo 'NO')" + + if [ -n "$JF_URL" ] && [ -n "$JF_USER" ] && [ -n "$JF_ACCESS_TOKEN" ]; then + echo "✓ All JFrog credentials available" + echo "Installing JFrog CLI..." + # Install JFrog CLI + curl -fL https://install-cli.jfrog.io | sh + export PATH=$PATH:$PWD + echo "JFrog CLI version: $(jf --version)" + + # Configure JFrog + echo "Configuring JFrog server: $JF_URL" + jf config add jfrog-server --url="$JF_URL" --user="$JF_USER" --access-token="$JF_ACCESS_TOKEN" --interactive=false + + # Test connection with verbose output + echo "Testing JFrog connection..." + if jf rt ping; then + JFROG_AVAILABLE="true" + echo "✓ JFrog connection successful" + echo "Repository: $ARTIFACTORY_REPOSITORY_SNAPSHOT" + else + echo "⚠ JFrog ping failed, will fall back to GitHub cache" + echo "Ping output was unsuccessful" + fi + else + echo "⚠ JFrog credentials not available, using GitHub cache only" + [ -z "$JF_URL" ] && echo " - Missing: JF_URL" + [ -z "$JF_USER" ] && echo " - Missing: JF_USER" + [ -z "$JF_ACCESS_TOKEN" ] && echo " - Missing: JF_ACCESS_TOKEN" + fi + echo "JFROG_AVAILABLE=$JFROG_AVAILABLE" + echo "===================" echo "" # Get the matrix from previous step @@ -119,15 +202,27 @@ jobs: # Build JSON array for apps that actually need building APPS_TO_BUILD="[]" - # Build JSON array for apps that are cached and need restoring + # Build JSON array for apps that are cached/in JFrog and need restoring + # This combines both GitHub cache and JFrog sources APPS_TO_RESTORE="[]" APPS_CHECKED=0 APPS_CACHED=0 + APPS_IN_JFROG=0 APPS_TO_BUILD_COUNT=0 + APPS_TO_RESTORE_COUNT=0 APPS_SHA_MAP="{}" + echo "" - echo "### đŸ“Ļ Cache Status Report" >> $GITHUB_STEP_SUMMARY + echo "### đŸ“Ļ Cache Status Report for (${{ github.ref }})" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY + if [ "$FORCE_REBUILD" == "true" ]; then + echo "**🔄 FORCE REBUILD MODE ENABLED** - All caches bypassed" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + fi + if [ "$JFROG_AVAILABLE" == "true" ]; then + echo "**đŸŽ¯ JFrog Artifact Cache**: Enabled for all branches" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + fi echo "| App | SHA | Cache Key | Status |" >> $GITHUB_STEP_SUMMARY echo "|-----|-----|-----------|--------|" >> $GITHUB_STEP_SUMMARY @@ -167,9 +262,57 @@ jobs: echo -n " Checking $APP_NAME (SHA: $SHORT_SHA)... " + # If force rebuild is enabled, skip cache check and rebuild everything + if [ "$FORCE_REBUILD" == "true" ]; then + echo "🔄 force rebuild" + echo "| $APP_NAME | \`$SHORT_SHA\` | \`$CACHE_KEY\` | 🔄 Force rebuild |" >> $GITHUB_STEP_SUMMARY + APPS_TO_BUILD=$(echo "$APPS_TO_BUILD" | jq -c --arg app "$APP_NAME" --arg sha "$CURRENT_SHA" '. + [{name: $app, sha: $sha}]') + APPS_TO_BUILD_COUNT=$((APPS_TO_BUILD_COUNT + 1)) + continue + fi + + # Check JFrog first before GitHub cache (available for all branches) + if [ "$JFROG_AVAILABLE" == "true" ]; then + JFROG_PATH="${ARTIFACTORY_REPOSITORY_SNAPSHOT}/dev-poc/apps/${APP_NAME}/${APP_NAME}-${CURRENT_SHA}.tar.gz" + + echo "" + echo " 🔍 Checking JFrog for $APP_NAME..." + echo " Path: $JFROG_PATH" + echo " Full SHA: $CURRENT_SHA" + + # Check if artifact exists in JFrog with verbose output + echo " Running: jf rt s \"$JFROG_PATH\"" + SEARCH_OUTPUT=$(jf rt s "$JFROG_PATH" 2>&1) + SEARCH_EXIT_CODE=$? + + echo " Search exit code: $SEARCH_EXIT_CODE" + if [ $SEARCH_EXIT_CODE -eq 0 ]; then + echo " Search output:" + echo "$SEARCH_OUTPUT" | sed 's/^/ /' + + if echo "$SEARCH_OUTPUT" | grep -q "$JFROG_PATH"; then + echo " ✓ Artifact found in JFrog!" + echo "✓ in JFrog" + echo "| $APP_NAME | \`$SHORT_SHA\` | \`$JFROG_PATH\` | đŸ“Ļ In JFrog |" >> $GITHUB_STEP_SUMMARY + APPS_IN_JFROG=$((APPS_IN_JFROG + 1)) + APPS_TO_RESTORE_COUNT=$((APPS_TO_RESTORE_COUNT + 1)) + # Add to restore list with JFrog source + APPS_TO_RESTORE=$(echo "$APPS_TO_RESTORE" | jq -c --argjson app "$app_json" --arg sha "$CURRENT_SHA" --arg jfrog_path "$JFROG_PATH" --arg source "jfrog" '. + [($app + {sha: $sha, jfrog_path: $jfrog_path, source: $source})]') + continue + else + echo " ✗ Artifact not found in search results" + fi + else + echo " ✗ Search failed with error:" + echo "$SEARCH_OUTPUT" | sed 's/^/ /' + fi + echo " → Falling back to GitHub cache check" + fi + # Check if cache exists using GitHub CLI + # Include --ref to access caches from the current ref (branch, PR, etc.) CACHE_EXISTS="false" - if ! CACHE_LIST=$(gh cache list --key "$CACHE_KEY" --json key --jq ".[].key" 2>&1); then + if ! CACHE_LIST=$(gh cache list --ref "${{ github.ref }}" --key "$CACHE_KEY" --json key --jq ".[].key" 2>&1); then echo "âš ī¸ Warning: Failed to query cache for $APP_NAME: $CACHE_LIST" echo "| $APP_NAME | \`$SHORT_SHA\` | \`$CACHE_KEY\` | âš ī¸ Cache check failed - will build |" >> $GITHUB_STEP_SUMMARY APPS_TO_BUILD=$(echo "$APPS_TO_BUILD" | jq -c --arg app "$APP_NAME" --arg sha "$CURRENT_SHA" '. + [{name: $app, sha: $sha}]') @@ -179,10 +322,11 @@ jobs: if echo "$CACHE_LIST" | grep -q "^${CACHE_KEY}$"; then CACHE_EXISTS="true" APPS_CACHED=$((APPS_CACHED + 1)) + APPS_TO_RESTORE_COUNT=$((APPS_TO_RESTORE_COUNT + 1)) echo "✓ cached" echo "| $APP_NAME | \`$SHORT_SHA\` | \`$CACHE_KEY\` | ✅ Cached |" >> $GITHUB_STEP_SUMMARY - # Add to restore list with SHA - APPS_TO_RESTORE=$(echo "$APPS_TO_RESTORE" | jq -c --argjson app "$app_json" --arg sha "$CURRENT_SHA" '. + [($app + {sha: $sha})]') + # Add to restore list with GitHub cache source + APPS_TO_RESTORE=$(echo "$APPS_TO_RESTORE" | jq -c --argjson app "$app_json" --arg sha "$CURRENT_SHA" --arg cache_key "$CACHE_KEY" --arg source "github-cache" '. + [($app + {sha: $sha, cache_key: $cache_key, source: $source})]') else echo "⚡ needs build" echo "| $APP_NAME | \`$SHORT_SHA\` | \`$CACHE_KEY\` | 🔨 Needs build |" >> $GITHUB_STEP_SUMMARY @@ -195,12 +339,14 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "**Summary:**" >> $GITHUB_STEP_SUMMARY echo "- Total apps checked: $APPS_CHECKED" >> $GITHUB_STEP_SUMMARY + echo "- đŸ“Ļ Apps in JFrog: $APPS_IN_JFROG" >> $GITHUB_STEP_SUMMARY echo "- ✅ Apps with cached builds: $APPS_CACHED" >> $GITHUB_STEP_SUMMARY echo "- 🔨 Apps needing build: $APPS_TO_BUILD_COUNT" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - if [ $APPS_CACHED -gt 0 ] && [ $APPS_CHECKED -gt 0 ]; then - CACHE_HIT_PERCENT=$((APPS_CACHED * 100 / APPS_CHECKED)) + TOTAL_AVAILABLE=$((APPS_IN_JFROG + APPS_CACHED)) + if [ $TOTAL_AVAILABLE -gt 0 ] && [ $APPS_CHECKED -gt 0 ]; then + CACHE_HIT_PERCENT=$((TOTAL_AVAILABLE * 100 / APPS_CHECKED)) echo "**Cache hit rate: ${CACHE_HIT_PERCENT}%** đŸŽ¯" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY fi @@ -208,9 +354,21 @@ jobs: echo "" echo "Summary:" echo " Total apps: $APPS_CHECKED" + echo " In JFrog: $APPS_IN_JFROG" echo " Cached: $APPS_CACHED" echo " To build: $APPS_TO_BUILD_COUNT" + # Validate no duplicate apps in build and restore lists + BUILD_APPS=$(echo "$APPS_TO_BUILD" | jq -r '.[].name' | sort) + RESTORE_APPS=$(echo "$APPS_TO_RESTORE" | jq -r '.[].name' | sort) + DUPLICATE_APPS=$(comm -12 <(echo "$BUILD_APPS") <(echo "$RESTORE_APPS")) + + if [ -n "$DUPLICATE_APPS" ]; then + echo "ERROR: Apps appear in both build and restore lists:" + echo "$DUPLICATE_APPS" + exit 1 + fi + # Validate that we built valid JSON if ! echo "$APPS_TO_BUILD" | jq empty 2>/dev/null; then echo "ERROR: Failed to build valid JSON for apps_to_build" @@ -230,7 +388,7 @@ jobs: echo "$APPS_TO_BUILD" >> $GITHUB_OUTPUT echo "APPS_TO_BUILD_JSON_EOF" >> $GITHUB_OUTPUT - # Output the list of apps to restore from cache + # Output the unified list of apps to restore (from either GitHub cache or JFrog) echo "apps_to_restore<> $GITHUB_OUTPUT echo "$APPS_TO_RESTORE" >> $GITHUB_OUTPUT echo "APPS_TO_RESTORE_JSON_EOF" >> $GITHUB_OUTPUT @@ -240,12 +398,17 @@ jobs: echo "$APPS_SHA_MAP" >> $GITHUB_OUTPUT echo "APPS_SHA_MAP_JSON_EOF" >> $GITHUB_OUTPUT - # Determine if there are cached apps by comparing counts - # If apps_to_build count is less than total apps, some are cached - if [ $APPS_TO_BUILD_COUNT -lt $APPS_CHECKED ]; then - echo "has_cached_apps=true" >> $GITHUB_OUTPUT + # Output flags for conditional job execution + if [ $APPS_TO_BUILD_COUNT -gt 0 ]; then + echo "has_apps_to_build=true" >> $GITHUB_OUTPUT else - echo "has_cached_apps=false" >> $GITHUB_OUTPUT + echo "has_apps_to_build=false" >> $GITHUB_OUTPUT + fi + + if [ $APPS_TO_RESTORE_COUNT -gt 0 ]; then + echo "has_apps_to_restore=true" >> $GITHUB_OUTPUT + else + echo "has_apps_to_restore=false" >> $GITHUB_OUTPUT fi echo "" @@ -258,8 +421,8 @@ jobs: build-external-apps: runs-on: ubuntu-latest needs: prepare-matrix - # Only run if there are apps to build - if: needs.prepare-matrix.outputs.apps_to_build != '[]' + # Only run if there are actually apps to build (prevents runner from starting) + if: needs.prepare-matrix.outputs.has_apps_to_build == 'true' permissions: contents: read @@ -318,13 +481,14 @@ jobs: if: steps.app-config.outputs.has-composer == 'true' uses: shivammathur/setup-php@c541c155eee45413f5b09a52248675b1a2575231 #v2.31.1 with: + php-version: '8.3' tools: composer:v2 extensions: gd, zip, curl, xml, xmlrpc, mbstring, sqlite, xdebug, pgsql, intl, imagick, gmp, apcu, bcmath, redis, soap, imap, opcache env: runner: ubuntu-latest - name: Cache Composer dependencies for ${{ matrix.app_info.name }} - if: steps.app-config.outputs.has-composer == 'true' + if: steps.app-config.outputs.has-composer == 'true' && github.event.inputs.force_rebuild != 'true' uses: actions/cache@v4 with: path: ${{ steps.app-config.outputs.path }}/vendor @@ -351,75 +515,103 @@ jobs: path: ${{ steps.app-config.outputs.path }} key: ${{ env.CACHE_VERSION }}-app-build-${{ matrix.app_info.name }}-${{ matrix.app_info.sha }} - - name: Upload ${{ matrix.app_info.name }} build artifacts - uses: actions/upload-artifact@v5 - with: - retention-days: 1 - name: external-app-build-${{ matrix.app_info.name }} - path: | - ${{ steps.app-config.outputs.path }} - !${{ steps.app-config.outputs.path }}/node_modules - - restore-cached-apps: - runs-on: ubuntu-latest - needs: prepare-matrix - # Only run if there are cached apps to restore - if: needs.prepare-matrix.outputs.apps_to_restore != '[]' - - permissions: - contents: read - - name: restore-cached-apps - strategy: - max-parallel: 20 - matrix: - # Use the filtered list of apps that need restoring from cache - app: ${{ fromJson(needs.prepare-matrix.outputs.apps_to_restore) }} - - steps: - - name: Restore cached build from cache - uses: actions/cache/restore@v4 - with: - path: ${{ matrix.app.path }} - key: ${{ env.CACHE_VERSION }}-app-build-${{ matrix.app.name }}-${{ matrix.app.sha }} - fail-on-cache-miss: true + # Push to JFrog for ionos-dev branch builds + - name: Setup JFrog CLI + uses: jfrog/setup-jfrog-cli@7c95feb32008765e1b4e626b078dfd897c4340ad # v4.4.1 + env: + JF_URL: ${{ secrets.JF_ARTIFACTORY_URL }} + JF_USER: ${{ secrets.JF_ARTIFACTORY_USER }} + JF_ACCESS_TOKEN: ${{ secrets.JF_ACCESS_TOKEN }} - - name: Validate cached build + - name: Push ${{ matrix.app_info.name }} to JFrog run: | - APP_PATH="${{ matrix.app.path }}" - - # Check that the directory exists and is not empty - if [ ! -d "$APP_PATH" ] || [ -z "$(ls -A $APP_PATH)" ]; then - echo "❌ Cache validation failed: Directory is empty or missing" + set -e + APP_NAME="${{ matrix.app_info.name }}" + APP_SHA="${{ matrix.app_info.sha }}" + APP_PATH="${{ steps.app-config.outputs.path }}" + + echo "=== JFrog Upload Debug Info ===" + echo "đŸ“Ļ Packaging $APP_NAME for JFrog upload..." + echo "App Name: $APP_NAME" + echo "App SHA: $APP_SHA" + echo "App Path: $APP_PATH" + echo "Repository: ${{ env.ARTIFACTORY_REPOSITORY_SNAPSHOT }}" + echo "===============================" + + # Verify app path exists + if [ ! -d "$APP_PATH" ]; then + echo "❌ ERROR: App path does not exist: $APP_PATH" exit 1 fi + + echo "App directory contents (top level):" + ls -la "$APP_PATH" | head -20 - # Check for appinfo/info.xml (required for all Nextcloud apps) - if [ ! -f "$APP_PATH/appinfo/info.xml" ]; then - echo "❌ Cache validation failed: Missing appinfo/info.xml" - exit 1 + # Create tar.gz archive of the built app (excluding node_modules and other build artifacts) + ARCHIVE_NAME="${APP_NAME}-${APP_SHA}.tar.gz" + echo "" + echo "Creating archive: $ARCHIVE_NAME" + echo "Running: tar -czf \"$ARCHIVE_NAME\" --exclude=\"node_modules\" --exclude=\".git\" --exclude=\"*.log\" -C \"$(dirname "$APP_PATH")\" \"$(basename "$APP_PATH")\"" + + tar -czf "$ARCHIVE_NAME" \ + --exclude="node_modules" \ + --exclude=".git" \ + --exclude="*.log" \ + -C "$(dirname "$APP_PATH")" \ + "$(basename "$APP_PATH")" + + echo "✓ Archive created successfully" + echo "Archive size:" + ls -lh "$ARCHIVE_NAME" + + # Upload to JFrog - store in snapshot repo under dev/apps/ + JFROG_PATH="${{ env.ARTIFACTORY_REPOSITORY_SNAPSHOT }}/dev-poc/apps/${APP_NAME}/${ARCHIVE_NAME}" + + echo "" + echo "Uploading to JFrog..." + echo "Target Path: $JFROG_PATH" + echo "Properties: app.name=${APP_NAME};app.sha=${APP_SHA};vcs.branch=${{ github.ref_name }};vcs.revision=${{ github.sha }}" + echo "Running: jf rt upload \"$ARCHIVE_NAME\" \"$JFROG_PATH\" --target-props \"...\"" + + if jf rt upload "$ARCHIVE_NAME" "$JFROG_PATH" \ + --target-props "app.name=${APP_NAME};app.sha=${APP_SHA};vcs.branch=${{ github.ref_name }};vcs.revision=${{ github.sha }}"; then + echo "✅ Successfully uploaded $APP_NAME to JFrog" + echo "" + echo "Verifying upload..." + if jf rt s "$JFROG_PATH"; then + echo "✓ Upload verified - artifact is accessible in JFrog" + else + echo "⚠ Upload succeeded but verification search failed" + fi + else + UPLOAD_EXIT_CODE=$? + echo "❌ Failed to upload to JFrog (exit code: $UPLOAD_EXIT_CODE)" + echo "âš ī¸ Continuing workflow despite upload failure..." fi - echo "✅ Cache validation passed for ${{ matrix.app.name }}" + # Clean up archive + echo "" + echo "Cleaning up local archive..." + rm -f "$ARCHIVE_NAME" + echo "✓ Cleanup complete" - - name: Upload cached ${{ matrix.app.name }} build artifacts - uses: actions/upload-artifact@v5 + - name: Upload ${{ matrix.app_info.name }} build artifacts + uses: actions/upload-artifact@v4 with: retention-days: 1 - name: external-app-build-${{ matrix.app.name }} + name: external-app-build-${{ matrix.app_info.name }} path: | - ${{ matrix.app.path }} - !${{ matrix.app.path }}/node_modules + ${{ steps.app-config.outputs.path }} + !${{ steps.app-config.outputs.path }}/node_modules build-artifact: runs-on: ubuntu-latest - needs: [prepare-matrix, build-external-apps, restore-cached-apps] - # Always run this job, even if restore-cached-apps is skipped + needs: [prepare-matrix, build-external-apps] + # Always run this job, even if build-external-apps is skipped if: | always() && needs.prepare-matrix.result == 'success' && - (needs.build-external-apps.result == 'success' || needs.build-external-apps.result == 'skipped') && - (needs.restore-cached-apps.result == 'success' || needs.restore-cached-apps.result == 'skipped') + (needs.build-external-apps.result == 'success' || needs.build-external-apps.result == 'skipped') permissions: contents: read @@ -435,8 +627,93 @@ jobs: submodules: true fetch-depth: 1 + - name: Setup JFrog CLI + if: needs.prepare-matrix.outputs.has_apps_to_restore == 'true' + uses: jfrog/setup-jfrog-cli@7c95feb32008765e1b4e626b078dfd897c4340ad # v4.4.1 + env: + JF_URL: ${{ secrets.JF_ARTIFACTORY_URL }} + JF_USER: ${{ secrets.JF_ARTIFACTORY_USER }} + JF_ACCESS_TOKEN: ${{ secrets.JF_ACCESS_TOKEN }} + + - name: Restore cached apps + if: needs.prepare-matrix.outputs.has_apps_to_restore == 'true' + run: | + set -e + + echo "đŸ“Ļ Restoring cached apps..." + APPS_TO_RESTORE='${{ needs.prepare-matrix.outputs.apps_to_restore }}' + + # Process each app in the restore list + echo "$APPS_TO_RESTORE" | jq -c '.[]' | while read -r app_json; do + APP_NAME=$(echo "$app_json" | jq -r '.name') + APP_SHA=$(echo "$app_json" | jq -r '.sha') + APP_PATH=$(echo "$app_json" | jq -r '.path') + SOURCE=$(echo "$app_json" | jq -r '.source') + + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "Restoring: $APP_NAME (source: $SOURCE)" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + + if [ "$SOURCE" == "jfrog" ]; then + # Restore from JFrog + JFROG_PATH=$(echo "$app_json" | jq -r '.jfrog_path') + ARCHIVE_NAME="${APP_NAME}-${APP_SHA}.tar.gz" + + echo "đŸ“Ĩ Downloading from JFrog: $JFROG_PATH" + + if jf rt download "$JFROG_PATH" "$ARCHIVE_NAME" --flat=true; then + echo "✅ Downloaded successfully" + echo "Extracting to $APP_PATH..." + mkdir -p "$(dirname "$APP_PATH")" + tar -xzf "$ARCHIVE_NAME" -C "$(dirname "$APP_PATH")" + + if [ -d "$APP_PATH" ] && [ -f "$APP_PATH/appinfo/info.xml" ]; then + echo "✅ Restored $APP_NAME from JFrog" + else + echo "❌ Failed to extract or validate $APP_NAME" + exit 1 + fi + + rm -f "$ARCHIVE_NAME" + else + echo "❌ Failed to download from JFrog" + exit 1 + fi + + elif [ "$SOURCE" == "github-cache" ]; then + # Restore from GitHub cache + CACHE_KEY=$(echo "$app_json" | jq -r '.cache_key') + + echo "💾 Restoring from GitHub cache: $CACHE_KEY" + + # Use actions/cache/restore in a way that works in a script context + # We need to use gh CLI to restore the cache + if gh cache restore "$CACHE_KEY" --key "$CACHE_KEY"; then + echo "✅ Restored $APP_NAME from GitHub cache" + + # Validate restoration + if [ ! -d "$APP_PATH" ] || [ ! -f "$APP_PATH/appinfo/info.xml" ]; then + echo "❌ Validation failed for $APP_NAME" + exit 1 + fi + else + echo "❌ Failed to restore from GitHub cache" + exit 1 + fi + else + echo "❌ Unknown source: $SOURCE" + exit 1 + fi + done + + echo "" + echo "✅ All cached apps restored successfully" + env: + GH_TOKEN: ${{ github.token }} + - name: Download build external apps - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v5 with: pattern: external-app-build-* path: apps-external/ @@ -489,7 +766,7 @@ jobs: uses: actions/setup-node@v6 with: node-version-file: "package.json" - cache: 'npm' + cache: ${{ github.event.inputs.force_rebuild != 'true' && 'npm' || '' }} - name: Install Dependencies run: sudo apt-get update && sudo apt-get install -y make zip unzip @@ -500,12 +777,14 @@ jobs: - name: Setup PHP with PECL extension uses: shivammathur/setup-php@c541c155eee45413f5b09a52248675b1a2575231 #v2.31.1 with: + php-version: '8.3' tools: composer:v2 extensions: gd, zip, curl, xml, xmlrpc, mbstring, sqlite, xdebug, pgsql, intl, imagick, gmp, apcu, bcmath, redis, soap, imap, opcache env: runner: ubuntu-latest - name: Cache Composer dependencies + if: github.event.inputs.force_rebuild != 'true' uses: actions/cache@v4 with: path: vendor @@ -540,7 +819,7 @@ jobs: echo "NC_VERSION=$NC_VERSION" >> $GITHUB_OUTPUT - name: Upload artifact result for job build-artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v4 with: retention-days: 30 name: nextcloud_workspace_build_artifact @@ -561,11 +840,10 @@ jobs: (github.event_name == 'pull_request' || github.ref_name == 'ionos-dev' || github.ref_name == 'ionos-stable') && needs.prepare-matrix.result == 'success' && (needs.build-external-apps.result == 'success' || needs.build-external-apps.result == 'skipped') && - (needs.restore-cached-apps.result == 'success' || needs.restore-cached-apps.result == 'skipped') && needs.build-artifact.result == 'success' name: Push to artifactory - needs: [prepare-matrix, build-external-apps, restore-cached-apps, build-artifact] + needs: [prepare-matrix, build-external-apps, build-artifact] outputs: ARTIFACTORY_LAST_BUILD_PATH: ${{ steps.artifactory_upload.outputs.ARTIFACTORY_LAST_BUILD_PATH }} @@ -603,7 +881,7 @@ jobs: fi - name: Download artifact zip - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v5 with: name: nextcloud_workspace_build_artifact @@ -691,7 +969,6 @@ jobs: always() && needs.prepare-matrix.result == 'success' && (needs.build-external-apps.result == 'success' || needs.build-external-apps.result == 'skipped') && - (needs.restore-cached-apps.result == 'success' || needs.restore-cached-apps.result == 'skipped') && needs.build-artifact.result == 'success' permissions: @@ -699,11 +976,11 @@ jobs: packages: write name: Push artifact to ghcr.io - needs: [prepare-matrix, build-external-apps, restore-cached-apps, build-artifact] + needs: [prepare-matrix, build-external-apps, build-artifact] steps: - name: Download artifact zip - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v5 with: name: nextcloud_workspace_build_artifact @@ -757,12 +1034,15 @@ jobs: name: Trigger remote workflow needs: [prepare-matrix, build-artifact, upload-to-artifactory] # Trigger remote build on "ionos-dev|ionos-stable" branch *push* defined in the on:push:branches + # Can be disabled via repository variable 'DISABLE_REMOTE_TRIGGER' (set to 'true' to disable) + # Configure at: https://github.com/IONOS-Productivity/ncw-server/settings/variables/actions if: | github.event_name == 'push' && (github.ref_name == 'ionos-dev' || github.ref_name == 'ionos-stable') && needs.prepare-matrix.result == 'success' && needs.build-artifact.result == 'success' && - needs.upload-to-artifactory.result == 'success' + needs.upload-to-artifactory.result == 'success' && + vars.DISABLE_REMOTE_TRIGGER != 'true' steps: - name: Trigger remote workflow run: | From 53765ad235f87453363b155d9852ffa5b3ce0509 Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Mon, 15 Dec 2025 11:38:54 +0100 Subject: [PATCH 65/83] IONOS(ci): Refactor cache detection logic into separate script Extract cache detection logic from workflow YAML into a dedicated script file for better maintainability and testability. Changes: - Create `.github/scripts/detect-app-cache.sh` with all detection logic - Simplify workflow file by calling the external script - Improve code readability and separation of concerns - Enable easier testing and iteration of cache detection logic Signed-off-by: Kai Henseler --- .github/scripts/detect-app-cache.sh | 296 +++++++++++++++++++++++++++ .github/workflows/build-artifact.yml | 269 +----------------------- 2 files changed, 298 insertions(+), 267 deletions(-) create mode 100644 .github/scripts/detect-app-cache.sh diff --git a/.github/scripts/detect-app-cache.sh b/.github/scripts/detect-app-cache.sh new file mode 100644 index 0000000000000..27f880fc4463e --- /dev/null +++ b/.github/scripts/detect-app-cache.sh @@ -0,0 +1,296 @@ +#!/bin/bash + +# SPDX-FileCopyrightText: 2025 STRATO AG +# SPDX-License-Identifier: AGPL-3.0-or-later + +# Script to detect which apps need building vs. can be restored from cache +# Supports multiple cache sources: GitHub Actions cache and JFrog Artifactory +# Outputs JSON arrays for apps to build and apps to restore + +set -e # Exit on error +set -u # Exit on undefined variable +set -o pipefail # Exit if any command in pipeline fails + +# Required environment variables +: "${GH_TOKEN:?GH_TOKEN not set}" +: "${CACHE_VERSION:?CACHE_VERSION not set}" +: "${FORCE_REBUILD:?FORCE_REBUILD not set}" +: "${ARTIFACTORY_REPOSITORY_SNAPSHOT:?ARTIFACTORY_REPOSITORY_SNAPSHOT not set}" + +# Optional JFrog variables +JF_URL="${JF_URL:-}" +JF_USER="${JF_USER:-}" +JF_ACCESS_TOKEN="${JF_ACCESS_TOKEN:-}" + +# Input: MATRIX (JSON array of app configurations) +# Input: GITHUB_REF (current GitHub ref) +# Input: GITHUB_STEP_SUMMARY (path to step summary file) + +# Outputs to $GITHUB_OUTPUT: +# - apps_to_build: JSON array of apps that need building +# - apps_to_restore: JSON array of apps that can be restored from cache +# - apps_sha_map: JSON object mapping app names to their SHAs +# - has_apps_to_build: boolean flag +# - has_apps_to_restore: boolean flag + +echo "Collecting app SHAs and checking cache status..." +echo "Force rebuild mode: $FORCE_REBUILD" +echo "" + +# Setup JFrog CLI if credentials are available +JFROG_AVAILABLE="false" +echo "=== JFrog Setup ===" +echo "JF_URL present: $([ -n "$JF_URL" ] && echo 'YES' || echo 'NO')" +echo "JF_USER present: $([ -n "$JF_USER" ] && echo 'YES' || echo 'NO')" +echo "JF_ACCESS_TOKEN present: $([ -n "$JF_ACCESS_TOKEN" ] && echo 'YES' || echo 'NO')" + +if [ -n "$JF_URL" ] && [ -n "$JF_USER" ] && [ -n "$JF_ACCESS_TOKEN" ]; then + echo "✓ All JFrog credentials available" + echo "Installing JFrog CLI..." + # Install JFrog CLI + curl -fL https://install-cli.jfrog.io | sh + export PATH=$PATH:$PWD + echo "JFrog CLI version: $(jf --version)" + + # Configure JFrog + echo "Configuring JFrog server: $JF_URL" + jf config add jfrog-server --url="$JF_URL" --user="$JF_USER" --access-token="$JF_ACCESS_TOKEN" --interactive=false + + # Test connection with verbose output + echo "Testing JFrog connection..." + if jf rt ping; then + JFROG_AVAILABLE="true" + echo "✓ JFrog connection successful" + echo "Repository: $ARTIFACTORY_REPOSITORY_SNAPSHOT" + else + echo "⚠ JFrog ping failed, will fall back to GitHub cache" + echo "Ping output was unsuccessful" + fi +else + echo "⚠ JFrog credentials not available, using GitHub cache only" + [ -z "$JF_URL" ] && echo " - Missing: JF_URL" + [ -z "$JF_USER" ] && echo " - Missing: JF_USER" + [ -z "$JF_ACCESS_TOKEN" ] && echo " - Missing: JF_ACCESS_TOKEN" +fi +echo "JFROG_AVAILABLE=$JFROG_AVAILABLE" +echo "===================" +echo "" + +# Get the matrix from input (passed as argument) +MATRIX="$1" + +# Build JSON arrays for apps that need building/restoring +APPS_TO_BUILD="[]" +APPS_TO_RESTORE="[]" +APPS_CHECKED=0 +APPS_CACHED=0 +APPS_IN_JFROG=0 +APPS_TO_BUILD_COUNT=0 +APPS_TO_RESTORE_COUNT=0 +APPS_SHA_MAP="{}" +echo "" + +echo "### đŸ“Ļ Cache Status Report for ($GITHUB_REF)" >> "$GITHUB_STEP_SUMMARY" +echo "" >> "$GITHUB_STEP_SUMMARY" +if [ "$FORCE_REBUILD" == "true" ]; then + echo "**🔄 FORCE REBUILD MODE ENABLED** - All caches bypassed" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" +fi +if [ "$JFROG_AVAILABLE" == "true" ]; then + echo "**đŸŽ¯ JFrog Artifact Cache**: Enabled for all branches" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" +fi +echo "| App | SHA | Cache Key | Status |" >> "$GITHUB_STEP_SUMMARY" +echo "|-----|-----|-----------|--------|" >> "$GITHUB_STEP_SUMMARY" + +# Iterate through each app in the matrix +while IFS= read -r app_json; do + APP_NAME=$(echo "$app_json" | jq -r '.name') + APP_PATH=$(echo "$app_json" | jq -r '.path') + + APPS_CHECKED=$((APPS_CHECKED + 1)) + + # Get current submodule SHA + if [ -d "$APP_PATH" ]; then + CURRENT_SHA=$(git -C "$APP_PATH" rev-parse HEAD 2>/dev/null || echo "") + else + echo "⊘ $APP_NAME - directory not found, will build" + echo "| $APP_NAME | N/A | N/A | ⊘ Directory not found |" >> "$GITHUB_STEP_SUMMARY" + APPS_TO_BUILD=$(echo "$APPS_TO_BUILD" | jq -c --arg app "$APP_NAME" --arg sha "unknown" '. + [{name: $app, sha: $sha}]') + APPS_TO_BUILD_COUNT=$((APPS_TO_BUILD_COUNT + 1)) + continue + fi + + if [ -z "$CURRENT_SHA" ]; then + echo "⊘ $APP_NAME - not a git repo, will build" + echo "| $APP_NAME | N/A | N/A | ⊘ Not a git repo |" >> "$GITHUB_STEP_SUMMARY" + APPS_TO_BUILD=$(echo "$APPS_TO_BUILD" | jq -c --arg app "$APP_NAME" --arg sha "unknown" '. + [{name: $app, sha: $sha}]') + APPS_TO_BUILD_COUNT=$((APPS_TO_BUILD_COUNT + 1)) + continue + fi + + # Add SHA to the map for all apps (regardless of cache status) + APPS_SHA_MAP=$(echo "$APPS_SHA_MAP" | jq -c --arg app "$APP_NAME" --arg sha "$CURRENT_SHA" '.[$app] = $sha') + + # Cache key that would be used for this app + # Format: -app-build-- + CACHE_KEY="${CACHE_VERSION}-app-build-${APP_NAME}-${CURRENT_SHA}" + SHORT_SHA="${CURRENT_SHA:0:8}" + + echo -n " Checking $APP_NAME (SHA: $SHORT_SHA)... " + + # If force rebuild is enabled, skip cache check and rebuild everything + if [ "$FORCE_REBUILD" == "true" ]; then + echo "🔄 force rebuild" + echo "| $APP_NAME | \`$SHORT_SHA\` | \`$CACHE_KEY\` | 🔄 Force rebuild |" >> "$GITHUB_STEP_SUMMARY" + APPS_TO_BUILD=$(echo "$APPS_TO_BUILD" | jq -c --arg app "$APP_NAME" --arg sha "$CURRENT_SHA" '. + [{name: $app, sha: $sha}]') + APPS_TO_BUILD_COUNT=$((APPS_TO_BUILD_COUNT + 1)) + continue + fi + + # Check JFrog first before GitHub cache (available for all branches) + if [ "$JFROG_AVAILABLE" == "true" ]; then + JFROG_PATH="${ARTIFACTORY_REPOSITORY_SNAPSHOT}/dev-poc/apps/${APP_NAME}/${APP_NAME}-${CURRENT_SHA}.tar.gz" + + echo "" + echo " 🔍 Checking JFrog for $APP_NAME..." + echo " Path: $JFROG_PATH" + echo " Full SHA: $CURRENT_SHA" + + # Check if artifact exists in JFrog with verbose output + echo " Running: jf rt s \"$JFROG_PATH\"" + SEARCH_OUTPUT=$(jf rt s "$JFROG_PATH" 2>&1) + SEARCH_EXIT_CODE=$? + + echo " Search exit code: $SEARCH_EXIT_CODE" + if [ $SEARCH_EXIT_CODE -eq 0 ]; then + echo " Search output:" + echo "$SEARCH_OUTPUT" | sed 's/^/ /' + + if echo "$SEARCH_OUTPUT" | grep -q "$JFROG_PATH"; then + echo " ✓ Artifact found in JFrog!" + echo "✓ in JFrog" + echo "| $APP_NAME | \`$SHORT_SHA\` | \`$JFROG_PATH\` | đŸ“Ļ In JFrog |" >> "$GITHUB_STEP_SUMMARY" + APPS_IN_JFROG=$((APPS_IN_JFROG + 1)) + APPS_TO_RESTORE_COUNT=$((APPS_TO_RESTORE_COUNT + 1)) + # Add to restore list with JFrog source + APPS_TO_RESTORE=$(echo "$APPS_TO_RESTORE" | jq -c --argjson app "$app_json" --arg sha "$CURRENT_SHA" --arg jfrog_path "$JFROG_PATH" --arg source "jfrog" '. + [($app + {sha: $sha, jfrog_path: $jfrog_path, source: $source})]') + continue + else + echo " ✗ Artifact not found in search results" + fi + else + echo " ✗ Search failed with error:" + echo "$SEARCH_OUTPUT" | sed 's/^/ /' + fi + echo " → Falling back to GitHub cache check" + fi + + # Check if cache exists using GitHub CLI + # Include --ref to access caches from the current ref (branch, PR, etc.) + CACHE_EXISTS="false" + if ! CACHE_LIST=$(gh cache list --ref "$GITHUB_REF" --key "$CACHE_KEY" --json key --jq ".[].key" 2>&1); then + echo "âš ī¸ Warning: Failed to query cache for $APP_NAME: $CACHE_LIST" + echo "| $APP_NAME | \`$SHORT_SHA\` | \`$CACHE_KEY\` | âš ī¸ Cache check failed - will build |" >> "$GITHUB_STEP_SUMMARY" + APPS_TO_BUILD=$(echo "$APPS_TO_BUILD" | jq -c --arg app "$APP_NAME" --arg sha "$CURRENT_SHA" '. + [{name: $app, sha: $sha}]') + APPS_TO_BUILD_COUNT=$((APPS_TO_BUILD_COUNT + 1)) + continue + fi + if echo "$CACHE_LIST" | grep -q "^${CACHE_KEY}$"; then + CACHE_EXISTS="true" + APPS_CACHED=$((APPS_CACHED + 1)) + APPS_TO_RESTORE_COUNT=$((APPS_TO_RESTORE_COUNT + 1)) + echo "✓ cached" + echo "| $APP_NAME | \`$SHORT_SHA\` | \`$CACHE_KEY\` | ✅ Cached |" >> "$GITHUB_STEP_SUMMARY" + # Add to restore list with GitHub cache source + APPS_TO_RESTORE=$(echo "$APPS_TO_RESTORE" | jq -c --argjson app "$app_json" --arg sha "$CURRENT_SHA" --arg cache_key "$CACHE_KEY" --arg source "github-cache" '. + [($app + {sha: $sha, cache_key: $cache_key, source: $source})]') + else + echo "⚡ needs build" + echo "| $APP_NAME | \`$SHORT_SHA\` | \`$CACHE_KEY\` | 🔨 Needs build |" >> "$GITHUB_STEP_SUMMARY" + APPS_TO_BUILD=$(echo "$APPS_TO_BUILD" | jq -c --arg app "$APP_NAME" --arg sha "$CURRENT_SHA" '. + [{name: $app, sha: $sha}]') + APPS_TO_BUILD_COUNT=$((APPS_TO_BUILD_COUNT + 1)) + fi + +done < <(echo "$MATRIX" | jq -c '.[]') + +echo "" >> "$GITHUB_STEP_SUMMARY" +echo "**Summary:**" >> "$GITHUB_STEP_SUMMARY" +echo "- Total apps checked: $APPS_CHECKED" >> "$GITHUB_STEP_SUMMARY" +echo "- đŸ“Ļ Apps in JFrog: $APPS_IN_JFROG" >> "$GITHUB_STEP_SUMMARY" +echo "- ✅ Apps with cached builds: $APPS_CACHED" >> "$GITHUB_STEP_SUMMARY" +echo "- 🔨 Apps needing build: $APPS_TO_BUILD_COUNT" >> "$GITHUB_STEP_SUMMARY" +echo "" >> "$GITHUB_STEP_SUMMARY" + +TOTAL_AVAILABLE=$((APPS_IN_JFROG + APPS_CACHED)) +if [ $TOTAL_AVAILABLE -gt 0 ] && [ $APPS_CHECKED -gt 0 ]; then + CACHE_HIT_PERCENT=$((TOTAL_AVAILABLE * 100 / APPS_CHECKED)) + echo "**Cache hit rate: ${CACHE_HIT_PERCENT}%** đŸŽ¯" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" +fi + +echo "" +echo "Summary:" +echo " Total apps: $APPS_CHECKED" +echo " In JFrog: $APPS_IN_JFROG" +echo " Cached: $APPS_CACHED" +echo " To build: $APPS_TO_BUILD_COUNT" + +# Validate no duplicate apps in build and restore lists +BUILD_APPS=$(echo "$APPS_TO_BUILD" | jq -r '.[].name' | sort) +RESTORE_APPS=$(echo "$APPS_TO_RESTORE" | jq -r '.[].name' | sort) +DUPLICATE_APPS=$(comm -12 <(echo "$BUILD_APPS") <(echo "$RESTORE_APPS")) + +if [ -n "$DUPLICATE_APPS" ]; then + echo "ERROR: Apps appear in both build and restore lists:" + echo "$DUPLICATE_APPS" + exit 1 +fi + +# Validate that we built valid JSON +if ! echo "$APPS_TO_BUILD" | jq empty 2>/dev/null; then + echo "ERROR: Failed to build valid JSON for apps_to_build" + echo "Content: $APPS_TO_BUILD" + exit 1 +fi + +if ! echo "$APPS_TO_RESTORE" | jq empty 2>/dev/null; then + echo "ERROR: Failed to build valid JSON for apps_to_restore" + echo "Content: $APPS_TO_RESTORE" + exit 1 +fi + +# Output app list with SHAs for the build job to use +# Use proper multiline output format for GitHub Actions +echo "apps_to_build<> "$GITHUB_OUTPUT" +echo "$APPS_TO_BUILD" >> "$GITHUB_OUTPUT" +echo "APPS_TO_BUILD_JSON_EOF" >> "$GITHUB_OUTPUT" + +# Output the unified list of apps to restore (from either GitHub cache or JFrog) +echo "apps_to_restore<> "$GITHUB_OUTPUT" +echo "$APPS_TO_RESTORE" >> "$GITHUB_OUTPUT" +echo "APPS_TO_RESTORE_JSON_EOF" >> "$GITHUB_OUTPUT" + +# Output the SHA map for all apps +echo "apps_sha_map<> "$GITHUB_OUTPUT" +echo "$APPS_SHA_MAP" >> "$GITHUB_OUTPUT" +echo "APPS_SHA_MAP_JSON_EOF" >> "$GITHUB_OUTPUT" + +# Output flags for conditional job execution +if [ $APPS_TO_BUILD_COUNT -gt 0 ]; then + echo "has_apps_to_build=true" >> "$GITHUB_OUTPUT" +else + echo "has_apps_to_build=false" >> "$GITHUB_OUTPUT" +fi + +if [ $APPS_TO_RESTORE_COUNT -gt 0 ]; then + echo "has_apps_to_restore=true" >> "$GITHUB_OUTPUT" +else + echo "has_apps_to_restore=false" >> "$GITHUB_OUTPUT" +fi + +echo "" +if [ $APPS_TO_BUILD_COUNT -eq 0 ]; then + echo "🎉 All apps are cached! No builds needed." +else + echo "✓ Will build $APPS_TO_BUILD_COUNT app(s)" +fi diff --git a/.github/workflows/build-artifact.yml b/.github/workflows/build-artifact.yml index 47c91c8f4e12c..a3314e69d0bd2 100644 --- a/.github/workflows/build-artifact.yml +++ b/.github/workflows/build-artifact.yml @@ -149,274 +149,9 @@ jobs: JF_USER: ${{ secrets.JF_ARTIFACTORY_USER }} JF_ACCESS_TOKEN: ${{ secrets.JF_ACCESS_TOKEN }} ARTIFACTORY_REPOSITORY_SNAPSHOT: ${{ env.ARTIFACTORY_REPOSITORY_SNAPSHOT }} + GITHUB_REF: ${{ github.ref }} run: | - set -e # Exit on error - set -u # Exit on undefined variable - set -o pipefail # Exit if any command in pipeline fails - - echo "Collecting app SHAs and checking cache status..." - echo "Force rebuild mode: $FORCE_REBUILD" - echo "" - - # Setup JFrog CLI if credentials are available - JFROG_AVAILABLE="false" - echo "=== JFrog Setup ===" - echo "JF_URL present: $([ -n "$JF_URL" ] && echo 'YES' || echo 'NO')" - echo "JF_USER present: $([ -n "$JF_USER" ] && echo 'YES' || echo 'NO')" - echo "JF_ACCESS_TOKEN present: $([ -n "$JF_ACCESS_TOKEN" ] && echo 'YES' || echo 'NO')" - - if [ -n "$JF_URL" ] && [ -n "$JF_USER" ] && [ -n "$JF_ACCESS_TOKEN" ]; then - echo "✓ All JFrog credentials available" - echo "Installing JFrog CLI..." - # Install JFrog CLI - curl -fL https://install-cli.jfrog.io | sh - export PATH=$PATH:$PWD - echo "JFrog CLI version: $(jf --version)" - - # Configure JFrog - echo "Configuring JFrog server: $JF_URL" - jf config add jfrog-server --url="$JF_URL" --user="$JF_USER" --access-token="$JF_ACCESS_TOKEN" --interactive=false - - # Test connection with verbose output - echo "Testing JFrog connection..." - if jf rt ping; then - JFROG_AVAILABLE="true" - echo "✓ JFrog connection successful" - echo "Repository: $ARTIFACTORY_REPOSITORY_SNAPSHOT" - else - echo "⚠ JFrog ping failed, will fall back to GitHub cache" - echo "Ping output was unsuccessful" - fi - else - echo "⚠ JFrog credentials not available, using GitHub cache only" - [ -z "$JF_URL" ] && echo " - Missing: JF_URL" - [ -z "$JF_USER" ] && echo " - Missing: JF_USER" - [ -z "$JF_ACCESS_TOKEN" ] && echo " - Missing: JF_ACCESS_TOKEN" - fi - echo "JFROG_AVAILABLE=$JFROG_AVAILABLE" - echo "===================" - echo "" - - # Get the matrix from previous step - MATRIX='${{ steps.set_matrix.outputs.matrix }}' - - # Build JSON array for apps that actually need building - APPS_TO_BUILD="[]" - # Build JSON array for apps that are cached/in JFrog and need restoring - # This combines both GitHub cache and JFrog sources - APPS_TO_RESTORE="[]" - APPS_CHECKED=0 - APPS_CACHED=0 - APPS_IN_JFROG=0 - APPS_TO_BUILD_COUNT=0 - APPS_TO_RESTORE_COUNT=0 - APPS_SHA_MAP="{}" - echo "" - - echo "### đŸ“Ļ Cache Status Report for (${{ github.ref }})" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - if [ "$FORCE_REBUILD" == "true" ]; then - echo "**🔄 FORCE REBUILD MODE ENABLED** - All caches bypassed" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - fi - if [ "$JFROG_AVAILABLE" == "true" ]; then - echo "**đŸŽ¯ JFrog Artifact Cache**: Enabled for all branches" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - fi - echo "| App | SHA | Cache Key | Status |" >> $GITHUB_STEP_SUMMARY - echo "|-----|-----|-----------|--------|" >> $GITHUB_STEP_SUMMARY - - # Iterate through each app in the matrix - while IFS= read -r app_json; do - APP_NAME=$(echo "$app_json" | jq -r '.name') - APP_PATH=$(echo "$app_json" | jq -r '.path') - - APPS_CHECKED=$((APPS_CHECKED + 1)) - - # Get current submodule SHA - if [ -d "$APP_PATH" ]; then - CURRENT_SHA=$(git -C "$APP_PATH" rev-parse HEAD 2>/dev/null || echo "") - else - echo "⊘ $APP_NAME - directory not found, will build" - echo "| $APP_NAME | N/A | N/A | ⊘ Directory not found |" >> $GITHUB_STEP_SUMMARY - APPS_TO_BUILD=$(echo "$APPS_TO_BUILD" | jq -c --arg app "$APP_NAME" --arg sha "unknown" '. + [{name: $app, sha: $sha}]') - APPS_TO_BUILD_COUNT=$((APPS_TO_BUILD_COUNT + 1)) - continue - fi - - if [ -z "$CURRENT_SHA" ]; then - echo "⊘ $APP_NAME - not a git repo, will build" - echo "| $APP_NAME | N/A | N/A | ⊘ Not a git repo |" >> $GITHUB_STEP_SUMMARY - APPS_TO_BUILD=$(echo "$APPS_TO_BUILD" | jq -c --arg app "$APP_NAME" --arg sha "unknown" '. + [{name: $app, sha: $sha}]') - APPS_TO_BUILD_COUNT=$((APPS_TO_BUILD_COUNT + 1)) - continue - fi - - # Add SHA to the map for all apps (regardless of cache status) - APPS_SHA_MAP=$(echo "$APPS_SHA_MAP" | jq -c --arg app "$APP_NAME" --arg sha "$CURRENT_SHA" '.[$app] = $sha') - - # Cache key that would be used for this app - # Format: -app-build-- - CACHE_KEY="${CACHE_VERSION}-app-build-${APP_NAME}-${CURRENT_SHA}" - SHORT_SHA="${CURRENT_SHA:0:8}" - - echo -n " Checking $APP_NAME (SHA: $SHORT_SHA)... " - - # If force rebuild is enabled, skip cache check and rebuild everything - if [ "$FORCE_REBUILD" == "true" ]; then - echo "🔄 force rebuild" - echo "| $APP_NAME | \`$SHORT_SHA\` | \`$CACHE_KEY\` | 🔄 Force rebuild |" >> $GITHUB_STEP_SUMMARY - APPS_TO_BUILD=$(echo "$APPS_TO_BUILD" | jq -c --arg app "$APP_NAME" --arg sha "$CURRENT_SHA" '. + [{name: $app, sha: $sha}]') - APPS_TO_BUILD_COUNT=$((APPS_TO_BUILD_COUNT + 1)) - continue - fi - - # Check JFrog first before GitHub cache (available for all branches) - if [ "$JFROG_AVAILABLE" == "true" ]; then - JFROG_PATH="${ARTIFACTORY_REPOSITORY_SNAPSHOT}/dev-poc/apps/${APP_NAME}/${APP_NAME}-${CURRENT_SHA}.tar.gz" - - echo "" - echo " 🔍 Checking JFrog for $APP_NAME..." - echo " Path: $JFROG_PATH" - echo " Full SHA: $CURRENT_SHA" - - # Check if artifact exists in JFrog with verbose output - echo " Running: jf rt s \"$JFROG_PATH\"" - SEARCH_OUTPUT=$(jf rt s "$JFROG_PATH" 2>&1) - SEARCH_EXIT_CODE=$? - - echo " Search exit code: $SEARCH_EXIT_CODE" - if [ $SEARCH_EXIT_CODE -eq 0 ]; then - echo " Search output:" - echo "$SEARCH_OUTPUT" | sed 's/^/ /' - - if echo "$SEARCH_OUTPUT" | grep -q "$JFROG_PATH"; then - echo " ✓ Artifact found in JFrog!" - echo "✓ in JFrog" - echo "| $APP_NAME | \`$SHORT_SHA\` | \`$JFROG_PATH\` | đŸ“Ļ In JFrog |" >> $GITHUB_STEP_SUMMARY - APPS_IN_JFROG=$((APPS_IN_JFROG + 1)) - APPS_TO_RESTORE_COUNT=$((APPS_TO_RESTORE_COUNT + 1)) - # Add to restore list with JFrog source - APPS_TO_RESTORE=$(echo "$APPS_TO_RESTORE" | jq -c --argjson app "$app_json" --arg sha "$CURRENT_SHA" --arg jfrog_path "$JFROG_PATH" --arg source "jfrog" '. + [($app + {sha: $sha, jfrog_path: $jfrog_path, source: $source})]') - continue - else - echo " ✗ Artifact not found in search results" - fi - else - echo " ✗ Search failed with error:" - echo "$SEARCH_OUTPUT" | sed 's/^/ /' - fi - echo " → Falling back to GitHub cache check" - fi - - # Check if cache exists using GitHub CLI - # Include --ref to access caches from the current ref (branch, PR, etc.) - CACHE_EXISTS="false" - if ! CACHE_LIST=$(gh cache list --ref "${{ github.ref }}" --key "$CACHE_KEY" --json key --jq ".[].key" 2>&1); then - echo "âš ī¸ Warning: Failed to query cache for $APP_NAME: $CACHE_LIST" - echo "| $APP_NAME | \`$SHORT_SHA\` | \`$CACHE_KEY\` | âš ī¸ Cache check failed - will build |" >> $GITHUB_STEP_SUMMARY - APPS_TO_BUILD=$(echo "$APPS_TO_BUILD" | jq -c --arg app "$APP_NAME" --arg sha "$CURRENT_SHA" '. + [{name: $app, sha: $sha}]') - APPS_TO_BUILD_COUNT=$((APPS_TO_BUILD_COUNT + 1)) - continue - fi - if echo "$CACHE_LIST" | grep -q "^${CACHE_KEY}$"; then - CACHE_EXISTS="true" - APPS_CACHED=$((APPS_CACHED + 1)) - APPS_TO_RESTORE_COUNT=$((APPS_TO_RESTORE_COUNT + 1)) - echo "✓ cached" - echo "| $APP_NAME | \`$SHORT_SHA\` | \`$CACHE_KEY\` | ✅ Cached |" >> $GITHUB_STEP_SUMMARY - # Add to restore list with GitHub cache source - APPS_TO_RESTORE=$(echo "$APPS_TO_RESTORE" | jq -c --argjson app "$app_json" --arg sha "$CURRENT_SHA" --arg cache_key "$CACHE_KEY" --arg source "github-cache" '. + [($app + {sha: $sha, cache_key: $cache_key, source: $source})]') - else - echo "⚡ needs build" - echo "| $APP_NAME | \`$SHORT_SHA\` | \`$CACHE_KEY\` | 🔨 Needs build |" >> $GITHUB_STEP_SUMMARY - APPS_TO_BUILD=$(echo "$APPS_TO_BUILD" | jq -c --arg app "$APP_NAME" --arg sha "$CURRENT_SHA" '. + [{name: $app, sha: $sha}]') - APPS_TO_BUILD_COUNT=$((APPS_TO_BUILD_COUNT + 1)) - fi - - done < <(echo "$MATRIX" | jq -c '.[]') - - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Summary:**" >> $GITHUB_STEP_SUMMARY - echo "- Total apps checked: $APPS_CHECKED" >> $GITHUB_STEP_SUMMARY - echo "- đŸ“Ļ Apps in JFrog: $APPS_IN_JFROG" >> $GITHUB_STEP_SUMMARY - echo "- ✅ Apps with cached builds: $APPS_CACHED" >> $GITHUB_STEP_SUMMARY - echo "- 🔨 Apps needing build: $APPS_TO_BUILD_COUNT" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - TOTAL_AVAILABLE=$((APPS_IN_JFROG + APPS_CACHED)) - if [ $TOTAL_AVAILABLE -gt 0 ] && [ $APPS_CHECKED -gt 0 ]; then - CACHE_HIT_PERCENT=$((TOTAL_AVAILABLE * 100 / APPS_CHECKED)) - echo "**Cache hit rate: ${CACHE_HIT_PERCENT}%** đŸŽ¯" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - fi - - echo "" - echo "Summary:" - echo " Total apps: $APPS_CHECKED" - echo " In JFrog: $APPS_IN_JFROG" - echo " Cached: $APPS_CACHED" - echo " To build: $APPS_TO_BUILD_COUNT" - - # Validate no duplicate apps in build and restore lists - BUILD_APPS=$(echo "$APPS_TO_BUILD" | jq -r '.[].name' | sort) - RESTORE_APPS=$(echo "$APPS_TO_RESTORE" | jq -r '.[].name' | sort) - DUPLICATE_APPS=$(comm -12 <(echo "$BUILD_APPS") <(echo "$RESTORE_APPS")) - - if [ -n "$DUPLICATE_APPS" ]; then - echo "ERROR: Apps appear in both build and restore lists:" - echo "$DUPLICATE_APPS" - exit 1 - fi - - # Validate that we built valid JSON - if ! echo "$APPS_TO_BUILD" | jq empty 2>/dev/null; then - echo "ERROR: Failed to build valid JSON for apps_to_build" - echo "Content: $APPS_TO_BUILD" - exit 1 - fi - - if ! echo "$APPS_TO_RESTORE" | jq empty 2>/dev/null; then - echo "ERROR: Failed to build valid JSON for apps_to_restore" - echo "Content: $APPS_TO_RESTORE" - exit 1 - fi - - # Output app list with SHAs for the build job to use - # Use proper multiline output format for GitHub Actions - echo "apps_to_build<> $GITHUB_OUTPUT - echo "$APPS_TO_BUILD" >> $GITHUB_OUTPUT - echo "APPS_TO_BUILD_JSON_EOF" >> $GITHUB_OUTPUT - - # Output the unified list of apps to restore (from either GitHub cache or JFrog) - echo "apps_to_restore<> $GITHUB_OUTPUT - echo "$APPS_TO_RESTORE" >> $GITHUB_OUTPUT - echo "APPS_TO_RESTORE_JSON_EOF" >> $GITHUB_OUTPUT - - # Output the SHA map for all apps - echo "apps_sha_map<> $GITHUB_OUTPUT - echo "$APPS_SHA_MAP" >> $GITHUB_OUTPUT - echo "APPS_SHA_MAP_JSON_EOF" >> $GITHUB_OUTPUT - - # Output flags for conditional job execution - if [ $APPS_TO_BUILD_COUNT -gt 0 ]; then - echo "has_apps_to_build=true" >> $GITHUB_OUTPUT - else - echo "has_apps_to_build=false" >> $GITHUB_OUTPUT - fi - - if [ $APPS_TO_RESTORE_COUNT -gt 0 ]; then - echo "has_apps_to_restore=true" >> $GITHUB_OUTPUT - else - echo "has_apps_to_restore=false" >> $GITHUB_OUTPUT - fi - - echo "" - if [ $APPS_TO_BUILD_COUNT -eq 0 ]; then - echo "🎉 All apps are cached! No builds needed." - else - echo "✓ Will build $APPS_TO_BUILD_COUNT app(s)" - fi + bash .github/scripts/detect-app-cache.sh '${{ steps.set_matrix.outputs.matrix }}' build-external-apps: runs-on: ubuntu-latest From 3609eab6e4092863c7fc4351b124d9ae5f81fe39 Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Wed, 17 Dec 2025 09:44:01 +0100 Subject: [PATCH 66/83] IONOS(ci): use cache version env for jfrog invalidation Signed-off-by: Kai Henseler --- .github/scripts/detect-app-cache.sh | 2 +- .github/workflows/build-artifact.yml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/scripts/detect-app-cache.sh b/.github/scripts/detect-app-cache.sh index 27f880fc4463e..293383c31cfb0 100644 --- a/.github/scripts/detect-app-cache.sh +++ b/.github/scripts/detect-app-cache.sh @@ -150,7 +150,7 @@ while IFS= read -r app_json; do # Check JFrog first before GitHub cache (available for all branches) if [ "$JFROG_AVAILABLE" == "true" ]; then - JFROG_PATH="${ARTIFACTORY_REPOSITORY_SNAPSHOT}/dev-poc/apps/${APP_NAME}/${APP_NAME}-${CURRENT_SHA}.tar.gz" + JFROG_PATH="${ARTIFACTORY_REPOSITORY_SNAPSHOT}/apps/${CACHE_VERSION}/${APP_NAME}/${APP_NAME}-${CURRENT_SHA}.tar.gz" echo "" echo " 🔍 Checking JFrog for $APP_NAME..." diff --git a/.github/workflows/build-artifact.yml b/.github/workflows/build-artifact.yml index a3314e69d0bd2..d78e3a11d2e2b 100644 --- a/.github/workflows/build-artifact.yml +++ b/.github/workflows/build-artifact.yml @@ -300,7 +300,8 @@ jobs: ls -lh "$ARCHIVE_NAME" # Upload to JFrog - store in snapshot repo under dev/apps/ - JFROG_PATH="${{ env.ARTIFACTORY_REPOSITORY_SNAPSHOT }}/dev-poc/apps/${APP_NAME}/${ARCHIVE_NAME}" + # Include CACHE_VERSION in path to enable complete cache invalidation + JFROG_PATH="${{ env.ARTIFACTORY_REPOSITORY_SNAPSHOT }}/apps/${{ env.CACHE_VERSION }}/${APP_NAME}/${ARCHIVE_NAME}" echo "" echo "Uploading to JFrog..." From db7eab7fc8994f806a6b88405ebd0248154c17ae Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Wed, 17 Dec 2025 09:46:41 +0100 Subject: [PATCH 67/83] IONOS(ci): fix typos and spacing Signed-off-by: Kai Henseler --- .github/workflows/build-artifact.yml | 42 ++++++++++++++-------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/build-artifact.yml b/.github/workflows/build-artifact.yml index d78e3a11d2e2b..b167639ada379 100644 --- a/.github/workflows/build-artifact.yml +++ b/.github/workflows/build-artifact.yml @@ -92,19 +92,19 @@ jobs: if [ "${{ vars.DISABLE_REMOTE_TRIGGER }}" == "true" ]; then echo "âš ī¸ Remote trigger is DISABLED" - echo " The 'trigger-remote-dev-worflow' job will be SKIPPED" + echo " The 'trigger-remote-dev-workflow' job will be SKIPPED" echo "**Status:** âš ī¸ Remote trigger is **DISABLED**" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "The \`trigger-remote-dev-worflow\` job will be skipped." >> $GITHUB_STEP_SUMMARY + echo "The \`trigger-remote-dev-workflow\` job will be skipped." >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "To enable, delete the variable or set it to a value other than 'true' at:" >> $GITHUB_STEP_SUMMARY echo "https://github.com/${{ github.repository }}/settings/variables/actions" >> $GITHUB_STEP_SUMMARY else echo "✅ Remote trigger is ENABLED" - echo " The 'trigger-remote-dev-worflow' job will proceed" + echo " The 'trigger-remote-dev-workflow' job will proceed" echo "**Status:** ✅ Remote trigger is **ENABLED**" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "The \`trigger-remote-dev-worflow\` job will proceed to trigger the remote GitLab workflow." >> $GITHUB_STEP_SUMMARY + echo "The \`trigger-remote-dev-workflow\` job will proceed to trigger the remote GitLab workflow." >> $GITHUB_STEP_SUMMARY fi echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" @@ -278,7 +278,7 @@ jobs: echo "❌ ERROR: App path does not exist: $APP_PATH" exit 1 fi - + echo "App directory contents (top level):" ls -la "$APP_PATH" | head -20 @@ -287,7 +287,7 @@ jobs: echo "" echo "Creating archive: $ARCHIVE_NAME" echo "Running: tar -czf \"$ARCHIVE_NAME\" --exclude=\"node_modules\" --exclude=\".git\" --exclude=\"*.log\" -C \"$(dirname "$APP_PATH")\" \"$(basename "$APP_PATH")\"" - + tar -czf "$ARCHIVE_NAME" \ --exclude="node_modules" \ --exclude=".git" \ @@ -308,7 +308,7 @@ jobs: echo "Target Path: $JFROG_PATH" echo "Properties: app.name=${APP_NAME};app.sha=${APP_SHA};vcs.branch=${{ github.ref_name }};vcs.revision=${{ github.sha }}" echo "Running: jf rt upload \"$ARCHIVE_NAME\" \"$JFROG_PATH\" --target-props \"...\"" - + if jf rt upload "$ARCHIVE_NAME" "$JFROG_PATH" \ --target-props "app.name=${APP_NAME};app.sha=${APP_SHA};vcs.branch=${{ github.ref_name }};vcs.revision=${{ github.sha }}"; then echo "✅ Successfully uploaded $APP_NAME to JFrog" @@ -375,59 +375,59 @@ jobs: if: needs.prepare-matrix.outputs.has_apps_to_restore == 'true' run: | set -e - + echo "đŸ“Ļ Restoring cached apps..." APPS_TO_RESTORE='${{ needs.prepare-matrix.outputs.apps_to_restore }}' - + # Process each app in the restore list echo "$APPS_TO_RESTORE" | jq -c '.[]' | while read -r app_json; do APP_NAME=$(echo "$app_json" | jq -r '.name') APP_SHA=$(echo "$app_json" | jq -r '.sha') APP_PATH=$(echo "$app_json" | jq -r '.path') SOURCE=$(echo "$app_json" | jq -r '.source') - + echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "Restoring: $APP_NAME (source: $SOURCE)" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - + if [ "$SOURCE" == "jfrog" ]; then # Restore from JFrog JFROG_PATH=$(echo "$app_json" | jq -r '.jfrog_path') ARCHIVE_NAME="${APP_NAME}-${APP_SHA}.tar.gz" - + echo "đŸ“Ĩ Downloading from JFrog: $JFROG_PATH" - + if jf rt download "$JFROG_PATH" "$ARCHIVE_NAME" --flat=true; then echo "✅ Downloaded successfully" echo "Extracting to $APP_PATH..." mkdir -p "$(dirname "$APP_PATH")" tar -xzf "$ARCHIVE_NAME" -C "$(dirname "$APP_PATH")" - + if [ -d "$APP_PATH" ] && [ -f "$APP_PATH/appinfo/info.xml" ]; then echo "✅ Restored $APP_NAME from JFrog" else echo "❌ Failed to extract or validate $APP_NAME" exit 1 fi - + rm -f "$ARCHIVE_NAME" else echo "❌ Failed to download from JFrog" exit 1 fi - + elif [ "$SOURCE" == "github-cache" ]; then # Restore from GitHub cache CACHE_KEY=$(echo "$app_json" | jq -r '.cache_key') - + echo "💾 Restoring from GitHub cache: $CACHE_KEY" - + # Use actions/cache/restore in a way that works in a script context # We need to use gh CLI to restore the cache if gh cache restore "$CACHE_KEY" --key "$CACHE_KEY"; then echo "✅ Restored $APP_NAME from GitHub cache" - + # Validate restoration if [ ! -d "$APP_PATH" ] || [ ! -f "$APP_PATH/appinfo/info.xml" ]; then echo "❌ Validation failed for $APP_NAME" @@ -442,7 +442,7 @@ jobs: exit 1 fi done - + echo "" echo "✅ All cached apps restored successfully" env: @@ -764,7 +764,7 @@ jobs: git diff exit 1 # make it red to grab attention - trigger-remote-dev-worflow: + trigger-remote-dev-workflow: runs-on: self-hosted name: Trigger remote workflow From 767a7d4081490e8d98e4d15d50fc1b5afba1da3d Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Mon, 15 Dec 2025 15:34:45 +0100 Subject: [PATCH 68/83] IONOS(theming): fix themefolder settings dropdown colors Signed-off-by: Kai Henseler --- apps/theming/css/ionos/settings.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/theming/css/ionos/settings.css b/apps/theming/css/ionos/settings.css index fae279f518f7f..e61af4e60ab8e 100644 --- a/apps/theming/css/ionos/settings.css +++ b/apps/theming/css/ionos/settings.css @@ -25,4 +25,12 @@ div#content.app-settings { } } } + + #groupfolders-wrapper { + table tbody { + select { + background-color: var(--ion-context-menu-background); + } + } + } } From 6dcead8ee6bb6e991b1c203e38d5de678204c945 Mon Sep 17 00:00:00 2001 From: Tatjana Kaschperko Lindt Date: Wed, 17 Dec 2025 11:13:27 +0100 Subject: [PATCH 69/83] IONOS(Theming): fix the background color in darkmode partial revert of 647f4c4b43903fbc3f3 Signed-off-by: Tatjana Kaschperko Lindt --- apps/theming/lib/Themes/IonosTheme.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/theming/lib/Themes/IonosTheme.php b/apps/theming/lib/Themes/IonosTheme.php index 0a9433550e966..00f29a57d5a1b 100644 --- a/apps/theming/lib/Themes/IonosTheme.php +++ b/apps/theming/lib/Themes/IonosTheme.php @@ -234,7 +234,7 @@ public function getCSSVariables(): array { // used for different active/hover/focus/disabled states '--color-background-hover' => 'light-dark( var(--ion-color-blue-b1), var(--ion-color-blue-b8))', - '--color-background-dark' => $this->util->darken($colorMainBackground, 7), + '--color-background-dark' => 'light-dark( ' . $this->util->darken($colorMainBackground, 7) . ', var(--ion-color-blue-b6))', '--color-background-darker' => $this->util->darken($colorMainBackground, 14), '--color-background-plain' => $ionColorMainBackground, From 4c256ea0e31351521a54008a3369ca0887b57cbc Mon Sep 17 00:00:00 2001 From: Arsalan Ul Haq Sohni Date: Tue, 16 Dec 2025 14:54:09 +0100 Subject: [PATCH 70/83] IONOS(theming): update checkbox-radio-switch styles for better visibility in sharing dropdown Signed-off-by: Arsalan Ul Haq Sohni --- apps/theming/css/ionos/files.css | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/apps/theming/css/ionos/files.css b/apps/theming/css/ionos/files.css index 06d701d2c0c81..4838c7ac43515 100644 --- a/apps/theming/css/ionos/files.css +++ b/apps/theming/css/ionos/files.css @@ -498,3 +498,18 @@ aside#app-sidebar-vue { color: var(--ion-text); } } + +/* Files sharing css */ +span.checkbox-radio-switch.checkbox-radio-switch--checked +> span.checkbox-radio-switch__content.checkbox-content--button-variant.checkbox-content--has-text { + background-color: var(--ion-button-tertiary-background-default); + color: var(--ion-button-tertiary-text); +} + +.sharingTabDetailsView__delete { + display: flex; +} + +.sharingTabDetailsView__footer .button-group button { + margin-inline-start: 1px !important; +} From 06856e1444ba3a38f5b9d2882d3b00087e543bf2 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 11 Dec 2025 07:45:07 +0100 Subject: [PATCH 71/83] fix(comments): Check comment object Signed-off-by: Joas Schilling --- apps/dav/lib/Comments/EntityCollection.php | 9 ++++++-- .../unit/Comments/EntityCollectionTest.php | 23 +++++++++++++++---- lib/private/DB/QueryBuilder/QueryBuilder.php | 8 +++++++ .../Sharded/ShardedQueryBuilder.php | 12 ++++++++-- 4 files changed, 43 insertions(+), 9 deletions(-) diff --git a/apps/dav/lib/Comments/EntityCollection.php b/apps/dav/lib/Comments/EntityCollection.php index 33c58ee44d288..fadb5f5dabd12 100644 --- a/apps/dav/lib/Comments/EntityCollection.php +++ b/apps/dav/lib/Comments/EntityCollection.php @@ -77,6 +77,10 @@ public function getId() { public function getChild($name) { try { $comment = $this->commentsManager->get($name); + if ($comment->getObjectType() !== $this->name + || $comment->getObjectId() !== $this->id) { + throw new NotFound(); + } return new CommentNode( $this->commentsManager, $comment, @@ -130,8 +134,9 @@ public function findChildren($limit = 0, $offset = 0, ?\DateTime $datetime = nul */ public function childExists($name) { try { - $this->commentsManager->get($name); - return true; + $comment = $this->commentsManager->get($name); + return $comment->getObjectType() === $this->name + && $comment->getObjectId() === $this->id; } catch (NotFoundException $e) { return false; } diff --git a/apps/dav/tests/unit/Comments/EntityCollectionTest.php b/apps/dav/tests/unit/Comments/EntityCollectionTest.php index e5a68e5a72684..ea7c61e5a707a 100644 --- a/apps/dav/tests/unit/Comments/EntityCollectionTest.php +++ b/apps/dav/tests/unit/Comments/EntityCollectionTest.php @@ -60,14 +60,16 @@ public function testGetId(): void { } public function testGetChild(): void { + $comment = $this->createMock(IComment::class); + $comment->method('getObjectType') + ->willReturn('files'); + $comment->method('getObjectId') + ->willReturn('19'); + $this->commentsManager->expects($this->once()) ->method('get') ->with('55') - ->willReturn( - $this->getMockBuilder(IComment::class) - ->disableOriginalConstructor() - ->getMock() - ); + ->willReturn($comment); $node = $this->collection->getChild('55'); $this->assertTrue($node instanceof CommentNode); @@ -119,6 +121,17 @@ public function testFindChildren(): void { } public function testChildExistsTrue(): void { + $comment = $this->createMock(IComment::class); + $comment->method('getObjectType') + ->willReturn('files'); + $comment->method('getObjectId') + ->willReturn('19'); + + $this->commentsManager->expects($this->once()) + ->method('get') + ->with('44') + ->willReturn($comment); + $this->assertTrue($this->collection->childExists('44')); } diff --git a/lib/private/DB/QueryBuilder/QueryBuilder.php b/lib/private/DB/QueryBuilder/QueryBuilder.php index 640959c05c660..ef6d9360ebc34 100644 --- a/lib/private/DB/QueryBuilder/QueryBuilder.php +++ b/lib/private/DB/QueryBuilder/QueryBuilder.php @@ -1117,6 +1117,10 @@ public function orHaving(...$having) { * @return $this This QueryBuilder instance. */ public function orderBy($sort, $order = null) { + if ($order !== null && !in_array(strtoupper((string)$order), ['ASC', 'DESC'], true)) { + $order = null; + } + $this->queryBuilder->orderBy( $this->helper->quoteColumnName($sort), $order @@ -1134,6 +1138,10 @@ public function orderBy($sort, $order = null) { * @return $this This QueryBuilder instance. */ public function addOrderBy($sort, $order = null) { + if ($order !== null && !in_array(strtoupper((string)$order), ['ASC', 'DESC'], true)) { + $order = null; + } + $this->queryBuilder->addOrderBy( $this->helper->quoteColumnName($sort), $order diff --git a/lib/private/DB/QueryBuilder/Sharded/ShardedQueryBuilder.php b/lib/private/DB/QueryBuilder/Sharded/ShardedQueryBuilder.php index 04082f76ae891..b6e5e7f354edc 100644 --- a/lib/private/DB/QueryBuilder/Sharded/ShardedQueryBuilder.php +++ b/lib/private/DB/QueryBuilder/Sharded/ShardedQueryBuilder.php @@ -276,13 +276,21 @@ public function setFirstResult($firstResult) { } public function addOrderBy($sort, $order = null) { - $this->registerOrder((string)$sort, (string)$order ?? 'ASC'); + if ($order !== null && !in_array(strtoupper((string)$order), ['ASC', 'DESC'], true)) { + $order = null; + } + + $this->registerOrder((string)$sort, (string)($order ?? 'ASC')); return parent::addOrderBy($sort, $order); } public function orderBy($sort, $order = null) { + if ($order !== null && !in_array(strtoupper((string)$order), ['ASC', 'DESC'], true)) { + $order = null; + } + $this->sortList = []; - $this->registerOrder((string)$sort, (string)$order ?? 'ASC'); + $this->registerOrder((string)$sort, (string)($order ?? 'ASC')); return parent::orderBy($sort, $order); } From 1f8fde6042c4f1c59715222e4078961a2f404f08 Mon Sep 17 00:00:00 2001 From: Maksim Sukharev Date: Thu, 23 Oct 2025 16:13:42 +0200 Subject: [PATCH 72/83] fix(NewUserDialog): allow to deselect a group from the list Signed-off-by: Maksim Sukharev --- .../settings/src/components/Users/NewUserDialog.vue | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/apps/settings/src/components/Users/NewUserDialog.vue b/apps/settings/src/components/Users/NewUserDialog.vue index ede6153f6cbba..e3a8f322811bd 100644 --- a/apps/settings/src/components/Users/NewUserDialog.vue +++ b/apps/settings/src/components/Users/NewUserDialog.vue @@ -75,6 +75,7 @@ :create-option="(value) => ({ id: value, name: value, isCreating: true })" @search="searchGroups" @option:created="createGroup" + @option:deselected="removeGroup" @option:selected="options => addGroup(options.at(-1))" />