build: package the qualified precompiled QSequentialWorkflow bundle as tracked Next library assets

Track the component bundle and map compiled from the reviewed component source e6036d2 (Node 22.20.0, npm ci
--ignore-scripts, existing 3 suites passed, webpack) in dynamic-components/q-sequential-workflow/ with a sha256 lock
(scripts/dynamic-components.lock.json: source commit/tree, toolchain image digest, lockfile hash). The stage script
copies them into the export (build, dev and build:export) and refuses a missing or altered file; the pom enforcer
requires the staged bundle and map, so a release without the workflow editor fails packaging. No network, token or
component fetch is needed in the producer. The component main commit is recorded after its merge (pending in the lock).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
James MaesandClaude Sonnet 5.5 committed 2026-11-05 06:34:45 -06:00
1 parent 56ab7fe21c
commit 94726bb52c
9 files changed
+14886 -4

No files matched your search

+1
View File
@@ -0,0 +1 @@
dynamic-components/** -text
+1
View File
@@ -43,3 +43,4 @@ target/
/storybook-static/
/public/scripting-generated/
/public/pdf-generated/
/public/dynamic-qfmd-components/
File diff suppressed because it is too large. Load diff
@@ -0,0 +1 @@
{"version":3,"file":"q-sequential-workflow.bundle.js","mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;AACD,O;;;;;;;;;;;;;;ACVA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA6B;AAC7B;AACA;AACA;AACA;AACA;AACA;AACA,gBAAgB,SAAI,IAAI,SAAI;AAC5B;AACA,iDAAiD,OAAO;AACxD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,kDAAkD;AAClD;AACA;AACA;AACA,CAAC;AACyB;AAC1B,0C;;;;;;;;;;;;;;AClDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA6B;AAC7B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,gCAAgC,0CAA0C;AAC1E;AACA;AACA;AACA;AACA;AACA;AACA,4BAA4B,0BAA0B;AACtD;AACA;AACA;AACA;AACA;AACA,4BAA4B,4BAA4B;AACxD;AACA;AACA;AACA;AACA;AACA,CAAC;AACkB;AACnB,mC;;;;;;;;;;;;;;AC/DA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA6B;AAC7B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAa;AACb;AACA,yCAAyC,iBAAiB;AAC1D;AACA;AACA;AACA;AACA,CAAC;AACyB;AAC1B,0C;;;;;;;;;;;;;;;;;;ACzDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA6B;AAC7B;AACA;AACA;AACA;AACA;AACA;AACA,gBAAgB,SAAI,IAAI,SAAI;AAC5B;AACA,iDAAiD,OAAO;AACxD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,qBAAqB,SAAI,IAAI,SAAI;AACjC,6EAA6E,OAAO;AACpF;AACA;AACA;AACA;AACA;AACA;AACA;AACmD;AACE;AACH;AACJ;AAC9C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,4BAA4B,8BAA8B;AAC1D,yCAAyC,2DAAc;AACvD;AACA;AACA;AACA;AACA,4BAA4B,yGAAyG;AACrI,wDAAwD,2DAAc;AACtE;AACA;AACA,4BAA4B,uDAAY;AACxC;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6CAA6C,6DAAY;AACzD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,4BAA4B,4BAA4B;AACxD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,kEAAkE,gBAAgB;AAClF;AACA;AACA;AACA;AACA;AACA;AACA,mDAAmD,gBAAgB;AACnE;AACA;AACA;AACA;AACA;AACA;AACA,qDAAqD,gBAAgB;AACrE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS;AACT;AACA,2DAA2D,WAAW,iJAAiJ;AACvN;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;AACyB;AAC1B,0C;;;;;;;;;;;;;;AC5JA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA6B;AAC7B;AACA;AACA;AACA;AACA;AACA;AACA,gBAAgB,SAAI,IAAI,SAAI;AAC5B;AACA,iDAAiD,OAAO;AACxD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,4BAA4B,yBAAyB;AACrD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,4BAA4B,yBAAyB;AACrD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,oCAAoC,wBAAwB;AAC5D;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS;AACT,gDAAgD;AAChD;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;AACuB;AACxB,wC;;;;;;;;;;;;;;AC3GA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA6B;AAC7B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC,8CAA8C;AAC/C,6C;;;;;;;;;;;;;;AC9CA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA6B;AAC7B;AACA;AACA;AACA;AACA;AACA;AACA,qBAAqB,SAAI,IAAI,SAAI;AACjC,6EAA6E,OAAO;AACpF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;AAC0B;AAC3B,2C;;;;;;;;;;;;;;AClDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA6B;AAC7B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;AACyB;AAC1B,0C;;;;;;;;;;;;;;;;ACvCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA6B;AAC7B;AACA;AACA;AACA;AACA;AACA;AACoD;AACF;AAClD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,0CAA0C;AAC1C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAwB,6EAA6E;AACrG;AACA,8BAA8B,6DAAe;AAC7C;AACA,wBAAwB,6EAA6E;AACrG;AACA,8BAA8B,2DAAc;AAC5C;AACA,wBAAwB,+EAA+E;AACvG;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iDAAiD,gBAAgB;AACjE;AACA;AACA;AACA;AACA;AACA;AACA,iDAAiD,gBAAgB;AACjE;AACA;AACA;AACA;AACA;AACA;AACA,mDAAmD,gBAAgB;AACnE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;AACuB;AACxB,wC;;;;;;;;;;;;;;;;;;ACjHA;AACgG;AACjB;AAC/E,8BAA8B,sEAA2B,CAAC,+EAAqC;AAC/F;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACALine truncated
+4 -3
View File
@@ -6,9 +6,10 @@
"scripts": {
"scripting:worker": "node scripts/build-scripting-worker.mjs",
"scripting:worker:check": "node scripts/build-scripting-worker.mjs && node scripts/check-scripting-worker.mjs",
"dev": "node scripts/build-scripting-worker.mjs && node scripts/build-pdf-worker.mjs && next dev --turbopack",
"build": "node scripts/build-scripting-worker.mjs && node scripts/build-pdf-worker.mjs && next build",
"build:export": "node scripts/build-scripting-worker.mjs && node scripts/build-pdf-worker.mjs && node scripts/build-export.mjs",
"dev": "node scripts/build-scripting-worker.mjs && node scripts/build-pdf-worker.mjs && node scripts/stage-dynamic-components.mjs && next dev --turbopack",
"build": "node scripts/build-scripting-worker.mjs && node scripts/build-pdf-worker.mjs && node scripts/stage-dynamic-components.mjs && next build",
"build:export": "node scripts/build-scripting-worker.mjs && node scripts/build-pdf-worker.mjs && node scripts/stage-dynamic-components.mjs && node scripts/build-export.mjs",
"workflow:bundle": "node scripts/stage-dynamic-components.mjs",
"build:jar": "pnpm build:export && mvn -B -q clean package",
"start": "next start",
"lint": "eslint src && node scripts/check-license-headers.mjs",
+3 -1
View File
@@ -89,8 +89,10 @@
<files>
<file>${project.basedir}/out/index.html</file>
<file>${project.basedir}/out/app/_/_/index.html</file>
<file>${project.basedir}/out/dynamic-qfmd-components/q-sequential-workflow.bundle.js</file>
<file>${project.basedir}/out/dynamic-qfmd-components/q-sequential-workflow.bundle.js.map</file>
</files>
<message>Run "QQQ_NEXT_OUTPUT=export pnpm build" (or pnpm build:jar) before packaging.</message>
<message>Run pnpm build:export (or pnpm build:jar) before packaging; the export must contain the QSequentialWorkflow editor bundle staged from dynamic-components/.</message>
</requireFilesExist>
</rules>
</configuration>
+22
View File
@@ -0,0 +1,22 @@
{
"component": {
"repository": "QRun-IO/qqq-frontend-component-sequential-workflow",
"sourceCommit": "e6036d2a474a90d80c72bc4c5f242e666b3415f6",
"sourceTree": "9efc09210a81dc4121aa53e156443b7f4fcb6dd7",
"mainCommit": null,
"pending": "DRAFT: the component PR (reviewed source commit above) is not merged yet; record the component main commit after the merge and keep these hashes only if the merged tree is identical, otherwise requalify and replace the assets.",
"build": {
"node": "v22.20.0",
"npm": "10.9.3",
"image": "node@sha256:b21fe589dfbe5cc39365d0544b9be3f1f33f55f3c86c87a76ff65a02f8f5848e",
"commands": ["npm ci --ignore-scripts", "npm test (existing 3 suites, 62 tests)", "npx webpack"],
"packageLockSha256": "737cc1a3e416d942c050076dfae36345e9ba3f62d679dec71f04c2a2382b1a21",
"qualifiedUtc": "2026-11-05T12:31Z"
}
},
"assetDirectory": "dynamic-components/q-sequential-workflow",
"files": {
"q-sequential-workflow.bundle.js": "9f8358b105d0c157cb32a476313a7750281549f8e9423dd7d125ceaaafe8f32d",
"q-sequential-workflow.bundle.js.map": "776c716690a357213e00d109652ab7eb9c9025073b362c995564e11ca5c5ee92"
}
}
+45
View File
@@ -0,0 +1,45 @@
/*
* Copyright 2026 QRun.IO, Inc.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0
*/
/**
* @file Stage the versioned, precompiled QSequentialWorkflow editor bundle (the qbit-workflows `QSequentialWorkflow` editor)
* under `public/dynamic-qfmd-components/`, which is where qbit-workflows' widget metadata points
* (`/dynamic-qfmd-components/q-sequential-workflow.bundle.js`) and where the single-origin host serves the UI from.
* The bundle and its map are tracked library assets in `dynamic-components/q-sequential-workflow/`, compiled from the
* canonical component repository (source commit and toolchain recorded in `dynamic-components.lock.json`). Nothing is
* downloaded or built here, and a file is refused unless its sha256 matches the lock. A missing or altered bundle fails the
* build, so a release can never silently omit the workflow editor.
*/
import { createHash } from 'node:crypto'
import { copyFile, mkdir, readFile } from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const here = path.dirname(fileURLToPath(import.meta.url))
const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex')
/** Copies every locked file from `assetDir` to `outDir`; throws on a missing file or a hash mismatch. */
export async function stageDynamicComponents({ assetDir, lock, outDir }) {
await mkdir(outDir, { recursive: true })
const staged = []
for (const [name, expected] of Object.entries(lock.files)) {
if (path.basename(name) !== name) throw new Error(`Locked file name must be a plain file name: ${name}`)
if (!/^[0-9a-f]{64}$/.test(expected ?? '')) throw new Error(`${name} has no sha256 in the dynamic component lock`)
const source = path.join(assetDir, name)
const actual = sha256(await readFile(source).catch(() => { throw new Error(`Missing ${name} in ${assetDir}`) }))
if (actual !== expected) throw new Error(`${name} sha256 ${actual} does not match the lock ${expected}`)
await copyFile(source, path.join(outDir, name))
staged.push(name)
}
return staged
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const lock = JSON.parse(await readFile(path.join(here, 'dynamic-components.lock.json'), 'utf8'))
const staged = await stageDynamicComponents({ assetDir: path.join(here, '..', lock.assetDirectory), lock, outDir: path.join(here, '../public/dynamic-qfmd-components') })
console.log(`Staged dynamic components compiled from ${lock.component.repository}@${lock.component.sourceCommit.slice(0, 8)}: ${staged.join(', ')}`)
}
+43
View File
@@ -0,0 +1,43 @@
/*
* Copyright 2026 QRun.IO, Inc.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0
*/
/** @file The tracked workflow-editor bundle is staged only when its bytes match the committed lock. */
import { createHash } from 'node:crypto'
import { mkdtemp, readFile, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { describe, expect, it } from 'vitest'
import { stageDynamicComponents } from '../../scripts/stage-dynamic-components.mjs'
const bytes = Buffer.from('window.QSequentialWorkflow={QSequentialWorkflow:function(){}}')
const sha = (data: Buffer) => createHash('sha256').update(data).digest('hex')
const lock = { files: { 'q-sequential-workflow.bundle.js': sha(bytes) } }
describe('stageDynamicComponents', () => {
it('copies a bundle whose sha256 matches the lock to the served path name', async () => {
const assets = await mkdtemp(path.join(tmpdir(), 'dyn-assets-')); const out = await mkdtemp(path.join(tmpdir(), 'dyn-out-'))
await writeFile(path.join(assets, 'q-sequential-workflow.bundle.js'), bytes)
expect(await stageDynamicComponents({ assetDir: assets, lock, outDir: out })).toEqual(['q-sequential-workflow.bundle.js'])
expect(await readFile(path.join(out, 'q-sequential-workflow.bundle.js'))).toEqual(bytes)
})
it('refuses a bundle that differs from the lock, a missing one and a lock without a hash', async () => {
const assets = await mkdtemp(path.join(tmpdir(), 'dyn-assets-')); const out = await mkdtemp(path.join(tmpdir(), 'dyn-out-'))
await expect(stageDynamicComponents({ assetDir: assets, lock, outDir: out })).rejects.toThrow(/Missing/)
await writeFile(path.join(assets, 'q-sequential-workflow.bundle.js'), 'tampered')
await expect(stageDynamicComponents({ assetDir: assets, lock, outDir: out })).rejects.toThrow(/does not match the lock/)
await expect(stageDynamicComponents({ assetDir: assets, lock: { files: { 'q-sequential-workflow.bundle.js': null } }, outDir: out })).rejects.toThrow(/no sha256/)
})
it('the tracked canonical assets match the committed lock', async () => {
const real = JSON.parse(await readFile(path.join(process.cwd(), 'scripts/dynamic-components.lock.json'), 'utf8'))
expect(real.component.sourceCommit).toMatch(/^[0-9a-f]{40}$/)
for (const [name, hash] of Object.entries(real.files)) {
expect(hash).toMatch(/^[0-9a-f]{64}$/)
expect(sha(await readFile(path.join(process.cwd(), real.assetDirectory, name)))).toBe(hash)
}
})
})