From b2e27144b7bfafad56af25e444bffb9b799e744b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 7 Aug 2024 19:09:36 +0300 Subject: [PATCH 01/12] integration-test: Initialize Playwright --- .github/workflows/main.yml | 1 + .github/workflows/playwright.yml | 27 ++ .gitignore | 6 +- integration-tests/example.spec.ts | 18 ++ package-lock.json | 105 ++++++- package.json | 2 + playwright.config.ts | 78 +++++ tests-examples/demo-todo-app.spec.ts | 437 +++++++++++++++++++++++++++ 8 files changed, 664 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/playwright.yml create mode 100644 integration-tests/example.spec.ts create mode 100644 playwright.config.ts create mode 100644 tests-examples/demo-todo-app.spec.ts diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 407323c85..fe6f9a753 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -158,6 +158,7 @@ jobs: uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 with: images: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }} + tags: - name: Extract metadata (tags, labels) for DockerHub image id: dh-meta uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 000000000..467190be6 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,27 @@ +name: Playwright Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install dependencies + run: npm ci + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Run Playwright tests + run: npx playwright test + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index c87703b2b..91e8f08c3 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,8 @@ images/app-icons/png/16x16.png images/app-icons/png/32x32.png images/app-icons/png/512x512.png images/app-icons/png/1024x1024.png -images/app-icons/mac/*.png \ No newline at end of file +images/app-icons/mac/*.png +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/integration-tests/example.spec.ts b/integration-tests/example.spec.ts new file mode 100644 index 000000000..54a906a4e --- /dev/null +++ b/integration-tests/example.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from '@playwright/test'; + +test('has title', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Playwright/); +}); + +test('get started link', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Click the get started link. + await page.getByRole('link', { name: 'Get started' }).click(); + + // Expects page to have a heading with the name of Installation. + await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); +}); diff --git a/package-lock.json b/package-lock.json index e428fe2a0..4f66c02f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "trilium", - "version": "0.90.2-beta", + "version": "0.90.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "trilium", - "version": "0.90.2-beta", + "version": "0.90.3", "license": "AGPL-3.0-only", "dependencies": { "@braintree/sanitize-url": "^7.1.0", @@ -93,6 +93,7 @@ "@electron-forge/cli": "^6.4.2", "@electron-forge/maker-squirrel": "^6.4.2", "@electron-forge/plugin-auto-unpack-natives": "^6.4.2", + "@playwright/test": "^1.46.0", "@types/archiver": "^6.0.2", "@types/better-sqlite3": "^7.6.9", "@types/cls-hooked": "^4.3.8", @@ -110,6 +111,7 @@ "@types/jsdom": "^21.1.6", "@types/mime-types": "^2.1.4", "@types/multer": "^1.4.11", + "@types/node": "^22.1.0", "@types/safe-compare": "^1.1.2", "@types/sanitize-html": "^2.11.0", "@types/sax": "^1.2.7", @@ -2062,6 +2064,21 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.0.tgz", + "integrity": "sha512-/QYft5VArOrGRP5pgkrfKksqsKA6CEFyGQ/gjNe6q0y4tZ1aaPfq4gIjudr1s3D+pXyrPRdsy4opKDrjBabE5w==", + "dev": true, + "dependencies": { + "playwright": "1.46.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -2479,11 +2496,11 @@ } }, "node_modules/@types/node": { - "version": "20.14.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz", - "integrity": "sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==", + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.1.0.tgz", + "integrity": "sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.13.0" } }, "node_modules/@types/qs": { @@ -6265,6 +6282,19 @@ "node": ">=6 <7 || >=8" } }, + "node_modules/electron/node_modules/@types/node": { + "version": "20.14.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.14.tgz", + "integrity": "sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/electron/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "node_modules/elkjs": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.2.tgz", @@ -7354,6 +7384,19 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "devOptional": true }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -11613,6 +11656,36 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.0.tgz", + "integrity": "sha512-XYJ5WvfefWONh1uPAUAi0H2xXV5S3vrtcnXe6uAOgdGi3aSpqOSXX08IAjXW34xitfuOJsvXU5anXZxPSEQiJw==", + "dev": true, + "dependencies": { + "playwright-core": "1.46.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.0.tgz", + "integrity": "sha512-9Y/d5UIwuJk8t3+lhmMSAJyNP1BUC/DqP3cQJDQQL/oWqAiuPTLgy7Q5dzglmTLwcBRdetzgNM/gni7ckfTr6A==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/plist": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/plist/-/plist-3.0.5.tgz", @@ -13929,6 +14002,20 @@ "fsevents": "~2.3.3" } }, + "node_modules/tsx/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -14033,9 +14120,9 @@ "dev": true }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz", + "integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==" }, "node_modules/unescape": { "version": "1.0.1", diff --git a/package.json b/package.json index c5cca29c0..cc0fd5d00 100644 --- a/package.json +++ b/package.json @@ -128,6 +128,7 @@ "@electron-forge/cli": "^6.4.2", "@electron-forge/maker-squirrel": "^6.4.2", "@electron-forge/plugin-auto-unpack-natives": "^6.4.2", + "@playwright/test": "^1.46.0", "@types/archiver": "^6.0.2", "@types/better-sqlite3": "^7.6.9", "@types/cls-hooked": "^4.3.8", @@ -145,6 +146,7 @@ "@types/jsdom": "^21.1.6", "@types/mime-types": "^2.1.4", "@types/multer": "^1.4.11", + "@types/node": "^22.1.0", "@types/safe-compare": "^1.1.2", "@types/sanitize-html": "^2.11.0", "@types/sax": "^1.2.7", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 000000000..b59ef03c9 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,78 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './integration-tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/tests-examples/demo-todo-app.spec.ts b/tests-examples/demo-todo-app.spec.ts new file mode 100644 index 000000000..8641cb5f5 --- /dev/null +++ b/tests-examples/demo-todo-app.spec.ts @@ -0,0 +1,437 @@ +import { test, expect, type Page } from '@playwright/test'; + +test.beforeEach(async ({ page }) => { + await page.goto('https://demo.playwright.dev/todomvc'); +}); + +const TODO_ITEMS = [ + 'buy some cheese', + 'feed the cat', + 'book a doctors appointment' +] as const; + +test.describe('New Todo', () => { + test('should allow me to add todo items', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create 1st todo. + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + // Make sure the list only has one todo item. + await expect(page.getByTestId('todo-title')).toHaveText([ + TODO_ITEMS[0] + ]); + + // Create 2nd todo. + await newTodo.fill(TODO_ITEMS[1]); + await newTodo.press('Enter'); + + // Make sure the list now has two todo items. + await expect(page.getByTestId('todo-title')).toHaveText([ + TODO_ITEMS[0], + TODO_ITEMS[1] + ]); + + await checkNumberOfTodosInLocalStorage(page, 2); + }); + + test('should clear text input field when an item is added', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create one todo item. + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + // Check that input is empty. + await expect(newTodo).toBeEmpty(); + await checkNumberOfTodosInLocalStorage(page, 1); + }); + + test('should append new items to the bottom of the list', async ({ page }) => { + // Create 3 items. + await createDefaultTodos(page); + + // create a todo count locator + const todoCount = page.getByTestId('todo-count') + + // Check test using different methods. + await expect(page.getByText('3 items left')).toBeVisible(); + await expect(todoCount).toHaveText('3 items left'); + await expect(todoCount).toContainText('3'); + await expect(todoCount).toHaveText(/3/); + + // Check all items in one call. + await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS); + await checkNumberOfTodosInLocalStorage(page, 3); + }); +}); + +test.describe('Mark all as completed', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test.afterEach(async ({ page }) => { + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should allow me to mark all items as completed', async ({ page }) => { + // Complete all todos. + await page.getByLabel('Mark all as complete').check(); + + // Ensure all todos have 'completed' class. + await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + }); + + test('should allow me to clear the complete state of all items', async ({ page }) => { + const toggleAll = page.getByLabel('Mark all as complete'); + // Check and then immediately uncheck. + await toggleAll.check(); + await toggleAll.uncheck(); + + // Should be no completed classes. + await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']); + }); + + test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => { + const toggleAll = page.getByLabel('Mark all as complete'); + await toggleAll.check(); + await expect(toggleAll).toBeChecked(); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + + // Uncheck first todo. + const firstTodo = page.getByTestId('todo-item').nth(0); + await firstTodo.getByRole('checkbox').uncheck(); + + // Reuse toggleAll locator and make sure its not checked. + await expect(toggleAll).not.toBeChecked(); + + await firstTodo.getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + + // Assert the toggle all is checked again. + await expect(toggleAll).toBeChecked(); + }); +}); + +test.describe('Item', () => { + + test('should allow me to mark items as complete', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create two items. + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + // Check first item. + const firstTodo = page.getByTestId('todo-item').nth(0); + await firstTodo.getByRole('checkbox').check(); + await expect(firstTodo).toHaveClass('completed'); + + // Check second item. + const secondTodo = page.getByTestId('todo-item').nth(1); + await expect(secondTodo).not.toHaveClass('completed'); + await secondTodo.getByRole('checkbox').check(); + + // Assert completed class. + await expect(firstTodo).toHaveClass('completed'); + await expect(secondTodo).toHaveClass('completed'); + }); + + test('should allow me to un-mark items as complete', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create two items. + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + const firstTodo = page.getByTestId('todo-item').nth(0); + const secondTodo = page.getByTestId('todo-item').nth(1); + const firstTodoCheckbox = firstTodo.getByRole('checkbox'); + + await firstTodoCheckbox.check(); + await expect(firstTodo).toHaveClass('completed'); + await expect(secondTodo).not.toHaveClass('completed'); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + await firstTodoCheckbox.uncheck(); + await expect(firstTodo).not.toHaveClass('completed'); + await expect(secondTodo).not.toHaveClass('completed'); + await checkNumberOfCompletedTodosInLocalStorage(page, 0); + }); + + test('should allow me to edit an item', async ({ page }) => { + await createDefaultTodos(page); + + const todoItems = page.getByTestId('todo-item'); + const secondTodo = todoItems.nth(1); + await secondTodo.dblclick(); + await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]); + await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter'); + + // Explicitly assert the new text value. + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + 'buy some sausages', + TODO_ITEMS[2] + ]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); +}); + +test.describe('Editing', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should hide other controls when editing', async ({ page }) => { + const todoItem = page.getByTestId('todo-item').nth(1); + await todoItem.dblclick(); + await expect(todoItem.getByRole('checkbox')).not.toBeVisible(); + await expect(todoItem.locator('label', { + hasText: TODO_ITEMS[1], + })).not.toBeVisible(); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should save edits on blur', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur'); + + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + 'buy some sausages', + TODO_ITEMS[2], + ]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); + + test('should trim entered text', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages '); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); + + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + 'buy some sausages', + TODO_ITEMS[2], + ]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); + + test('should remove the item if an empty text string was entered', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(''); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); + + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + TODO_ITEMS[2], + ]); + }); + + test('should cancel edits on escape', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape'); + await expect(todoItems).toHaveText(TODO_ITEMS); + }); +}); + +test.describe('Counter', () => { + test('should display the current number of todo items', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // create a todo count locator + const todoCount = page.getByTestId('todo-count') + + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + await expect(todoCount).toContainText('1'); + + await newTodo.fill(TODO_ITEMS[1]); + await newTodo.press('Enter'); + await expect(todoCount).toContainText('2'); + + await checkNumberOfTodosInLocalStorage(page, 2); + }); +}); + +test.describe('Clear completed button', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + }); + + test('should display the correct text', async ({ page }) => { + await page.locator('.todo-list li .toggle').first().check(); + await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); + }); + + test('should remove completed items when clicked', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).getByRole('checkbox').check(); + await page.getByRole('button', { name: 'Clear completed' }).click(); + await expect(todoItems).toHaveCount(2); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test('should be hidden when there are no items that are completed', async ({ page }) => { + await page.locator('.todo-list li .toggle').first().check(); + await page.getByRole('button', { name: 'Clear completed' }).click(); + await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden(); + }); +}); + +test.describe('Persistence', () => { + test('should persist its data', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + const todoItems = page.getByTestId('todo-item'); + const firstTodoCheck = todoItems.nth(0).getByRole('checkbox'); + await firstTodoCheck.check(); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + await expect(firstTodoCheck).toBeChecked(); + await expect(todoItems).toHaveClass(['completed', '']); + + // Ensure there is 1 completed item. + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + // Now reload. + await page.reload(); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + await expect(firstTodoCheck).toBeChecked(); + await expect(todoItems).toHaveClass(['completed', '']); + }); +}); + +test.describe('Routing', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + // make sure the app had a chance to save updated todos in storage + // before navigating to a new view, otherwise the items can get lost :( + // in some frameworks like Durandal + await checkTodosInLocalStorage(page, TODO_ITEMS[0]); + }); + + test('should allow me to display active items', async ({ page }) => { + const todoItem = page.getByTestId('todo-item'); + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Active' }).click(); + await expect(todoItem).toHaveCount(2); + await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test('should respect the back button', async ({ page }) => { + const todoItem = page.getByTestId('todo-item'); + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + await test.step('Showing all items', async () => { + await page.getByRole('link', { name: 'All' }).click(); + await expect(todoItem).toHaveCount(3); + }); + + await test.step('Showing active items', async () => { + await page.getByRole('link', { name: 'Active' }).click(); + }); + + await test.step('Showing completed items', async () => { + await page.getByRole('link', { name: 'Completed' }).click(); + }); + + await expect(todoItem).toHaveCount(1); + await page.goBack(); + await expect(todoItem).toHaveCount(2); + await page.goBack(); + await expect(todoItem).toHaveCount(3); + }); + + test('should allow me to display completed items', async ({ page }) => { + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Completed' }).click(); + await expect(page.getByTestId('todo-item')).toHaveCount(1); + }); + + test('should allow me to display all items', async ({ page }) => { + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Active' }).click(); + await page.getByRole('link', { name: 'Completed' }).click(); + await page.getByRole('link', { name: 'All' }).click(); + await expect(page.getByTestId('todo-item')).toHaveCount(3); + }); + + test('should highlight the currently applied filter', async ({ page }) => { + await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected'); + + //create locators for active and completed links + const activeLink = page.getByRole('link', { name: 'Active' }); + const completedLink = page.getByRole('link', { name: 'Completed' }); + await activeLink.click(); + + // Page change - active items. + await expect(activeLink).toHaveClass('selected'); + await completedLink.click(); + + // Page change - completed items. + await expect(completedLink).toHaveClass('selected'); + }); +}); + +async function createDefaultTodos(page: Page) { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + for (const item of TODO_ITEMS) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } +} + +async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) { + return await page.waitForFunction(e => { + return JSON.parse(localStorage['react-todos']).length === e; + }, expected); +} + +async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) { + return await page.waitForFunction(e => { + return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e; + }, expected); +} + +async function checkTodosInLocalStorage(page: Page, title: string) { + return await page.waitForFunction(t => { + return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t); + }, title); +} From cc9bb31b9cdc232b31e03d0e608ae529afb57dcd Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 7 Aug 2024 19:10:41 +0300 Subject: [PATCH 02/12] integration-test: Disable webkit runner for now --- playwright.config.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/playwright.config.ts b/playwright.config.ts index b59ef03c9..10528c325 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -43,11 +43,6 @@ export default defineConfig({ use: { ...devices['Desktop Firefox'] }, }, - { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, - }, - /* Test against mobile viewports. */ // { // name: 'Mobile Chrome', From 2efbe972866a8dbb593acac9c51389a55e7938d2 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 7 Aug 2024 20:27:04 +0300 Subject: [PATCH 03/12] integration-test: Add basic test for KaTeX --- integration-tests/katex.spec.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 integration-tests/katex.spec.ts diff --git a/integration-tests/katex.spec.ts b/integration-tests/katex.spec.ts new file mode 100644 index 000000000..1b6be60a5 --- /dev/null +++ b/integration-tests/katex.spec.ts @@ -0,0 +1,26 @@ +import { test, expect } from '@playwright/test'; + +const ROOT_URL = "http://localhost:8080"; +const LOGIN_PASSWORD = "eliandoran"; + +test("Can insert equations", async ({ page }) => { + await page.setDefaultTimeout(60_000); + await page.setDefaultNavigationTimeout(60_000); + + await page.goto(ROOT_URL); + await expect(page).toHaveURL(`${ROOT_URL}/login`); + + // Log in + await page.getByRole("textbox", { name: "Password" }).fill(LOGIN_PASSWORD); + await page.getByRole("button", { name: "Login"}).click(); + await page.waitForURL(/\/#root\//); + + // Create a new note + // await page.locator("button.button-widget.bx-file-blank") + // .click(); + + const activeNote = page.locator(".component.note-split:visible"); + const noteContent = activeNote + .locator(".note-detail-editable-text-editor") + await noteContent.press("Ctrl+M"); +}); \ No newline at end of file From a8814f0e9b7531ab0d79fd85bf66484387408b4d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 7 Aug 2024 22:10:16 +0300 Subject: [PATCH 04/12] ci: Extract docker build into separate workflow --- .github/workflows/main-docker.yml | 96 +++++++++++++++++++++++++++++++ .github/workflows/main.yml | 83 +------------------------- 2 files changed, 97 insertions(+), 82 deletions(-) create mode 100644 .github/workflows/main-docker.yml diff --git a/.github/workflows/main-docker.yml b/.github/workflows/main-docker.yml new file mode 100644 index 000000000..ea35b8265 --- /dev/null +++ b/.github/workflows/main-docker.yml @@ -0,0 +1,96 @@ +on: + push: + branches: + - "develop" + - "feature/update**" + - "feature/server_esm**" + paths-ignore: + - "docs/**" + - "bin/**" + tags: + - "v*" + workflow_dispatch: + +env: + GHCR_REGISTRY: ghcr.io + DOCKERHUB_REGISTRY: docker.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build_docker: + name: Build Docker images + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + attestations: write + id-token: write + steps: + - uses: actions/checkout@v4 + - name: Extract metadata (tags, labels) for GHCR image + id: ghcr-meta + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }} + tags: + - name: Extract metadata (tags, labels) for DockerHub image + id: dh-meta + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }} + - name: Set up node & dependencies + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "npm" + - run: npm ci + - name: Run the TypeScript build + run: npx tsc + - name: Create server-package.json + run: cat package.json | grep -v electron > server-package.json + - name: Log in to the GHCR container registry + uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 + with: + registry: ${{ env.GHCR_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - uses: docker/setup-buildx-action@v3 + - name: Build and push container image to GHCR + uses: docker/build-push-action@v6 + id: ghcr-push + with: + context: . + push: true + tags: ${{ steps.ghcr-meta.outputs.tags }} + labels: ${{ steps.ghcr-meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + - name: Generate and push artifact attestation to GHCR + uses: actions/attest-build-provenance@v1 + with: + subject-name: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME}} + subject-digest: ${{ steps.ghcr-push.outputs.digest }} + push-to-registry: true + - name: Log in to the DockerHub container registry + uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 + with: + registry: ${{ env.DOCKERHUB_REGISTRY }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push image to DockerHub + uses: docker/build-push-action@v6 + id: dh-push + with: + context: . + push: true + tags: ${{ steps.dh-meta.outputs.tags }} + labels: ${{ steps.dh-meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + - name: Generate and push artifact attestation to DockerHub + uses: actions/attest-build-provenance@v1 + with: + subject-name: ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME}} + subject-digest: ${{ steps.dh-push.outputs.digest }} + push-to-registry: true + \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fe6f9a753..7a0acc027 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,11 +16,6 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -env: - GHCR_REGISTRY: ghcr.io - DOCKERHUB_REGISTRY: docker.io - IMAGE_NAME: ${{ github.repository }} - jobs: build_darwin-x64: name: Build macOS x86_64 @@ -142,80 +137,4 @@ jobs: uses: actions/upload-artifact@v4 with: name: TriliumNext Notes for Windows (Setup) - path: out/make/squirrel.windows/x64/*.exe - build_docker: - name: Build Docker images - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - attestations: write - id-token: write - steps: - - uses: actions/checkout@v4 - - name: Extract metadata (tags, labels) for GHCR image - id: ghcr-meta - uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 - with: - images: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }} - tags: - - name: Extract metadata (tags, labels) for DockerHub image - id: dh-meta - uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 - with: - images: ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }} - - name: Set up node & dependencies - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: "npm" - - run: npm ci - - name: Run the TypeScript build - run: npx tsc - - name: Create server-package.json - run: cat package.json | grep -v electron > server-package.json - - name: Log in to the GHCR container registry - uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 - with: - registry: ${{ env.GHCR_REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - uses: docker/setup-buildx-action@v3 - - name: Build and push container image to GHCR - uses: docker/build-push-action@v6 - id: ghcr-push - with: - context: . - push: true - tags: ${{ steps.ghcr-meta.outputs.tags }} - labels: ${{ steps.ghcr-meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - - name: Generate and push artifact attestation to GHCR - uses: actions/attest-build-provenance@v1 - with: - subject-name: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME}} - subject-digest: ${{ steps.ghcr-push.outputs.digest }} - push-to-registry: true - - name: Log in to the DockerHub container registry - uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 - with: - registry: ${{ env.DOCKERHUB_REGISTRY }} - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push image to DockerHub - uses: docker/build-push-action@v6 - id: dh-push - with: - context: . - push: true - tags: ${{ steps.dh-meta.outputs.tags }} - labels: ${{ steps.dh-meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - - name: Generate and push artifact attestation to DockerHub - uses: actions/attest-build-provenance@v1 - with: - subject-name: ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME}} - subject-digest: ${{ steps.dh-push.outputs.digest }} - push-to-registry: true + path: out/make/squirrel.windows/x64/*.exe \ No newline at end of file From 5295d956a21a024980cd8dccbd4ef2acdde2514c Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 7 Aug 2024 22:16:49 +0300 Subject: [PATCH 05/12] ci: Add Docker test step --- .github/workflows/main-docker.yml | 40 +++++++++++++++++++++++++++++++ .github/workflows/main.yml | 1 + 2 files changed, 41 insertions(+) diff --git a/.github/workflows/main-docker.yml b/.github/workflows/main-docker.yml index ea35b8265..019a8da93 100644 --- a/.github/workflows/main-docker.yml +++ b/.github/workflows/main-docker.yml @@ -15,11 +15,51 @@ env: GHCR_REGISTRY: ghcr.io DOCKERHUB_REGISTRY: docker.io IMAGE_NAME: ${{ github.repository }} + TEST_TAG: triliumnext/notes:test jobs: + test_docker: + name: Check Docker build + runs-on: ubuntu-latest + steps: + - name: Checkout the repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Set up node & dependencies + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "npm" + + - run: npm ci + + - name: Run the TypeScript build + run: npx tsc + + - name: Create server-package.json + run: cat package.json | grep -v electron > server-package.json + + - name: Build and export to Docker + uses: docker/build-push-action@v6 + with: + context: . + load: true + tags: ${{ env.TEST_TAG }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Run the container + run: | + docker run --rm ${{ env.TEST_TAG }} + build_docker: name: Build Docker images runs-on: ubuntu-latest + needs: + - test_docker permissions: contents: read packages: write diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7a0acc027..5870e3333 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,6 +8,7 @@ on: paths-ignore: - "docs/**" - "bin/**" + - ".github/workflows/main-docker.yml" tags: - "v*" workflow_dispatch: From e64234b462625d91834041e9a8f73cd33cd707ae Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 7 Aug 2024 22:29:35 +0300 Subject: [PATCH 06/12] ci: Add test for Docker healtcheck --- .github/workflows/main-docker.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main-docker.yml b/.github/workflows/main-docker.yml index 019a8da93..7c23a98f6 100644 --- a/.github/workflows/main-docker.yml +++ b/.github/workflows/main-docker.yml @@ -51,9 +51,11 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max - - name: Run the container - run: | - docker run --rm ${{ env.TEST_TAG }} + - name: Run the container in the background + run: docker run -d --rm ${{ env.TEST_TAG }} + + - name: Wait for the healthchecks to pass + run: timeout 60s sh -c 'until docker ps | grep ${{ env.TEST_TAG }} | grep -q healthy; do echo "Waiting for container to be healthy..."; sleep 2; done' build_docker: name: Build Docker images From 9cf14cc3a86c5ebd8be1adaffd74140a69f29055 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 7 Aug 2024 22:39:30 +0300 Subject: [PATCH 07/12] ci: Use healthcheck action --- .github/workflows/main-docker.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main-docker.yml b/.github/workflows/main-docker.yml index 7c23a98f6..c50374614 100644 --- a/.github/workflows/main-docker.yml +++ b/.github/workflows/main-docker.yml @@ -52,10 +52,15 @@ jobs: cache-to: type=gha,mode=max - name: Run the container in the background - run: docker run -d --rm ${{ env.TEST_TAG }} + run: docker run -d --rm --name trilium_local ${{ env.TEST_TAG }} - name: Wait for the healthchecks to pass - run: timeout 60s sh -c 'until docker ps | grep ${{ env.TEST_TAG }} | grep -q healthy; do echo "Waiting for container to be healthy..."; sleep 2; done' + uses: stringbean/docker-healthcheck-action@v1 + with: + container: trilium_local + wait-time: 50 + require-status: running + require-healthy: true build_docker: name: Build Docker images From f4e7dcd5e846a7f2c990d457906e6748be33910d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 7 Aug 2024 23:25:22 +0300 Subject: [PATCH 08/12] docker: Fix healthcheck & convert to TypeScript (closes #296) --- Dockerfile | 5 +++++ docker_healthcheck.js => docker_healthcheck.ts | 16 ++++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) rename docker_healthcheck.js => docker_healthcheck.ts (72%) diff --git a/Dockerfile b/Dockerfile index 8087af0af..339f44fd3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,6 +24,11 @@ COPY server-package.json package.json # Copy TypeScript build artifacts into the original directory structure. RUN ls RUN cp -R build/src/* src/. + +# Copy the healthcheck +RUN cp build/docker_healthcheck.js . +RUN rm docker_healthcheck.ts + RUN rm -r build # Install app dependencies diff --git a/docker_healthcheck.js b/docker_healthcheck.ts similarity index 72% rename from docker_healthcheck.js rename to docker_healthcheck.ts index 9761aebe2..c11c853a4 100755 --- a/docker_healthcheck.js +++ b/docker_healthcheck.ts @@ -1,7 +1,7 @@ -const http = require("http"); -const ini = require("ini"); -const fs = require("fs"); -const dataDir = require('./src/services/data_dir'); +import http from "http"; +import ini from "ini"; +import fs from "fs"; +import dataDir from './src/services/data_dir.js'; const config = ini.parse(fs.readFileSync(dataDir.CONFIG_INI_PATH, 'utf-8')); if (config.Network.https) { @@ -10,12 +10,12 @@ if (config.Network.https) { process.exit(0); } -const port = require('./src/services/port'); -const host = require('./src/services/host'); +import port from './src/services/port.js'; +import host from './src/services/host.js'; -const options = { timeout: 2000 }; +const options: http.RequestOptions = { timeout: 2000 }; -const callback = res => { +const callback: (res: http.IncomingMessage) => void = res => { console.log(`STATUS: ${res.statusCode}`); if (res.statusCode === 200) { process.exit(0); From 85db664756ddde6c757c5857c6b072c0c20996ab Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 7 Aug 2024 23:46:12 +0300 Subject: [PATCH 09/12] docker: Reintroduce cross-platform builds --- .github/workflows/main-docker.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/main-docker.yml b/.github/workflows/main-docker.yml index c50374614..9f1b7154c 100644 --- a/.github/workflows/main-docker.yml +++ b/.github/workflows/main-docker.yml @@ -67,6 +67,14 @@ jobs: runs-on: ubuntu-latest needs: - test_docker + strategy: + fail-fast: true + matrix: + platform: + - linux/amd64 + - linux/arm64 + - linux/arm/v7 + - linux/arm/v6 permissions: contents: read packages: write @@ -107,6 +115,7 @@ jobs: id: ghcr-push with: context: . + platforms: ${{ matrix.platform }} push: true tags: ${{ steps.ghcr-meta.outputs.tags }} labels: ${{ steps.ghcr-meta.outputs.labels }} @@ -129,6 +138,7 @@ jobs: id: dh-push with: context: . + platforms: ${{ matrix.platform }} push: true tags: ${{ steps.dh-meta.outputs.tags }} labels: ${{ steps.dh-meta.outputs.labels }} From 645fd43339def2f37aaa1fd60835071c9448e760 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 8 Aug 2024 00:03:51 +0300 Subject: [PATCH 10/12] Revert "docker: Reintroduce cross-platform builds" This reverts commit 85db664756ddde6c757c5857c6b072c0c20996ab. --- .github/workflows/main-docker.yml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.github/workflows/main-docker.yml b/.github/workflows/main-docker.yml index 9f1b7154c..c50374614 100644 --- a/.github/workflows/main-docker.yml +++ b/.github/workflows/main-docker.yml @@ -67,14 +67,6 @@ jobs: runs-on: ubuntu-latest needs: - test_docker - strategy: - fail-fast: true - matrix: - platform: - - linux/amd64 - - linux/arm64 - - linux/arm/v7 - - linux/arm/v6 permissions: contents: read packages: write @@ -115,7 +107,6 @@ jobs: id: ghcr-push with: context: . - platforms: ${{ matrix.platform }} push: true tags: ${{ steps.ghcr-meta.outputs.tags }} labels: ${{ steps.ghcr-meta.outputs.labels }} @@ -138,7 +129,6 @@ jobs: id: dh-push with: context: . - platforms: ${{ matrix.platform }} push: true tags: ${{ steps.dh-meta.outputs.tags }} labels: ${{ steps.dh-meta.outputs.labels }} From 865c0fb981e17d91cd7c5575377c17eb61f344a4 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 8 Aug 2024 00:06:19 +0300 Subject: [PATCH 11/12] docker: Reintroduce cross-platform builds --- .github/workflows/main-docker.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main-docker.yml b/.github/workflows/main-docker.yml index c50374614..96bffbd6e 100644 --- a/.github/workflows/main-docker.yml +++ b/.github/workflows/main-docker.yml @@ -16,6 +16,7 @@ env: DOCKERHUB_REGISTRY: docker.io IMAGE_NAME: ${{ github.repository }} TEST_TAG: triliumnext/notes:test + PLATFORMS: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 jobs: test_docker: @@ -72,8 +73,10 @@ jobs: packages: write attestations: write id-token: write - steps: + steps: - uses: actions/checkout@v4 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - name: Extract metadata (tags, labels) for GHCR image id: ghcr-meta uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 @@ -107,7 +110,8 @@ jobs: id: ghcr-push with: context: . - push: true + platforms: ${{ env.PLATFORMS }} + push: true tags: ${{ steps.ghcr-meta.outputs.tags }} labels: ${{ steps.ghcr-meta.outputs.labels }} cache-from: type=gha @@ -129,6 +133,7 @@ jobs: id: dh-push with: context: . + platforms: ${{ env.PLATFORMS }} push: true tags: ${{ steps.dh-meta.outputs.tags }} labels: ${{ steps.dh-meta.outputs.labels }} From 32eaded3af67a7257d42773571740458c8099484 Mon Sep 17 00:00:00 2001 From: Martin Carpella Date: Wed, 7 Aug 2024 23:15:19 +0200 Subject: [PATCH 12/12] docs: Add Dockerhub link to readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c6782e48f..f5bb6f4e4 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ To use TriliumNext on a mobile device: ### Server -To install TriliumNext on your own server (including via Docker) follow [the server installation docs](https://triliumnext.github.io/Docs/Wiki/server-installation). +To install TriliumNext on your own server (including via Docker from [Dockerhub](https://hub.docker.com/r/triliumnext/notes)) follow [the server installation docs](https://triliumnext.github.io/Docs/Wiki/server-installation). ## 📝 Documentation