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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions .github/workflows/pr-renderer-build.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: "Labs: PR Renderer Build"
name: 'Labs: PR Renderer Build'

on:
pull_request:
Expand Down Expand Up @@ -337,8 +337,6 @@ jobs:
echo "renderer_build_id=${RENDERER_BUILD_ID}" >> "${GITHUB_OUTPUT}"
echo "renderer_build_status=${RENDERER_BUILD_STATUS}" >> "${GITHUB_OUTPUT}"

<<<<<<< Updated upstream
=======
- name: Wait for Labs renderer artifact
id: wait_renderer_artifact
env:
Expand Down Expand Up @@ -644,7 +642,6 @@ jobs:
echo "| New run created | \`${{ steps.dispatch.outputs.created }}\` |"
} >> "${GITHUB_STEP_SUMMARY}"

>>>>>>> Stashed changes
cleanup:
name: Cleanup
if: >-
Expand Down
212 changes: 0 additions & 212 deletions .github/workflows/release-qualification-dispatch.yml

This file was deleted.

20 changes: 12 additions & 8 deletions cicd.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,21 +81,25 @@ main (next) → stable (latest) → X.x (maintenance)
- If the merge conflicts, commits the conflicted merge to the branch so a human can resolve it there
- Merging that PR triggers the automatic stable release workflow

#### 4. Release Qualification Dispatch (`release-qualification-dispatch.yml`)
#### 4. Labs PR Build and Stable Promotion Checks (`pr-renderer-build.yml`)

**Trigger**: Pull requests targeting `stable` (`opened`, `reopened`, `synchronize`, `ready_for_review`)
**Trigger**: Pull requests targeting `main` or `stable` (`opened`, `reopened`, `synchronize`, `ready_for_review`, `closed`)

**Actions**:

- Sends the PR head SHA and branch metadata to the Labs release-orchestrator service
- Polls Labs for the terminal release-qualification state
- Uses the GitHub Actions job itself as the required public status check
- Re-triggers automatically when new commits are pushed to the PR branch
- Builds a `superdoc.tgz` tarball for the PR head SHA
- Uploads the package artifact to Labs
- Registers the PR renderer build in Labs
- For PRs targeting `stable`, runs `Labs: Stable promotion checks` after the PR renderer build is registered
- Polls Labs for the terminal stable-promotion state
- Cleans up registered Labs artifacts when a same-repository PR is merged

Only same-repository PRs dispatch to Labs. Forked PRs are intentionally skipped so private Labs credentials are never exposed to untrusted branches.

**Required configuration**:

- variable: `LABS_API_URL`
- secret: `LABS_PR_BUILD_TOKEN` (falls back to `LABS_RELEASE_QUALIFICATION_TOKEN`)
- variable: `LABS_RELEASE_QUALIFICATION_URL`
- secret: `LABS_RELEASE_QUALIFICATION_TOKEN`

Expand Down Expand Up @@ -229,9 +233,9 @@ These skip semantic-release entirely — useful for re-publishing a failed platf

1. Run "Promote to Stable" workflow
2. Review the generated PR from the candidate branch into `stable`
3. Labs receives the PR head SHA, records the qualification run, and the workflow job polls Labs for the terminal result
3. Labs receives the PR package artifact, registers a PR renderer build, and then runs stable promotion checks for that exact PR head SHA
4. If needed, resolve merge conflicts on the candidate branch and push fixes
5. Re-run or wait for qualification on the new PR head SHA
5. Re-run or wait for the stable promotion checks on the new PR head SHA
6. Merge the PR into `stable`
7. Automatically publishes `1.1.0` as @latest
8. Syncs back to main with version bump
Expand Down
2 changes: 1 addition & 1 deletion packages/esign/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@superdoc-dev/esign",
"version": "2.6.0",
"version": "2.6.1",
"description": "React eSignature component for SuperDoc",
"type": "module",
"main": "./dist/index.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@superdoc-dev/react",
"version": "1.2.0",
"version": "1.2.1",
"description": "Official React wrapper for the SuperDoc document editor",
"type": "module",
"main": "./dist/index.cjs",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { describe, expect, it, vi } from 'vitest';
import { NodeSelection } from 'prosemirror-state';
import type { Node as ProseMirrorNode } from 'prosemirror-model';
import type { Editor } from '../../core/Editor.js';
import { resolveCurrentSelectionInfo } from './selection-info-resolver.js';
Expand Down Expand Up @@ -143,14 +144,31 @@ function doc(blocks: ProseMirrorNode[]): ProseMirrorNode {
return createNode('doc', blocks, { isBlock: false, inlineContent: false });
}

function makeRealNodeSelection(
from: number,
to: number,
node: { type: { name: string }; isBlock: boolean; isLeaf: boolean; isInline: boolean; nodeSize: number },
): NodeSelection {
const sel = Object.create(NodeSelection.prototype);
Object.defineProperty(sel, 'from', { value: from, configurable: true });
Object.defineProperty(sel, 'to', { value: to, configurable: true });
Object.defineProperty(sel, 'empty', { value: false, configurable: true });
Object.defineProperty(sel, 'node', { value: node, configurable: true });
return sel as NodeSelection;
}

/** Minimal editor stub whose doc + selection are controllable per test. */
function makeEditor(docNode: ProseMirrorNode, selection: { from: number; to: number; empty?: boolean }): Editor {
function makeEditor(
docNode: ProseMirrorNode,
selection: { from: number; to: number; empty?: boolean; node?: unknown },
): Editor {
const empty = selection.empty ?? selection.from === selection.to;
const pmSelection = 'node' in selection ? selection : { from: selection.from, to: selection.to, empty };
const listeners = new Map<string, Array<() => void>>();
return {
state: {
doc: docNode,
selection: { from: selection.from, to: selection.to, empty },
selection: pmSelection,
storedMarks: null,
},
on(event: string, listener: () => void) {
Expand Down Expand Up @@ -215,6 +233,41 @@ describe('resolveCurrentSelectionInfo', () => {
]);
});

it('returns null target for a NodeSelection over an addressable text block', () => {
// SelectionInfo.target is only for text selections. A NodeSelection
// over a text-bearing block still represents the node, not a user text
// range that can safely feed comments.create.
const paragraph = textBlock('p1', 'Hello');
const docNode = doc([paragraph]);
const selection = makeRealNodeSelection(1, 1 + paragraph.nodeSize, paragraph as any);
const editor = makeEditor(docNode, selection);

const info = resolveCurrentSelectionInfo(editor, {});

expect(info.empty).toBe(false);
expect(info.target).toBeNull();
});

it('returns null target for a NodeSelection over a text-bearing structured content block', () => {
// Presentation clicks can select a block SDT as a NodeSelection. Even
// though the wrapper contains textblocks, the selection itself is not
// a text selection and should not be projected into a TextTarget.
const innerParagraph = textBlock('p-inside-sdt', 'Field text');
const blockSdt = createNode('structuredContentBlock', [innerParagraph], {
isBlock: true,
inlineContent: false,
attrs: { sdBlockId: 'sdt-1' },
});
const docNode = doc([blockSdt]);
const selection = makeRealNodeSelection(1, 1 + blockSdt.nodeSize, blockSdt as any);
const editor = makeEditor(docNode, selection);

const info = resolveCurrentSelectionInfo(editor, {});

expect(info.empty).toBe(false);
expect(info.target).toBeNull();
});

it('returns null target when no selected block has an addressable blockId', () => {
// Block without sdBlockId / id / blockId — resolver skips it.
const textNode = createNode('text', [], { text: 'Hello' });
Expand Down
Loading
Loading