From 07ec71bbe1812e971ec9b7badadd2410fbbb0643 Mon Sep 17 00:00:00 2001 From: Lan_zhijiang Date: Thu, 13 Aug 2026 16:42:00 +0800 Subject: [PATCH 1/3] fix(preview): decouple database contract gate --- .github/workflows/ci.yml | 41 ++++++++++++++++++++---- .github/workflows/pages-preview.yml | 32 ++++++++++++++++-- scripts/check-local-runtime-contract.mjs | 2 +- scripts/twitter-mf-distribution.test.mjs | 6 ++++ 4 files changed, 70 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9555400..91e3721 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -105,8 +105,8 @@ jobs: fail-on-severity: high fail-on-scopes: development, runtime - workspace: - name: Workspace contract + database-contract: + name: Database contract needs: core-release runs-on: ubuntu-latest steps: @@ -159,6 +159,38 @@ jobs: packages/core/src/database/database.generated.ts packages/core/src/database/runtime-contract.generated.json + - name: Reject a moved stable release + run: >- + node scripts/resolve-core-release.mjs + --expect "${{ needs.core-release.outputs.image }}" + + workspace: + name: Workspace contract + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + + - name: Install the pinned pnpm release + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + with: + version: 11.11.0 + + - name: Set up Node + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version-file: package.json + cache: pnpm + registry-url: https://npm.pkg.github.com + scope: '@inkcre' + + - name: Install the frozen workspace + run: pnpm install --frozen-lockfile + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Verify static, typed, and package contracts run: pnpm check @@ -203,11 +235,6 @@ jobs: echo "- Twitter MF snapshot digest: \`${TWITTER_MF_ARTIFACT_DIGEST}\`" } >> "$GITHUB_STEP_SUMMARY" - - name: Reject a moved stable release - run: >- - node scripts/resolve-core-release.mjs - --expect "${{ needs.core-release.outputs.image }}" - - name: Upload unit-test evidence on failure if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/.github/workflows/pages-preview.yml b/.github/workflows/pages-preview.yml index 420deb7..a083071 100644 --- a/.github/workflows/pages-preview.yml +++ b/.github/workflows/pages-preview.yml @@ -16,7 +16,6 @@ jobs: identity: name: Resolve trusted preview identity if: >- - github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.head_repository.full_name == github.repository runs-on: ubuntu-latest @@ -41,8 +40,8 @@ jobs: core.setFailed('Preview artifact must come from the trusted Client checks workflow.') return } - if (run.conclusion !== 'success' || run.event !== 'pull_request') { - core.setFailed('Preview delivery requires a successful pull-request check run.') + if (run.status !== 'completed' || run.event !== 'pull_request') { + core.setFailed('Preview delivery requires a completed pull-request check run.') return } if (run.head_repository?.full_name !== `${context.repo.owner}/${context.repo.repo}`) { @@ -50,6 +49,33 @@ jobs: return } + const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, { + owner: context.repo.owner, + repo: context.repo.repo, + run_id: runId, + filter: 'latest', + per_page: 100, + }) + const workspace = jobs.find((job) => job.name === 'Workspace contract') + if (!workspace || workspace.status !== 'completed' || workspace.conclusion !== 'success') { + core.setFailed('Preview delivery requires the Workspace contract job to pass.') + return + } + + const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, { + owner: context.repo.owner, + repo: context.repo.repo, + run_id: runId, + per_page: 100, + }) + const previewArtifact = artifacts.find( + (artifact) => artifact.name === 'client-web-dist' && !artifact.expired + ) + if (!previewArtifact) { + core.setFailed('The successful Workspace contract job produced no live preview artifact.') + return + } + const pulls = await github.rest.repos.listPullRequestsAssociatedWithCommit({ owner: context.repo.owner, repo: context.repo.repo, diff --git a/scripts/check-local-runtime-contract.mjs b/scripts/check-local-runtime-contract.mjs index 84efdb1..bb75202 100644 --- a/scripts/check-local-runtime-contract.mjs +++ b/scripts/check-local-runtime-contract.mjs @@ -51,7 +51,7 @@ if (await exists('.node-version')) { errors.push('.node-version must remain absent; pnpm devEngines.runtime owns Node') } for (const [path, expectedNodeFiles] of [ - ['.github/workflows/ci.yml', Array(5).fill('package.json')], + ['.github/workflows/ci.yml', Array(6).fill('package.json')], ['.github/workflows/pages-cleanup.yml', ['controller/package.json']], ['.github/workflows/pages-preview.yml', ['controller/package.json']], ['.github/workflows/pages-deploy.yml', ['controller/package.json']], diff --git a/scripts/twitter-mf-distribution.test.mjs b/scripts/twitter-mf-distribution.test.mjs index 2b46bce..62ad651 100644 --- a/scripts/twitter-mf-distribution.test.mjs +++ b/scripts/twitter-mf-distribution.test.mjs @@ -229,6 +229,8 @@ test('retains exact-main checked-artifact governance without generic target deli ]) assert.match(ci, /push:\n\s+branches:\n\s+- main/) + assert.match(ci, /database-contract:\n\s+name: Database contract\n\s+needs: core-release/) + assert.match(ci, /workspace:\n\s+name: Workspace contract\n\s+runs-on:/) assert.match(ci, /name: twitter-mf-dist/) assert.match(ci, /verify-twitter-mf-distribution\.mjs inspect-local/) assert.doesNotMatch(ci, /INKCRE_EXTENSION_REGISTRY_TOKEN/) @@ -260,6 +262,10 @@ test('retains exact-main checked-artifact governance without generic target deli assert.match(delivery, /verify-twitter-mf-distribution\.mjs verify-public/) assert.match(delivery, /INKCRE_EXTENSION_REGISTRY_TOKEN/) assert.doesNotMatch(delivery, /target-publish|build-target|publish-target|target_digest/) + assert.doesNotMatch(preview, /workflow_run\.conclusion == 'success'/) + assert.match(preview, /job\.name === 'Workspace contract'/) + assert.match(preview, /workspace\.conclusion !== 'success'/) + assert.match(preview, /artifact\.name === 'client-web-dist' && !artifact\.expired/) assert.doesNotMatch(preview, /INKCRE_EXTENSION_REGISTRY_TOKEN/) const nativePublish = delivery.indexOf('Prepare the exact native Twitter Release') From b3fb637ae5fd7237c168d2259517659307271423 Mon Sep 17 00:00:00 2001 From: Lan_zhijiang Date: Thu, 13 Aug 2026 16:47:21 +0800 Subject: [PATCH 2/3] chore(database): sync stable core evidence --- packages/core/src/database/runtime-contract.generated.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/database/runtime-contract.generated.json b/packages/core/src/database/runtime-contract.generated.json index 5bd5f9e..ef39550 100644 --- a/packages/core/src/database/runtime-contract.generated.json +++ b/packages/core/src/database/runtime-contract.generated.json @@ -703,5 +703,5 @@ "schema": "inkcre" }, "revision": "peer-database-runtime-v2", - "source_revision": "34914b6deed4768237508fdb02e033655feb8073" + "source_revision": "63f57b26ed8685fa34a74516ade39e2af72218d9" } From 4c86dcbb40a57e9bbe8bbd9dc3a51f4ddbc41abb Mon Sep 17 00:00:00 2001 From: Lan_zhijiang Date: Thu, 13 Aug 2026 17:56:08 +0800 Subject: [PATCH 3/3] fix(preview): serve native twitter distribution --- .github/workflows/ci.yml | 16 +++ .github/workflows/pages-preview.yml | 44 ++++++- scripts/assemble-twitter-preview.mjs | 145 +++++++++++++++++++++ scripts/twitter-mf-distribution.test.mjs | 62 ++++++++- scripts/verify-twitter-mf-distribution.mjs | 17 +++ 5 files changed, 277 insertions(+), 7 deletions(-) create mode 100644 scripts/assemble-twitter-preview.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 91e3721..c009254 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -213,6 +213,14 @@ jobs: --core-package packages/core/package.json --artifact-directory extensions/twitter/dist/client-web + - name: Build the read-only Twitter preview Release + run: >- + node scripts/verify-twitter-mf-distribution.mjs preview-release + --package extensions/twitter/package.json + --core-package packages/core/package.json + --artifact-directory extensions/twitter/dist/client-web + --output .preview-release/twitter.json + - name: Upload the exact Twitter Module Federation snapshot id: twitter-mf-artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -222,6 +230,14 @@ jobs: if-no-files-found: error retention-days: 7 + - name: Upload the exact Twitter preview Release + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: twitter-mf-preview-release + path: .preview-release/twitter.json + if-no-files-found: error + retention-days: 7 + - name: Record checked artifact provenance env: CLIENT_WEB_ARTIFACT_DIGEST: ${{ steps.client-web-artifact.outputs['artifact-digest'] }} diff --git a/.github/workflows/pages-preview.yml b/.github/workflows/pages-preview.yml index a083071..3fe46ac 100644 --- a/.github/workflows/pages-preview.yml +++ b/.github/workflows/pages-preview.yml @@ -68,12 +68,20 @@ jobs: run_id: runId, per_page: 100, }) - const previewArtifact = artifacts.find( - (artifact) => artifact.name === 'client-web-dist' && !artifact.expired - ) - if (!previewArtifact) { - core.setFailed('The successful Workspace contract job produced no live preview artifact.') - return + for (const artifactName of [ + 'client-web-dist', + 'twitter-mf-dist', + 'twitter-mf-preview-release', + ]) { + const artifact = artifacts.find( + (candidate) => candidate.name === artifactName && !candidate.expired + ) + if (!artifact) { + core.setFailed( + `The successful Workspace contract job produced no live ${artifactName} artifact.` + ) + return + } } const pulls = await github.rest.repos.listPullRequestsAssociatedWithCommit({ @@ -132,6 +140,30 @@ jobs: run-id: ${{ needs.identity.outputs.run_id }} github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Download the exact checked Twitter snapshot + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: twitter-mf-dist + path: .pages-preview/twitter-mf + run-id: ${{ needs.identity.outputs.run_id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Download the exact checked Twitter preview Release + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: twitter-mf-preview-release + path: .pages-preview/twitter-release + run-id: ${{ needs.identity.outputs.run_id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Assemble the read-only native Extension preview + run: >- + node controller/scripts/assemble-twitter-preview.mjs + --pages-directory .pages-preview/dist + --snapshot-directory .pages-preview/twitter-mf + --release .pages-preview/twitter-release/twitter.json + --public-origin https://preview-client-web-pr-${{ needs.identity.outputs.pull_number }}.${{ vars.CLOUDFLARE_PAGES_PROJECT }}.pages.dev + - name: Reverify pull-request identity before delivery uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: diff --git a/scripts/assemble-twitter-preview.mjs b/scripts/assemble-twitter-preview.mjs new file mode 100644 index 0000000..e2f088f --- /dev/null +++ b/scripts/assemble-twitter-preview.mjs @@ -0,0 +1,145 @@ +import assert from 'node:assert/strict' +import { cp, lstat, mkdir, readFile, writeFile } from 'node:fs/promises' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const EXTENSION_NAME = 'inkcre/twitter' +const VERSION_PATTERN = + /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/ + +async function readJson(filePath) { + return JSON.parse(await readFile(filePath, 'utf8')) +} + +function parseArguments(argumentList) { + const options = {} + for (let index = 0; index < argumentList.length; index += 2) { + const option = argumentList[index] + const value = argumentList[index + 1] + assert.ok(option?.startsWith('--') && value, `expected option/value near ${option ?? ''}`) + options[option.slice(2)] = value + } + return options +} + +function requireOption(options, name) { + assert.ok(options[name], `--${name} is required`) + return options[name] +} + +export async function assembleTwitterPreview({ + pagesDirectory, + snapshotDirectory, + releasePath, + publicOrigin, +}) { + const [release, manifest, pagesIndex, snapshotStats] = await Promise.all([ + readJson(releasePath), + readJson(path.join(snapshotDirectory, 'mf-manifest.json')), + lstat(path.join(pagesDirectory, 'index.html')), + lstat(snapshotDirectory), + ]) + + assert.ok(pagesIndex.isFile(), 'Pages preview artifact must contain index.html') + assert.ok(snapshotStats.isDirectory(), 'Twitter snapshot must be a directory') + assert.equal(release.name, EXTENSION_NAME, 'preview Release must be inkcre/twitter') + assert.equal(typeof release.nickname, 'string', 'preview Release nickname is required') + assert.match(release.version, VERSION_PATTERN, 'preview Release version must be strict SemVer') + assert.equal(release.state, 'published', 'preview Release must be published') + assert.deepEqual(release.python, undefined, 'Web preview must not claim a Python Distribution') + assert.deepEqual( + release.module_federation, + { + manifest_url: `/extensions/${EXTENSION_NAME}/${release.version}/module-federation/mf-manifest.json`, + host_sdk: '@inkcre/core', + host_sdk_version: release.module_federation?.host_sdk_version, + }, + 'preview Release must contain exactly one native Web association' + ) + assert.equal( + typeof release.module_federation.host_sdk_version, + 'string', + 'preview Web Host SDK range is required' + ) + assert.equal(manifest.name, 'extension.twitter', 'unexpected Module Federation Remote name') + assert.equal(manifest.metaData?.publicPath, './', "preview manifest publicPath must be './'") + assert.equal( + manifest.metaData?.remoteEntry?.name, + 'remoteEntry.js', + 'preview manifest remote entry must be remoteEntry.js' + ) + const canonicalOrigin = new URL(publicOrigin) + assert.equal(canonicalOrigin.protocol, 'https:', 'preview public origin must use HTTPS') + assert.equal(canonicalOrigin.pathname, '/', 'preview public origin must not contain a path') + assert.equal(canonicalOrigin.search, '', 'preview public origin must not contain a query') + assert.equal(canonicalOrigin.hash, '', 'preview public origin must not contain a fragment') + + const releaseFile = path.join( + pagesDirectory, + 'v1', + 'extensions', + 'inkcre', + 'twitter', + 'releases', + release.version + ) + const distributionDirectory = path.join( + pagesDirectory, + 'extensions', + 'inkcre', + 'twitter', + release.version, + 'module-federation' + ) + await Promise.all([ + mkdir(path.dirname(releaseFile), { recursive: true }), + mkdir(distributionDirectory, { recursive: true }), + ]) + await cp(snapshotDirectory, distributionDirectory, { recursive: true, force: false }) + manifest.metaData.publicPath = new URL( + `/extensions/${EXTENSION_NAME}/${release.version}/module-federation/`, + canonicalOrigin + ).href + await writeFile( + path.join(distributionDirectory, 'mf-manifest.json'), + `${JSON.stringify(manifest, null, 2)}\n` + ) + await writeFile(releaseFile, `${JSON.stringify(release, null, 2)}\n`) + + const headersPath = path.join(pagesDirectory, '_headers') + const existingHeaders = await readFile(headersPath, 'utf8').catch(() => '') + const previewHeaders = [ + '/v1/extensions/*', + ' Access-Control-Allow-Origin: *', + ' Cache-Control: no-store', + ' Content-Type: application/json; charset=utf-8', + '/extensions/*', + ' Access-Control-Allow-Origin: *', + ' Cache-Control: no-store', + '', + ].join('\n') + await writeFile( + headersPath, + `${existingHeaders.trim()}${existingHeaders.trim() ? '\n' : ''}${previewHeaders}` + ) + + return { name: release.name, version: release.version, releaseFile, distributionDirectory } +} + +async function main() { + const options = parseArguments(process.argv.slice(2)) + await assembleTwitterPreview({ + pagesDirectory: requireOption(options, 'pages-directory'), + snapshotDirectory: requireOption(options, 'snapshot-directory'), + releasePath: requireOption(options, 'release'), + publicOrigin: requireOption(options, 'public-origin'), + }) +} + +const currentFile = fileURLToPath(import.meta.url) +if (process.argv[1] && path.resolve(process.argv[1]) === currentFile) { + main().catch((error) => { + console.error(`[ERROR] ${error.message}`) + process.exitCode = 1 + }) +} diff --git a/scripts/twitter-mf-distribution.test.mjs b/scripts/twitter-mf-distribution.test.mjs index 62ad651..40e5221 100644 --- a/scripts/twitter-mf-distribution.test.mjs +++ b/scripts/twitter-mf-distribution.test.mjs @@ -15,8 +15,10 @@ import { import { inspectNativeModuleFederation, prepareBody, + previewRelease, verifyPublicModuleFederation, } from './verify-twitter-mf-distribution.mjs' +import { assembleTwitterPreview } from './assemble-twitter-preview.mjs' const repoRoot = fileURLToPath(new URL('..', import.meta.url)) @@ -160,6 +162,58 @@ test('builds the frozen native prepare payload with distribution-owned provenanc } }) +test('builds and assembles a read-only native Twitter preview projection', async () => { + const fixture = await createFixture() + const pagesDirectory = path.join(fixture.root, 'pages') + const releasePath = path.join(fixture.root, 'release.json') + try { + await mkdir(pagesDirectory) + await writeFile(path.join(pagesDirectory, 'index.html'), '\n') + const release = previewRelease(await inspectNativeModuleFederation(fixture)) + await writeFile(releasePath, `${JSON.stringify(release)}\n`) + await assembleTwitterPreview({ + pagesDirectory, + snapshotDirectory: fixture.artifactDirectory, + releasePath, + publicOrigin: 'https://preview.example/', + }) + + const publicRelease = JSON.parse( + await readFile( + path.join(pagesDirectory, 'v1/extensions/inkcre/twitter/releases/0.1.1'), + 'utf8' + ) + ) + assert.deepEqual(publicRelease, release) + assert.equal( + await readFile( + path.join( + pagesDirectory, + 'extensions/inkcre/twitter/0.1.1/module-federation/remoteEntry.js' + ), + 'utf8' + ), + fixture.files.get('remoteEntry.js').toString() + ) + const publicManifest = JSON.parse( + await readFile( + path.join( + pagesDirectory, + 'extensions/inkcre/twitter/0.1.1/module-federation/mf-manifest.json' + ), + 'utf8' + ) + ) + assert.equal( + publicManifest.metaData.publicPath, + 'https://preview.example/extensions/inkcre/twitter/0.1.1/module-federation/' + ) + assert.match(await readFile(path.join(pagesDirectory, '_headers'), 'utf8'), /no-store/) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } +}) + test('rejects a native manifest whose referenced closure is missing', async () => { const fixture = await createFixture() try { @@ -232,6 +286,8 @@ test('retains exact-main checked-artifact governance without generic target deli assert.match(ci, /database-contract:\n\s+name: Database contract\n\s+needs: core-release/) assert.match(ci, /workspace:\n\s+name: Workspace contract\n\s+runs-on:/) assert.match(ci, /name: twitter-mf-dist/) + assert.match(ci, /name: twitter-mf-preview-release/) + assert.match(ci, /verify-twitter-mf-distribution\.mjs preview-release/) assert.match(ci, /verify-twitter-mf-distribution\.mjs inspect-local/) assert.doesNotMatch(ci, /INKCRE_EXTENSION_REGISTRY_TOKEN/) @@ -265,7 +321,11 @@ test('retains exact-main checked-artifact governance without generic target deli assert.doesNotMatch(preview, /workflow_run\.conclusion == 'success'/) assert.match(preview, /job\.name === 'Workspace contract'/) assert.match(preview, /workspace\.conclusion !== 'success'/) - assert.match(preview, /artifact\.name === 'client-web-dist' && !artifact\.expired/) + assert.match(preview, /candidate\.name === artifactName && !candidate\.expired/) + assert.match(preview, /'twitter-mf-dist'/) + assert.match(preview, /'twitter-mf-preview-release'/) + assert.match(preview, /assemble-twitter-preview\.mjs/) + assert.match(preview, /--public-origin https:\/\/preview-client-web-pr-/) assert.doesNotMatch(preview, /INKCRE_EXTENSION_REGISTRY_TOKEN/) const nativePublish = delivery.indexOf('Prepare the exact native Twitter Release') diff --git a/scripts/verify-twitter-mf-distribution.mjs b/scripts/verify-twitter-mf-distribution.mjs index 768eb06..0dbd497 100644 --- a/scripts/verify-twitter-mf-distribution.mjs +++ b/scripts/verify-twitter-mf-distribution.mjs @@ -284,6 +284,19 @@ export function prepareBody(local, provenance) { } } +export function previewRelease(local) { + return { + name: local.name, + nickname: local.nickname, + version: local.version, + state: 'published', + module_federation: { + manifest_url: `/extensions/${local.name}/${local.version}/module-federation/mf-manifest.json`, + ...local.module_federation, + }, + } +} + function parseArguments(argumentList) { const [command, ...remaining] = argumentList assert.ok(command, 'a command is required') @@ -338,6 +351,10 @@ async function main() { ) return } + if (command === 'preview-release') { + await writeResult(options.output, previewRelease(await inspectNativeModuleFederation(shared))) + return + } if (command === 'verify-public') { const result = await verifyPublicModuleFederation({ ...shared,