Skip to content

fix(publish): flatten the composed artifact before posting it to the marketplace (#2053) - #2054

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-2053-publish-flatten
Oct 10, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-2053-publish-flatten

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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 build writes dist/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 (type module, 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 (type app, 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.mjs read that file into bundle and posted it unchanged as { version, bundle, release_notes, auto_approve } to POST /api/v1/cloud/packages/:id/versions. Running DRY_RUN=1 node scripts/publish-marketplace.mjs on 6162c43f exits 0 and prints ✓ published version 4.0.0. The bundle it would post has top-level keys manifest, packages, i18n, docs, bundle.objects is absent, and the 18 objects (3 + 15) exist only under packages[]. 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.0 release asset's dist/objectstack.json (2,012,194 B) has 21 flat top-level keys: manifest (the same 9 identity keys), translations 1, i18n, objects 18, apps 1, views 14, pages 8, dashboards 5, reports 10, datasets 9, actions 31, flows 29, positions 12, permissions 7, sharingRules 10, skills 6, hooks 39, mappings 3, data 19, requires 8, docs 4.

The card assumed some things that the measurement changed:

  • 3.1.0 did not post id/name/version/type/category/description at the top level. It posted a manifest object with those keys. The flat top-level fields in cloud's stored row are cloud's own projection. category comes from objectstack.manifest.json through the package upsert, not from the bundle. So the flat bundle keeps the app's manifest object, exactly as 3.1.0 did.

  • "De-duplicate by name, or id" would refuse the real artifact. Some names repeat legitimately:

    • Actions: log_call, log_meeting and schedule_meeting each exist on 5 objects, 4 of them inside the app package alone.
    • Email templates: each template name appears once per locale.

    The flatten therefore keys an item the way the platform does. An action is keyed by objectName + name, as in collectComposedActionKeyCollisions (@objectstack/spec). An email template is keyed by name + locale, as in ITEM_KEY_DISCRIMINATORS (@objectstack/metadata-core).

  • Some items have no name or id: view containers, seed datasets, translation bundles and requires strings. These are only de-duplicated when two are exactly equal, because data legitimately carries two datasets each for crm_event_attendee and crm_campaign_member.

  • The module's navigationContributions is 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:

  • Package order comes from 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].
  • Collections are every key that COMPOSE_KEY_DISPOSITIONS (@objectstack/spec) composes by concat or objects, concatenated in that order. Before manifest: 'preserve' moved this list into the package bodies, composeStacks built 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 changes requires (12 down to 8).
  • Navigation contributions are folded into apps[].navigation by SchemaRegistry.applyNavContributions (@objectstack/objectql), the fold the runtime applies when it reads an app. The bundle carries no navigationContributions afterwards, so nothing applies it twice.
  • Kept: i18n, docs and translations, merged across levels. An array is concatenated. A single value must be equal at every level that carries it, or the flatten refuses.
  • Refused with BundleError, naming the item (never dropped):
    • two different items under one identity;
    • a module manifest key that differs from the app's (for example namespace);
    • a module dependency outside the artifact;
    • a navigation contribution into an app the artifact does not carry;
    • a non-array collection;
    • packages the platform cannot order.
  • A flat artifact (one with no packages array) passes through unchanged.

The guard (assertBundleCarriesEveryObject) refuses a bundle with 0 objects, 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, in DRY_RUN too.

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 6162c43f plus this change)

DRY_RUN=1 pnpm publish:marketplace exits 0 and prints:

── HotCRM (app.objectstack.hotcrm) @ 4.0.0
DRY_RUN=1 — no HTTP calls will be made.
  bundle (flattened from 2 packages): manifest {9 keys} · i18n {3 keys} · docs 4 · permissions 7 · objects 18 · data 20 · translations 1 · apps 1 · views 14 · pages 8 · dashboards 5 · reports 10 · datasets 9 · actions 31 · flows 32 · emailTemplates 76 · positions 12 · sharingRules 10 · skills 6 · hooks 44 · mappings 3 · requires 8

Key set compared with 3.1.0:

  • Keys 3.1.0 carried that the flat bundle lacks: none.
  • New key: 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 from sys_package_version.manifest_json.
  • manifest: the same 9 keys as 3.1.0.
  • Not carried, on purpose:
    • packages, the composed envelope.
    • The module's dependencies, which only point at the app inside this artifact.
    • The module's navigationContributions, now folded into the app's navigation: nav_case, nav_knowledge and nav_service_dashboard under group_service, nav_my_cases under group_work, and nav_report_sla under group_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:

key 3.1.0 sum of packages flat
data 19 20 20
flows 29 32 32
hooks 39 44 44
requires 8 12 8 (4 capabilities both packages require)

Every other key matches 3.1.0.

Size (compact JSON, the form the POST serialises):

bundle bytes
flat 4.0.x bundle 1,376,671
3.1.0 bundle 1,296,470
cloud's stored 3.1.0 row 1,251,460
cloud's stored 4.0.0 row 20,471

Tests

test/marketplace-bundle.test.ts has 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:

  • The flat output holds the collections of both packages, app package first. One action name on two objects stays two actions, and a capability both packages require is carried once.
  • The app manifest's identity is the top-level manifest, and no dependencies survive.
  • The module's navigation contribution lands in the group it targets, and no navigationContributions key remains.
  • A flat artifact passes through as the same object.
  • A real collision is refused: a second, different ex_account in the module throws BundleError naming 'ex_account'. A module namespace that differs from the app's is refused the same way.
  • Negative control: the composed artifact posted without flattening trips the guard (BundleError; objects is absent while the build carries 3). A bundle carrying fewer objects than the build trips it too.
  • On HotCRM's own 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 against git diff HEAD). No test file is kept for them:

  • CLI: bundle = flattenArtifact(artifact); was replaced by bundle = artifact;, then DRY_RUN=1 node scripts/publish-marketplace.mjs was 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] POST line. Restored: blob ebe323ad equals HEAD.
  • Navigation fold: the applyNavContributions line 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: blob ecb89816 equals HEAD.

Gates: pnpm verify exits 0 on 6c78cfa1. That covers validate, typecheck, lint, lint:i18n-gate, hygiene, hygiene:tokens, build and test: 176 files, 3633 passed, 1 skipped.

Acceptance notes

  • These are left for the maintainer's post-tag acceptance on staging: the stored row size and object count, a fresh install, the 3.1.0 to 4.0.1 upgrade, and whether emailTemplates survives 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

…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>
@vercel

vercel Bot commented Oct 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
hotcrm Ignored Ignored Oct 10, 2026 3:01pm UTC

Request Review

@github-actions github-actions Bot added the ci/cd CI plumbing and the verification pipeline label Oct 10, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 10, 2026 15:13
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 10, 2026
Merged via the queue into main with commit d7decd4 Oct 10, 2026
10 checks passed
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd CI plumbing and the verification pipeline

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants