Repository navigation
fix(publish): flatten the composed artifact before posting it to the marketplace (#2053) - #2054
Merged
Merged
Conversation
…marketplace objectstack build writes a composed artifact since the packaging: every collection sits inside packages[], none at the top level. The marketplace ingest reads the flat 3.x shape, so v4.0.0 was stored as an app with no objects while the version POST answered 201. publish-marketplace now builds the flat bundle from the composed artifact with the platform's own pieces (resolveArtifactPackageOrder for package order, COMPOSE_KEY_DISPOSITIONS for the collection keys, the registry's navigation-contribution fold for the module's nav entries), refuses what a one-package bundle cannot carry, and refuses before any HTTP call, DRY_RUN included, a bundle missing any object the build carries. Claude-Session: https://claude.ai/code/session_012zh91QzFgePbkmuHnugLN3 Co-authored-by: Claude <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
This was referenced Oct 10, 2026
os-zhuang
pushed a commit
that referenced
this pull request
Oct 10, 2026
`pnpm changeset:version` consumed the 2 pending changesets (2 patch: 2051-inline-logo, 2053-publish-flat-bundle) and computed 4.0.0 -> 4.0.1: it wrote `package.json` and spliced the 4.0.1 section into `CHANGELOG.md` (+16 lines; every byte from `## 4.0.0` down unchanged). Each entry is stamped with the main commit that added its changeset: 4547858 (#2055) and d7decd4 (#2054). Claude-Session: https://claude.ai/code/session_012zh91QzFgePbkmuHnugLN3 Co-authored-by: Claude <noreply@anthropic.com>
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #2053
Clause-②: no
This PR is the flatten and the guard. The 4.0.1 version cut is a second PR after this one lands, so that release can also carry #2051. That is why this PR does not close the card.
Before: the wrong result at the public door (measured on
6162c43f)pnpm buildwritesdist/objectstack.json(2,449,818 B on disk). Its top level is{ manifest, packages, i18n, docs }:manifest: 9 keys, the app's identity (id app.objectstack.hotcrm,namespace,defaultDatasource,version,type,scope,name,description,engines).packages: 2 entries, each{ manifest: BODY }. Every collection lives in a body:app.objectstack.hotcrm.service(typemodule, 25 keys): objects 3, data 2, views 2, pages 1, dashboards 1, reports 3, datasets 1, actions 8, flows 7, emailTemplates 8, sharingRules 3, skills 1, hooks 9, requires 4, navigationContributions 3, dependencies on the app.app.objectstack.hotcrm(typeapp, 28 keys): objects 15, data 18, translations 1, apps 1, views 12, pages 7, dashboards 4, reports 7, datasets 8, actions 23, flows 25, emailTemplates 68, positions 12, permissions 7, sharingRules 7, skills 5, hooks 35, mappings 3, requires 8.i18n: 3 keys.docs: 4.scripts/publish-marketplace.mjsread that file intobundleand posted it unchanged as{ version, bundle, release_notes, auto_approve }toPOST /api/v1/cloud/packages/:id/versions. RunningDRY_RUN=1 node scripts/publish-marketplace.mjson6162c43fexits 0 and prints✓ published version 4.0.0. The bundle it would post has top-level keysmanifest, packages, i18n, docs,bundle.objectsis absent, and the 18 objects (3 + 15) exist only underpackages[]. Cloud's ingest reads the flat 3.x shape, so it stored an app with no objects.The reference: 3.1.0. The
v3.1.0release asset'sdist/objectstack.json(2,012,194 B) has 21 flat top-level keys:manifest(the same 9 identity keys),translations1,i18n,objects18,apps1,views14,pages8,dashboards5,reports10,datasets9,actions31,flows29,positions12,permissions7,sharingRules10,skills6,hooks39,mappings3,data19,requires8,docs4.The card assumed some things that the measurement changed:
3.1.0 did not post
id/name/version/type/category/descriptionat the top level. It posted amanifestobject with those keys. The flat top-level fields in cloud's stored row are cloud's own projection.categorycomes fromobjectstack.manifest.jsonthrough the package upsert, not from the bundle. So the flat bundle keeps the app'smanifestobject, exactly as 3.1.0 did."De-duplicate by
name, orid" would refuse the real artifact. Some names repeat legitimately:log_call,log_meetingandschedule_meetingeach exist on 5 objects, 4 of them inside the app package alone.The flatten therefore keys an item the way the platform does. An action is keyed by
objectName+name, as incollectComposedActionKeyCollisions(@objectstack/spec). An email template is keyed byname+locale, as inITEM_KEY_DISCRIMINATORS(@objectstack/metadata-core).Some items have no
nameorid: view containers, seed datasets, translation bundles andrequiresstrings. These are only de-duplicated when two are exactly equal, becausedatalegitimately carries two datasets each forcrm_event_attendeeandcrm_campaign_member.The module's
navigationContributionsis a manifest key, not a collection. A one-package bundle has no other package for it to contribute from. Dropping it would remove Cases, Knowledge, Service Overview, My Cases and SLA Performance from a hosted install's navigation. The flatten folds it into the app's navigation (see below).The fix
All of the work lives in a new module,
scripts/lib/marketplace-bundle.mjs, which the script imports. Tests import the same module, so they make no network call, and the CLI behaves as before apart from the new summary line and the refusal.The flatten (
flattenArtifact) replays the platform's own composition and adds no merge rule of its own:resolveArtifactPackageOrder(@objectstack/core), the order the runtime registers packages in: the app first, then the module that depends on it. Seed rows therefore replay in the order a packaged boot replays them. In the artifact itself,packages[]is[service, app].COMPOSE_KEY_DISPOSITIONS(@objectstack/spec) composes byconcatorobjects, concatenated in that order. Beforemanifest: 'preserve'moved this list into the package bodies,composeStacksbuilt exactly this list at the top level. It only moves the list after the bodies reproduce it item for item. Exact duplicates are kept once, which only changesrequires(12 down to 8).apps[].navigationbySchemaRegistry.applyNavContributions(@objectstack/objectql), the fold the runtime applies when it reads an app. The bundle carries nonavigationContributionsafterwards, so nothing applies it twice.i18n,docsandtranslations, merged across levels. An array is concatenated. A single value must be equal at every level that carries it, or the flatten refuses.BundleError, naming the item (never dropped):namespace);packagesarray) passes through unchanged.The guard (
assertBundleCarriesEveryObject) refuses a bundle with 0objects, or with fewer objects than the artifact carries across all its packages. It runs right after the file is read, before the first HTTP call, inDRY_RUNtoo.Why this is the minimal fix. It follows the maintainer's ruling: flatten at publish, in this script, and leave cloud unchanged. Every rule about order, which keys are collections, and how navigation folds is imported from the platform the runtime uses, not re-derived here. The only rule this repo adds is which items share an identity, and that rule exists only to refuse.
After (measured on
6162c43fplus this change)DRY_RUN=1 pnpm publish:marketplaceexits 0 and prints:Key set compared with 3.1.0:
emailTemplates(76). It was authored after 3.1.0 by feat(flows): notify nodes speak the recipient's own language #1949, so notify nodes write in the recipient's language. The 3.x shape never had it. Whether cloud stores it is for the post-tag acceptance to read fromsys_package_version.manifest_json.manifest: the same 9 keys as 3.1.0.packages, the composed envelope.dependencies, which only point at the app inside this artifact.navigationContributions, now folded into the app's navigation:nav_case,nav_knowledgeandnav_service_dashboardundergroup_service,nav_my_casesundergroup_work, andnav_report_slaundergroup_insights.Counts compared with 3.1.0. Every flat count equals the sum across the two packages, except
requires. So the differences below come from authoring since 3.1.0, not from the flatten:Every other key matches 3.1.0.
Size (compact JSON, the form the POST serialises):
Tests
test/marketplace-bundle.test.tshas 11 cases. Most use a two-package fixture,test/fixtures/composed-artifact.json, which is schema-valid because the platform's ordering step parses it. Two run on the composition HotCRM actually builds:manifest, and nodependenciessurvive.navigationContributionskey remains.ex_accountin the module throwsBundleErrornaming'ex_account'. A modulenamespacethat differs from the app's is refused the same way.BundleError;objectsis absent while the build carries 3). A bundle carrying fewer objects than the build trips it too.objectstack.config.ts: the flattened objects are exactly the objects of every package (counts derived from the artifact, not hard-coded), and every contributed navigation item is inside the app. A future package that cannot be flattened therefore fails on its PR, not at a release tag.One-shot ablations, run through
ablation-replace(anchor hit and restore proven againstgit diff HEAD). No test file is kept for them:bundle = flattenArtifact(artifact);was replaced bybundle = artifact;, thenDRY_RUN=1 node scripts/publish-marketplace.mjswas run. It printed✗ refusing to publish: the bundle to post carries 0 objects (top-level keys: manifest, packages, i18n, docs); the build carries 18. Refusing to publish an empty app.and exited 1, before any[dry-run] POSTline. Restored: blobebe323adequals HEAD.applyNavContributionsline was removed. Exactly two cases went red: the fixture fold, and the case that checks every contributed navigation item is in the app. The other 9 stayed green. Restored: blobecb89816equals HEAD.Gates:
pnpm verifyexits 0 on6c78cfa1. That coversvalidate,typecheck,lint,lint:i18n-gate,hygiene,hygiene:tokens,buildandtest: 176 files, 3633 passed, 1 skipped.Acceptance notes
emailTemplatessurvives cloud's ingest.docs/RELEASE_STRATEGY.md§Marketplace Publish does not yet describe the new summary line or the refusal. Nothing in the doc becomes false, but a release operator might want that sentence there.Generated by Claude Code