Skip to content

fix(mcp): preserve omitted blocks fields on content writes #10916

fix(mcp): preserve omitted blocks fields on content writes

fix(mcp): preserve omitted blocks fields on content writes #10916

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
merge_group:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
change-plan:
name: Test Plan
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
unit: ${{ steps.plan.outputs.unit }}
unit_mode: ${{ steps.plan.outputs.unit_mode }}
unit_packages: ${{ steps.plan.outputs.unit_packages }}
smoke: ${{ steps.plan.outputs.smoke }}
integration: ${{ steps.plan.outputs.integration }}
d1: ${{ steps.plan.outputs.d1 }}
browser: ${{ steps.plan.outputs.browser }}
browser_admin: ${{ steps.plan.outputs.browser_admin }}
browser_embeds: ${{ steps.plan.outputs.browser_embeds }}
browser_release: ${{ steps.plan.outputs.browser_release }}
e2e_node: ${{ steps.plan.outputs.e2e_node }}
e2e_cloudflare: ${{ steps.plan.outputs.e2e_cloudflare }}
e2e_table: ${{ steps.plan.outputs.e2e_table }}
e2e_playground: ${{ steps.plan.outputs.e2e_playground }}
full: ${{ steps.plan.outputs.full }}
plan: ${{ steps.plan.outputs.plan }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
- name: Select tests for this change
id: plan
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
git diff --name-only --no-renames --diff-filter=ACDMRT "$BASE_SHA...$HEAD_SHA" |
node .github/scripts/test-plan.mjs --github-output >> "$GITHUB_OUTPUT"
else
node .github/scripts/test-plan.mjs --github-output </dev/null >> "$GITHUB_OUTPUT"
fi
typecheck:
name: Typecheck
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22
cache: pnpm
- name: Check package versions
run: node .github/scripts/check-no-major.mjs
- run: pnpm install --frozen-lockfile
- run: pnpm build
- run: pnpm typecheck
- run: pnpm run --filter emdash-demo --filter @emdash-cms/demo-cloudflare typecheck
- run: pnpm typecheck:templates
- run: node scripts/typecheck-public-source.mjs
- run: node scripts/sync-skills-repo.mjs --check
- run: pnpm run --filter @emdash-cms/release-service --filter @emdash-cms/release-verifier --filter @emdash-cms/release-action typecheck
- run: pnpm run --filter @emdash-cms/release-service --filter @emdash-cms/release-verifier --filter @emdash-cms/release-action build
- run: git diff --exit-code -- apps/release-action/dist/index.js
- run: pnpm --dir apps/release-service exec wrangler types --check
- run: pnpm --dir apps/release-verifier exec wrangler types --check
- run: pnpm --dir apps/release-service exec wrangler deploy --dry-run
- run: pnpm --dir apps/release-verifier exec wrangler deploy --dry-run
- run: pnpm lint
- name: Validate changesets
run: |
base=$(git merge-base origin/main HEAD)
added=$(git diff --name-only --diff-filter=d "$base" -- ':(glob).changeset/*.md' ':(exclude).changeset/README.md')
if [ -n "$added" ]; then
pnpm changeset status --since=origin/main
else
echo "No changesets added since origin/main; skipping validation."
fi
# Compatibility checks for the current ruleset. The Typecheck job above now
# covers these checks; skipped jobs report their legacy contexts without
# reserving runners. Remove these after the ruleset stops requiring them.
lint:
name: Lint
if: false
runs-on: ubuntu-latest
steps:
- run: echo "Lint runs in the Typecheck job"
version-check:
name: Version Check
if: false
runs-on: ubuntu-latest
steps:
- run: echo "Version validation runs in the Typecheck job"
test-shards:
name: Test Shard (${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
needs: change-plan
if: needs.change-plan.outputs.unit_mode == 'full'
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: true
matrix:
shardIndex: [1, 2, 3, 4]
shardTotal: [4]
services:
postgres:
image: postgres:17
env:
POSTGRES_PASSWORD: test
POSTGRES_DB: emdash_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22.16.0
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm run --filter emdash... build
- run: pnpm --filter emdash exec vitest run --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
env:
EMDASH_TEST_PG: postgres://postgres:test@localhost:5432/emdash_test
# Render tests use the Astro Vite plugin (vitest.repro.config.ts);
# they can't run under the plain-node config in test:unit.
- if: matrix.shardIndex == 1
run: pnpm --filter emdash exec vitest run --config vitest.repro.config.ts
test-packages-and-fixtures:
name: Package and Fixture Tests
needs: change-plan
if: needs.change-plan.outputs.unit == 'true' || needs.change-plan.outputs.smoke == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22.16.0
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Build and test selected packages
if: needs.change-plan.outputs.unit == 'true'
env:
UNIT_PACKAGES: ${{ needs.change-plan.outputs.unit_packages }}
run: |
packages=()
while IFS= read -r package; do
packages+=("$package")
done < <(node -e 'for (const name of JSON.parse(process.env.UNIT_PACKAGES)) console.log(name)')
if [ "${#packages[@]}" -eq 0 ]; then
echo "::error::Unit tests were selected without any packages"
exit 1
fi
build_filters=()
test_filters=()
check_registry_verification=false
for package in "${packages[@]}"; do
build_filters+=(--filter "${package}...")
if [ "$package" = "@emdash-cms/registry-verification" ]; then
check_registry_verification=true
else
test_filters+=(--filter "$package")
fi
done
pnpm run --if-present "${build_filters[@]}" build
if [ "${#test_filters[@]}" -gt 0 ]; then
pnpm run "${test_filters[@]}" test
fi
if [ "$check_registry_verification" = true ]; then
pnpm --filter @emdash-cms/registry-verification test:package
fi
- name: Test seed fixtures and prerendering
if: needs.change-plan.outputs.smoke == 'true'
run: >-
pnpm --filter emdash exec vitest run
tests/integration/smoke/template-smoke.test.ts
tests/integration/smoke/sqlite-prerender.test.ts
--config vitest.smoke.config.ts
test:
name: Tests
if: always()
needs:
[
change-plan,
test-shards,
test-packages-and-fixtures,
test-smoke-isolated,
test-smoke-sites,
test-backend,
test-browser,
test-e2e,
test-e2e-table,
]
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Check selected test results
env:
PLAN_RESULT: ${{ needs.change-plan.result }}
CORE_UNIT_SELECTED: ${{ needs.change-plan.outputs.unit_mode == 'full' }}
CORE_UNIT_RESULT: ${{ needs.test-shards.result }}
PACKAGE_OR_FIXTURES_SELECTED: ${{ needs.change-plan.outputs.unit == 'true' || needs.change-plan.outputs.smoke == 'true' }}
PACKAGE_OR_FIXTURES_RESULT: ${{ needs.test-packages-and-fixtures.result }}
SMOKE_SELECTED: ${{ needs.change-plan.outputs.smoke }}
SMOKE_ISOLATED_RESULT: ${{ needs.test-smoke-isolated.result }}
SMOKE_SITES_RESULT: ${{ needs.test-smoke-sites.result }}
BACKEND_SELECTED: ${{ needs.change-plan.outputs.integration == 'true' || needs.change-plan.outputs.d1 == 'true' }}
BACKEND_RESULT: ${{ needs.test-backend.result }}
BROWSER_SELECTED: ${{ needs.change-plan.outputs.browser == 'true' || needs.change-plan.outputs.e2e_playground == 'true' }}
BROWSER_RESULT: ${{ needs.test-browser.result }}
E2E_SELECTED: ${{ needs.change-plan.outputs.e2e_node == 'true' || needs.change-plan.outputs.e2e_cloudflare == 'true' }}
E2E_RESULT: ${{ needs.test-e2e.result }}
TABLE_SELECTED: ${{ needs.change-plan.outputs.e2e_table }}
TABLE_RESULT: ${{ needs.test-e2e-table.result }}
run: |
failed=0
check_lane() {
name="$1"
selected="$2"
result="$3"
expected=skipped
if [ "$selected" = true ]; then expected=success; fi
if [ "$result" != "$expected" ]; then
echo "::error::$name was $result; test plan expected $expected"
failed=1
fi
}
if [ "$PLAN_RESULT" != success ]; then
echo "::error::Test planning failed or was cancelled"
failed=1
fi
check_lane "Core unit tests" "$CORE_UNIT_SELECTED" "$CORE_UNIT_RESULT"
check_lane "Package and fixture tests" "$PACKAGE_OR_FIXTURES_SELECTED" "$PACKAGE_OR_FIXTURES_RESULT"
check_lane "Isolated-install smoke tests" "$SMOKE_SELECTED" "$SMOKE_ISOLATED_RESULT"
check_lane "Live-site smoke tests" "$SMOKE_SELECTED" "$SMOKE_SITES_RESULT"
check_lane "Backend tests" "$BACKEND_SELECTED" "$BACKEND_RESULT"
check_lane "Browser and playground tests" "$BROWSER_SELECTED" "$BROWSER_RESULT"
check_lane "Node/Cloudflare E2E" "$E2E_SELECTED" "$E2E_RESULT"
check_lane "Table E2E" "$TABLE_SELECTED" "$TABLE_RESULT"
exit "$failed"
test-smoke-isolated:
name: Smoke Isolated Install
needs: change-plan
if: needs.change-plan.outputs.smoke == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: >-
pnpm --filter emdash exec vitest run
tests/integration/smoke/isolated-install.test.ts
--config vitest.smoke.config.ts
test-smoke-sites:
name: Smoke Tests
needs: change-plan
if: needs.change-plan.outputs.smoke == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: >-
pnpm --filter emdash exec vitest run
tests/integration/smoke/site-matrix-smoke.test.ts
--config vitest.smoke.config.ts
test-backend:
name: Backend Tests
needs: change-plan
if: needs.change-plan.outputs.integration == 'true' || needs.change-plan.outputs.d1 == 'true'
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- if: needs.change-plan.outputs.integration == 'true'
run: pnpm --filter emdash exec vitest run --config vitest.integration.config.ts
# The suite imports the Cloudflare package, which resolves `emdash`
# through its published entry points rather than source.
- if: needs.change-plan.outputs.d1 == 'true'
run: pnpm run --filter emdash... build
- if: needs.change-plan.outputs.d1 == 'true'
run: pnpm --filter emdash exec vitest run --config vitest.workerd.config.ts
test-browser:
name: Browser Tests
needs: change-plan
if: needs.change-plan.outputs.browser == 'true' || needs.change-plan.outputs.e2e_playground == 'true'
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Build selected browser suites
if: needs.change-plan.outputs.browser == 'true'
env:
BROWSER_ADMIN: ${{ needs.change-plan.outputs.browser_admin }}
BROWSER_EMBEDS: ${{ needs.change-plan.outputs.browser_embeds }}
BROWSER_RELEASE: ${{ needs.change-plan.outputs.browser_release }}
run: |
filters=()
if [ "$BROWSER_ADMIN" = true ]; then filters+=(--filter "@emdash-cms/admin..."); fi
if [ "$BROWSER_EMBEDS" = true ]; then filters+=(--filter "@emdash-cms/plugin-embeds..."); fi
if [ "$BROWSER_RELEASE" = true ]; then filters+=(--filter "@emdash-cms/release-service..."); fi
if [ "${#filters[@]}" -eq 0 ]; then
echo "::error::Browser tests were selected without a browser suite"
exit 1
fi
pnpm run --if-present "${filters[@]}" build
- name: Build playground
if: needs.change-plan.outputs.e2e_playground == 'true'
run: pnpm run --filter "@emdash-cms/playground^..." build
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: playwright-${{ hashFiles('pnpm-lock.yaml') }}
- run: pnpm exec playwright install --with-deps chromium
if: steps.playwright-cache.outputs.cache-hit != 'true'
- name: Test release service in Chromium
if: needs.change-plan.outputs.browser_release == 'true'
run: pnpm --filter @emdash-cms/release-service test:browser
- name: Test admin UI
if: needs.change-plan.outputs.browser_admin == 'true'
run: pnpm run --filter @emdash-cms/admin test
- name: Test embeds in Chromium
if: needs.change-plan.outputs.browser_embeds == 'true'
run: pnpm --filter @emdash-cms/plugin-embeds test:browser
- name: Test playground
if: needs.change-plan.outputs.e2e_playground == 'true'
run: pnpm run test:e2e:playground
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: failure() && needs.change-plan.outputs.e2e_playground == 'true'
with:
name: playwright-report-playground
path: test-results/
retention-days: 7
test-e2e-rollup:
name: E2E Tests
if: false
runs-on: ubuntu-latest
steps:
- run: echo "E2E results are aggregated by the Tests job"
test-e2e:
name: E2E shard (${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
needs: change-plan
if: needs.change-plan.outputs.e2e_node == 'true' || needs.change-plan.outputs.e2e_cloudflare == 'true'
runs-on: ubuntu-latest
timeout-minutes: 40
strategy:
fail-fast: true
matrix:
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8]
shardTotal: [8]
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Build Node fixture
id: node-build
if: ${{ !cancelled() && needs.change-plan.outputs.e2e_node == 'true' }}
run: pnpm run --filter emdash... build
- name: Build Cloudflare fixture
id: cloudflare-build
if: ${{ !cancelled() && needs.change-plan.outputs.e2e_cloudflare == 'true' }}
run: pnpm run --filter "emdash-e2e-fixture-cloudflare..." build
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
if: ${{ !cancelled() && (steps.node-build.outcome == 'success' || steps.cloudflare-build.outcome == 'success') }}
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: playwright-${{ hashFiles('pnpm-lock.yaml') }}
- run: pnpm exec playwright install --with-deps chromium
if: ${{ !cancelled() && (steps.node-build.outcome == 'success' || steps.cloudflare-build.outcome == 'success') && steps.playwright-cache.outputs.cache-hit != 'true' }}
- name: Run Node E2E
id: node-e2e
if: ${{ !cancelled() && steps.node-build.outcome == 'success' }}
run: pnpm exec playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: failure() && steps.node-e2e.outcome == 'failure'
with:
name: playwright-report-node-${{ matrix.shardIndex }}
path: |
playwright-report/
test-results/
retention-days: 7
- name: Run Cloudflare E2E
id: cloudflare-e2e
if: ${{ !cancelled() && steps.cloudflare-build.outcome == 'success' }}
run: pnpm exec playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
env:
EMDASH_E2E_TARGET: cloudflare
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: failure() && steps.cloudflare-e2e.outcome == 'failure'
with:
name: playwright-report-cloudflare-${{ matrix.shardIndex }}
path: |
playwright-report/
test-results/
retention-days: 7
test-e2e-table:
name: Table E2E
needs: change-plan
if: needs.change-plan.outputs.e2e_table == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm run --filter emdash... build
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: playwright-all-browsers-${{ hashFiles('pnpm-lock.yaml') }}
- run: pnpm exec playwright install chromium firefox webkit
if: steps.playwright-cache.outputs.cache-hit != 'true'
- run: pnpm exec playwright install-deps chromium firefox webkit
- run: pnpm run test:e2e:table
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: failure()
with:
name: playwright-table
path: |
playwright-report/
test-results/
retention-days: 7