From 717724205e04bda3e1b3ad41e3e23a156e034be7 Mon Sep 17 00:00:00 2001 From: hossein Date: Thu, 17 Sep 2026 17:28:44 +0330 Subject: [PATCH 1/2] ci: restore webapp builds and run backend tests on pull requests --- .github/workflows/backend-ci.yml | 14 +++++++++----- .github/workflows/prod.docs.plus.yml | 6 +++++- .github/workflows/stage.docs.plus.yml | 2 ++ scripts/check-ci.ts | 1 + 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index 1f9014bdf..9f03d0f83 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -1,11 +1,14 @@ name: Backend CI -# Reusable backend quality gates (no-infra unit/integration + real-infra E2E). -# Called only by the prod pipeline, gated on `has_back || app_deploy` โ€” every -# deploy migrates the database and rolls the backend, so every deploy runs these. -# Stage and pull requests never invoke it. +# Pull requests run the backend suites without services. +# Production build triggers also run E2E against Postgres and Redis. on: - workflow_call: {} + workflow_call: + inputs: + run_e2e: + description: Run the Postgres and Redis E2E suites + type: boolean + default: true permissions: contents: read @@ -39,6 +42,7 @@ jobs: run: bun run test backend-e2e: + if: inputs.run_e2e name: ๐Ÿงช Backend E2E (real Postgres + Redis) runs-on: ubuntu-latest timeout-minutes: 20 diff --git a/.github/workflows/prod.docs.plus.yml b/.github/workflows/prod.docs.plus.yml index 4c354dda7..6d077b775 100644 --- a/.github/workflows/prod.docs.plus.yml +++ b/.github/workflows/prod.docs.plus.yml @@ -262,8 +262,10 @@ jobs: # runs the Prisma migration and rolls all three backend services. Gating on # `has_back` alone let a `(build): front` push deploy backend code no test # had ever touched. `has_back` stays so `(build): back no-deploy` still runs. - if: needs.triage.outputs.has_back == 'true' || needs.triage.outputs.app_deploy == 'true' + if: github.event_name == 'pull_request' || needs.triage.outputs.has_back == 'true' || needs.triage.outputs.app_deploy == 'true' uses: ./.github/workflows/backend-ci.yml + with: + run_e2e: ${{ github.event_name != 'pull_request' }} extension-tests: name: ๐Ÿงช Ext (${{ matrix.ext }}) @@ -386,6 +388,8 @@ jobs: - name: ๐Ÿ—๏ธ Build Webapp run: bun run --filter @docs.plus/webapp build:ci env: + # This build checks compilation only; deployment builds use the host's environment. + NEXT_PUBLIC_RESTAPI_URL: http://localhost:4000 NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL || 'http://localhost:54321' }} NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY || 'dummy-key' }} diff --git a/.github/workflows/stage.docs.plus.yml b/.github/workflows/stage.docs.plus.yml index b6578cd0f..6ee90e76f 100644 --- a/.github/workflows/stage.docs.plus.yml +++ b/.github/workflows/stage.docs.plus.yml @@ -173,6 +173,8 @@ jobs: - name: ๐Ÿ—๏ธ Build Webapp run: bun run --filter @docs.plus/webapp build:ci env: + # This build checks compilation only; deployment builds use the host's environment. + NEXT_PUBLIC_RESTAPI_URL: http://localhost:4000 NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL || 'http://localhost:54321' }} NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY || 'dummy-key' }} diff --git a/scripts/check-ci.ts b/scripts/check-ci.ts index b263f4b8f..ab06b8336 100644 --- a/scripts/check-ci.ts +++ b/scripts/check-ci.ts @@ -37,6 +37,7 @@ const SHARED_PREFIXES = [ ] const SUPABASE_STUB = { + NEXT_PUBLIC_RESTAPI_URL: process.env.NEXT_PUBLIC_RESTAPI_URL || 'http://localhost:4000', NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL || 'http://localhost:54321', NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || 'dummy-key', NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3003', From 903b9a378ebcea02afcf02114a63771e7410589e Mon Sep 17 00:00:00 2001 From: hossein Date: Thu, 17 Sep 2026 17:35:34 +0330 Subject: [PATCH 2/2] fix(hypermultimedia): preserve unlisted Vimeo path hashes Keep a watch URL's path hash when building its iframe URL. Existing h query parameters retain precedence through insertion and HTML round-trips. The original report and contribution are HMarzban/extension-hypermultimedia#4: https://github.com/HMarzban/extension-hypermultimedia/pull/4 Port the behavior to the current parser without importing the legacy toolchain changes. Four rendered-iframe regressions cover path hashes, query hashes, player URLs, and precedence. --- .../extension-hypermultimedia/CHANGELOG.md | 3 ++ .../extension-hypermultimedia/README.md | 2 ++ .../cypress/e2e/embeds/vimeo-soundcloud.cy.ts | 34 +++++++++++++++++++ .../src/nodes/vimeo/README.md | 2 +- .../src/nodes/vimeo/embedOptions.ts | 3 +- 5 files changed, 42 insertions(+), 2 deletions(-) diff --git a/extensions/extension-hypermultimedia/CHANGELOG.md b/extensions/extension-hypermultimedia/CHANGELOG.md index 963fb31e4..130a48707 100644 --- a/extensions/extension-hypermultimedia/CHANGELOG.md +++ b/extensions/extension-hypermultimedia/CHANGELOG.md @@ -15,6 +15,9 @@ historical Conventional Commits format. The project adheres to ### Fixed +- Preserve the path hash in unlisted Vimeo URLs through insertion and HTML round-trips. + Query-string hashes keep precedence. Addresses [the standalone contribution](https://github.com/HMarzban/extension-hypermultimedia/pull/4). + - Iframe embeds no longer set `allowfullscreen` when `allow` already includes `fullscreen`. Chrome treats `allow` as the winner and warned on Vimeo, Loom, and Spotify. YouTube still emits `allowfullscreen` because its `allow` list diff --git a/extensions/extension-hypermultimedia/README.md b/extensions/extension-hypermultimedia/README.md index 99148fcd1..d00ce3b0d 100644 --- a/extensions/extension-hypermultimedia/README.md +++ b/extensions/extension-hypermultimedia/README.md @@ -150,6 +150,8 @@ editor.commands.setLoom({ src: 'https://www.loom.com/share/abcdef1234567890' }) editor.commands.updateImageDimensions({ keyId: 'abc123', width: 480, height: 320 }) ``` +For unlisted Vimeo videos, pass the full URL with its path hash or `h` query parameter. HTML export and import preserve that hash. + `updateImageDimensions` edits an image that already sits in the document. It finds the node by its `keyId` attribute and writes `width` and `height`. It returns `false` when no image carries that `keyId`, so `editor.can()` reports a miss. `setImage` writes a fresh `keyId` on every insert, and a host reads it back with `node.attrs.keyId`. Chain any of them like a normal Tiptap command: diff --git a/extensions/extension-hypermultimedia/cypress/e2e/embeds/vimeo-soundcloud.cy.ts b/extensions/extension-hypermultimedia/cypress/e2e/embeds/vimeo-soundcloud.cy.ts index b15f8f35c..0a04d1ef6 100644 --- a/extensions/extension-hypermultimedia/cypress/e2e/embeds/vimeo-soundcloud.cy.ts +++ b/extensions/extension-hypermultimedia/cypress/e2e/embeds/vimeo-soundcloud.cy.ts @@ -13,6 +13,40 @@ describe('Vimeo and SoundCloud embed hosts', () => { cy.nodeCount('vimeo').should('eq', 1) }) + for (const { label, src, hash } of [ + { label: 'watch URL path', src: 'https://vimeo.com/123456789/deadbeef', hash: 'deadbeef' }, + { label: 'watch URL query', src: 'https://vimeo.com/123456789?h=abc123', hash: 'abc123' }, + { + label: 'player URL query', + src: 'https://player.vimeo.com/video/123456789?h=abc123', + hash: 'abc123' + }, + { + label: 'query before path', + src: 'https://vimeo.com/123456789/deadbeef?h=abc123', + hash: 'abc123' + } + ]) { + it(`preserves the unlisted Vimeo hash from the ${label} through HTML round-trip`, () => { + cy.getEditor().then((editor) => { + editor.commands.setVimeo({ src }) + }) + const checkPlayer = () => { + cy.get('#editor iframe').should(($iframe) => { + const url = new URL($iframe.attr('src')!) + expect(url.origin).to.eq('https://player.vimeo.com') + expect(url.pathname).to.eq('/video/123456789') + expect(url.searchParams.get('h')).to.eq(hash) + }) + } + checkPlayer() + cy.getEditor().then((editor) => { + editor.commands.setContent(editor.getHTML()) + }) + checkPlayer() + }) + } + it('inserts a SoundCloud embed with w.soundcloud.com player src', () => { cy.getEditor().then((editor) => { editor.commands.setSoundCloud({ src: 'https://soundcloud.com/forss/flickermood' }) diff --git a/extensions/extension-hypermultimedia/src/nodes/vimeo/README.md b/extensions/extension-hypermultimedia/src/nodes/vimeo/README.md index ae99820c4..c5c0ced4f 100644 --- a/extensions/extension-hypermultimedia/src/nodes/vimeo/README.md +++ b/extensions/extension-hypermultimedia/src/nodes/vimeo/README.md @@ -23,7 +23,7 @@ HyperMultimediaKit.configure({ Configure defaults on `HyperMultimediaKit.configure({ Vimeo: { โ€ฆ } })`. Each node stores its own attributes; unset values fall back to kit defaults when the embed URL is built. -Override player behavior per insert with `setVimeo({ โ€ฆ })`. The node `start` attribute (seconds) maps to Vimeo `start_time`. Unlisted videos need the full watch URL including the `h` query param โ€” it is preserved in the embed URL. +Override player behavior per insert with `setVimeo({ โ€ฆ })`. The node `start` attribute (seconds) maps to Vimeo `start_time`. Unlisted videos need their full watch URL, including the path hash (`/123456789/deadbeef`) or `h` query parameter. The embed preserves that hash through HTML export and import. An explicit `h` query parameter takes precedence over a path hash. Query names follow [Vimeo embed options](https://developer.vimeo.com/player/sdk/embed) (`start_time`, `end_time`, etc.). diff --git a/extensions/extension-hypermultimedia/src/nodes/vimeo/embedOptions.ts b/extensions/extension-hypermultimedia/src/nodes/vimeo/embedOptions.ts index 4f805059b..ed43ce6d8 100644 --- a/extensions/extension-hypermultimedia/src/nodes/vimeo/embedOptions.ts +++ b/extensions/extension-hypermultimedia/src/nodes/vimeo/embedOptions.ts @@ -123,7 +123,8 @@ export const parseVimeoVideoRef = (url: string): { id: string; h?: string } | nu if (host === 'vimeo.com') { const id = parsed.pathname.match(/\/(\d+)/)?.[1] if (!id) return null - const h = parsed.searchParams.get('h') ?? undefined + const pathHash = parsed.pathname.match(/\/\d+\/([0-9a-f]+)\/?$/i)?.[1] + const h = parsed.searchParams.get('h') ?? pathHash return { id, h } } } catch {