From 2790f9069ff386e549036b958c83eb11999a5d59 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Wed, 1 Jul 2026 21:13:25 +0200 Subject: [PATCH 1/2] fix(docker): all to install all shipped apps in `configureNextcloud` This fixes the problem that some shipped apps could not be installed. Moreover this resolves the mapping issue for shipped apps with different default branch names, e.g. `main` on text but `master` on viewer. Signed-off-by: Ferdinand Thiessen --- lib/docker.ts | 84 +++++++++++++++++++++++++++++--------------- package.json | 5 ++- tests/docker.spec.ts | 35 ++++++++++++++++++ 3 files changed, 94 insertions(+), 30 deletions(-) create mode 100644 tests/docker.spec.ts diff --git a/lib/docker.ts b/lib/docker.ts index 2eeaa89a..0fbc5314 100644 --- a/lib/docker.ts +++ b/lib/docker.ts @@ -15,15 +15,9 @@ import { basename, join, resolve, sep } from 'path' import { existsSync, readFileSync } from 'fs' import { XMLParser } from 'fast-xml-parser' -import { User } from './User' +import { User } from './User.ts' const SERVER_IMAGE = 'ghcr.io/nextcloud/continuous-integration-shallow-server' -const VENDOR_APPS: Record = { - text: 'https://github.com/nextcloud/text.git', - viewer: 'https://github.com/nextcloud/viewer.git', - notifications: 'https://github.com/nextcloud/notifications.git', - activity: 'https://github.com/nextcloud/activity.git', -} export const docker = new Docker() @@ -268,13 +262,20 @@ export const configureNextcloud = async function(apps = ['viewer'], vendoredBran } else if (app in applist.disabled) { // built in or mounted already as the app under development await runOcc(['app:enable', '--force', app], { container, verbose: true }) - } else if (app in VENDOR_APPS) { - // apps that are vendored but still missing (i.e. not build in or mounted already) - await runExec(['git', 'clone', '--depth=1', `--branch=${vendoredBranch}`, VENDOR_APPS[app], `apps/${app}`], { container, verbose: true }) - await runOcc(['app:enable', '--force', app], { container, verbose: true }) } else { - // try appstore - await runOcc(['app:install', '--force', app], { container, verbose: true }) + const { shippedApps } = JSON.parse(await runExec(['cat', 'core/shipped.json'], { container })) + if (shippedApps.includes(app)) { + const branchOption = ['main', 'master'].includes(vendoredBranch) ? [] : [`--branch=${vendoredBranch}`] + // apps that are vendored but still missing (i.e. not build in or mounted already) + await runExec( + ['git', 'clone', '--depth=1', ...branchOption, `https://github.com/nextcloud/${encodeURIComponent(app)}.git`, `apps/${app}`], + { container, verbose: true }, + ) + await runOcc(['app:enable', '--force', app], { container, verbose: true }) + } else { + // try appstore + await runOcc(['app:install', '--force', app], { container, verbose: true }) + } } } console.log('└─ Nextcloud is now ready to use 🎉') @@ -339,7 +340,7 @@ export const stopNextcloud = async function() { * * @param container name of the container */ -export const getContainerIP = async function( +export async function getContainerIP( container = getContainer() ): Promise { const containerInspect = await container.inspect() @@ -354,15 +355,17 @@ export const getContainerIP = async function( while (ip === '' && tries < 10) { tries++ - await container.inspect((_err, data) => { - ip = data?.NetworkSettings?.Networks?.default?.IPAddress - // @ts-expect-error -- fallback to legacy network settings - || data?.NetworkSettings?.IPAddress - || '' - }) - - if (ip !== '') { - break + try { + const containerInfo = await container.inspect() + const network = containerInfo.NetworkSettings.Networks.default + || containerInfo.NetworkSettings.Networks.bridge + || Object.values(containerInfo.NetworkSettings.Networks)[0] + if (network.IPAddress) { + ip = network.IPAddress + break + } + } catch { + // ignore and retry } await sleep(1000 * tries) @@ -383,10 +386,26 @@ export const waitOnNextcloud = async function(ip: string) { } interface RunExecOptions { - container: Docker.Container; - user: string; - env: string[]; - verbose: boolean; + /** + * The container to run the command in. If not provided, the current container will be used. + */ + container: Docker.Container + /** + * The user to run the command as. Defaults to 'www-data'. + */ + user: string + /** + * The command will throw an error if it exits with a non-zero exit code. Defaults to true. + */ + failOnError: boolean + /** + * Environment variables to set for the command. Defaults to an empty array. + */ + env: string[] + /** + * If true, the command's output will be printed to the console. Defaults to false. + */ + verbose: boolean } /** @@ -394,7 +413,7 @@ interface RunExecOptions { */ export const runExec = async function( command: string | string[], - { container, user='www-data', verbose=false, env=[] }: Partial = {}, + { container, user='www-data', verbose=false, env=[], failOnError=true }: Partial = {}, ) { container = container || getContainer() const exec = await container.exec({ @@ -427,7 +446,14 @@ export const runExec = async function( } }) dataStream.on('error', (err) => reject(err)) - dataStream.on('end', () => resolve(data.join(''))) + dataStream.on('end', async () => { + const result = await exec.inspect() + if (result.ExitCode && failOnError) { + reject(new Error(data.join(''), { cause: result.ExitCode })) + return + } + resolve(data.join('')) + }) }) } diff --git a/package.json b/package.json index 94759207..3a0d893d 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,10 @@ "cypress:gui": "cypress open", "dev": "echo 'No dev build available, production only' && npm run build", "start:nextcloud": "node playwright/start-nextcloud-server.mjs", - "test": "npm run build && playwright install chromium --only-shell && playwright test", + "test": "npm run test:node && npm run test:playwright", + "test:node": "ts-node --esm tests/*.spec.ts", + "test:playwright": "playwright test", + "pretest": "playwright install chromium --only-shell", "watch": "vite --mode production build --watch" }, "dependencies": { diff --git a/tests/docker.spec.ts b/tests/docker.spec.ts new file mode 100644 index 00000000..6bcda4af --- /dev/null +++ b/tests/docker.spec.ts @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { after, before, describe, test } from 'node:test' +import { configureNextcloud, getContainer, runExec, startNextcloud, stopNextcloud, waitOnNextcloud } from '../lib/docker.ts' + +describe('Docker: Pre-installation of apps', async () => { + before(async () => { + const ip = await startNextcloud('master', false) + await waitOnNextcloud(ip) + await configureNextcloud(['viewer', 'text', 'forms']) + }) + + after(async () => await stopNextcloud()) + + await test('Additional apps: Default mapping works', async () => { + const container = getContainer() + // this must not throw + await runExec(['file', '-f', 'apps/viewer/appinfo/info.xml'], { container, failOnError: true }) + }) + + await test('Additional apps: Mapping "main" branches', async () => { + const container = getContainer() + // this must not throw + await runExec(['file', '-f', 'apps/text/appinfo/info.xml'], { container, failOnError: true}) + }) + + await test('Additional apps: fetching from appstore works', async () => { + const container = getContainer() + // this must not throw + await runExec(['file', '-f', 'apps/forms/appinfo/info.xml'], { container, failOnError: true}) + }) +}) From 6845583275576c4ca6e8f352b105615e4943a0c5 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Wed, 1 Jul 2026 23:00:24 +0200 Subject: [PATCH 2/2] test: fix cypress runs Signed-off-by: Ferdinand Thiessen --- .github/workflows/cypress.yml | 2 +- cypress.config.ts | 11 +++++++---- cypress/support/e2e.ts | 1 - cypress/tsconfig.json | 2 ++ lib/cypress.ts | 4 +++- package-lock.json | 20 ++++++++++++++++++++ package.json | 3 ++- tsconfig.json | 1 + 8 files changed, 36 insertions(+), 8 deletions(-) diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml index f8ac4396..48a4dddc 100644 --- a/.github/workflows/cypress.yml +++ b/.github/workflows/cypress.yml @@ -71,4 +71,4 @@ jobs: name: cypress-summary steps: - name: Summary status - run: if ${{ (needs.cypress-e2e.result != 'success' && needs.cypress-e2e.result != 'skipped') || (needs.cypress-component.result != 'success' && needs.cypress-component.result != 'skipped') }}; then exit 1; fi + run: if ${{ needs.cypress-e2e.result != 'success' && needs.cypress-e2e.result != 'skipped' }}; then exit 1; fi diff --git a/cypress.config.ts b/cypress.config.ts index 56e77cc3..a05b78f5 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -8,6 +8,7 @@ process.env.npm_package_name = 'nextcloud-e2e-test-server' import { configureNextcloud, createSnapshot, setupUsers, startNextcloud, stopNextcloud, waitOnNextcloud } from './lib/docker' import { defineConfig } from 'cypress' +import vitePreprocessor from 'cypress-vite' export default defineConfig({ projectId: 'h2z7r3', @@ -23,6 +24,8 @@ export default defineConfig({ testIsolation: false, setupNodeEvents(on, config) { + on('file:preprocessor', vitePreprocessor({ configFile: false })) + // Remove container after run on('after:run', () => { stopNextcloud() @@ -30,15 +33,15 @@ export default defineConfig({ // Before the browser launches // starting Nextcloud testing container - return startNextcloud(process.env.BRANCH) + return startNextcloud(process.env.BRANCH, false, { forceRecreate: true }) .then((ip) => { // Setting container's IP as base Url config.baseUrl = `http://${ip}/index.php` return ip }) - .then(waitOnNextcloud as (ip: string) => Promise) // void !== undefined for Typescript - .then(configureNextcloud as () => Promise) - .then(setupUsers as () => Promise) + .then((ip) => waitOnNextcloud(ip)) + .then(() => configureNextcloud()) + .then(() => setupUsers()) .then(() => createSnapshot('init')) .then(() => { return config diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index aea7a22b..a8133a93 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -17,5 +17,4 @@ // https://on.cypress.io/configuration // *********************************************************** -import '@cypress/code-coverage/support' import './commands' diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json index b5aa4ff8..6c41a319 100644 --- a/cypress/tsconfig.json +++ b/cypress/tsconfig.json @@ -1,5 +1,6 @@ { "extends": "../tsconfig.json", + "exclude": [], "include": ["./**/*.ts"], "compilerOptions": { "types": ["cypress", "node"], @@ -14,6 +15,7 @@ "strict": true, "noImplicitAny": false, "outDir": "./dist", + "rootDir": "..", "ignoreDeprecations": "6.0" }, } diff --git a/lib/cypress.ts b/lib/cypress.ts index 646cdca6..9bfc0adc 100644 --- a/lib/cypress.ts +++ b/lib/cypress.ts @@ -2,11 +2,13 @@ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ + +import type { Selector } from "./selectors" + import { User } from "./User" import { getNc, restoreState, runCommand, runOccCommand, saveState } from "./commands" import { login, logout } from "./commands/sessions" import { createRandomUser, createUser, deleteUser, modifyUser, listUsers, getUserData, enableUser } from "./commands/users" -import type { Selector } from "./selectors" declare global { namespace Cypress { diff --git a/package-lock.json b/package-lock.json index 823cfee7..9684661d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@types/dockerode": "^4.0.1", "@types/wait-on": "^5.3.4", "cypress": "^15.18.0", + "cypress-vite": "^1.10.2", "ts-node": "^10.9.2", "tslib": "^2.6.2", "typedoc": "^0.28.0", @@ -2987,6 +2988,18 @@ "node": "^20.1.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/cypress-vite": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/cypress-vite/-/cypress-vite-1.10.2.tgz", + "integrity": "sha512-tmCH7riwzprnl5M21ZXfU4jzBY7XBFHLBOAZA7B+SXSYOHmMbNPFrt+Y45EzlW8DVQCTVWs/dH76zx3WXvCZAA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "chokidar": "^2 || ^3 || ^4 || ^5", + "debug": "^2 || ^3 || ^4", + "vite": "^2.9 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, "node_modules/cypress/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -9614,6 +9627,13 @@ } } }, + "cypress-vite": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/cypress-vite/-/cypress-vite-1.10.2.tgz", + "integrity": "sha512-tmCH7riwzprnl5M21ZXfU4jzBY7XBFHLBOAZA7B+SXSYOHmMbNPFrt+Y45EzlW8DVQCTVWs/dH76zx3WXvCZAA==", + "dev": true, + "requires": {} + }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", diff --git a/package.json b/package.json index 3a0d893d..91dac5c6 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "build": "vite --mode production build", "build:doc": "typedoc --out dist/doc lib/commands lib/selectors/index.ts lib && touch dist/doc/.nojekyll", "build:instrumented": "vite --mode instrumented build", - "cypress": "npm run cypress:e2e && npm run cypress:component", + "cypress": "npm run cypress:e2e", "cypress:e2e": "cypress run --e2e", "cypress:gui": "cypress open", "dev": "echo 'No dev build available, production only' && npm run build", @@ -89,6 +89,7 @@ "@types/dockerode": "^4.0.1", "@types/wait-on": "^5.3.4", "cypress": "^15.18.0", + "cypress-vite": "^1.10.2", "ts-node": "^10.9.2", "tslib": "^2.6.2", "typedoc": "^0.28.0", diff --git a/tsconfig.json b/tsconfig.json index 1894336e..c6895087 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "include": ["./lib"], + "exclude": ["./cypress"], "compilerOptions": { "lib": [ "ES2024",