Skip to content

Commit 882d4e4

Browse files
fix(cli): os package publish sends the manifest's listing tags, and uses a top-level tagline only as a missing description (#27573)
domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw <!-- claude-projects-attribution: {"github_login":"os-sales"} --> _Requested by **Sales** · [project thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuVpEmthvkRbuHGjC6oSQZvd)_ Fixes #27364 Clause-②: no Metadata-protocol: none Skills-docs: updated here — content/docs/deployment/marketplace-handbook.mdx, content/docs/deployment/cli.mdx (commit 94a3512, claim amendment 6498466275) ## 维护者速读 - **改了什么**:`os package publish` 现在把清单里的 `tags` 原样发给云端;警告改口:`publisher`、`isStarter` 由平台决定、作者写了不生效(不再说「暂未携带」);顶层 `tagline` 只在没有 description 时充当 description,否则不发并提示。 - **为什么改**:云端发布接口已能校验并保存 `tags`(cloud#3361),CLI 还在丢弃它,警告也说错了原因;`tagline` 云端没有这一列,一直白发。 - **风险与回滚**:只动 CLI 一个命令的请求体与警告文案;超长或过多的标签由云端 400 拒收并原样打印,CLI 不自设上限。回滚即 revert 本 PR。 - **先看**:`packages/cli/src/commands/package/publish.ts`(`pkgBody.tags` 与 tagline/description 那一段、`uncarriedManifestWarnings`);`packages/cli/test/package-publish-uncarried-manifest-keys.test.ts`。 - **两分钟验证**:`pnpm --filter @objectstack/cli exec vitest run --project unit test/package-publish-uncarried-manifest-keys.test.ts`,预期 14 个用例全绿。 - **需要你做的**:无,席位落地。 ## What changes `packages/cli/src/commands/package/publish.ts`, one command: - **`tags` is sent.** When `objectstack.manifest.json` declares `tags`, the package POST carries `tags` as written. The hosted route owns the shape and the bounds; the CLI keeps no copy of them and pre-checks nothing, so a refused list comes back as the server's 400 and is printed as the server wrote it (the existing failure path). An absent key leaves the stored tags alone; `[]` clears them. `tags` leaves `UNCARRIED_MANIFEST_KEYS`. - **Warning wording.** `publisher` and `isStarter` get their own warning: decided by the platform, an author's value has no effect, so publish does not send them. ⛔ Neither is ever sent. `preview` keeps the "hosted catalog does not carry yet" warning with the tracking link; `visibility` keeps its flag warning. - **Top-level `tagline`.** With no `--description` flag and no manifest `description`, the trimmed tagline is sent as `description`. Otherwise the tagline is not sent, and a warning names it (dropping an authored key silently is the defect this module's warnings exist for). The per-locale `translations.LOCALE.tagline` path is unchanged. Importers of the two exported constants: `UNCARRIED_MANIFEST_KEYS` and `CATALOG_SEND_ISSUE` are imported only by `packages/cli/test/package-publish-uncarried-manifest-keys.test.ts` (repo-wide grep at the branch head). One changeset, `@objectstack/cli` patch. ## Tests `packages/cli/test/package-publish-uncarried-manifest-keys.test.ts` drives the real command (`PackagePublish.run`) against a stubbed `fetch` and reads the request body it sends: - tags: sent as written; `[]` sent; no key when undeclared; 21 over-long tags reach the wire unchanged, and the server's 400 message reaches the output, exit 1, no version POST. - tagline: becomes `description` when none is given; not sent and named in a warning when the manifest or the flag gives one; per-locale tagline untouched. - warnings: `publisher`/`isStarter` platform-decided and never sent; `preview` still not carried, with the link; `visibility` separate; quiet when only sent keys are declared. - the schema-key partition moves `tags` to SENT. ## Acceptance notes - **Docs updated here (commit `94a3512f`, file surface extended by claim amendment 6498466275).** `content/docs/deployment/marketplace-handbook.mdx`: the warn callout no longer says `tags` is not sent (`publisher` / `isStarter` are platform-decided, never sent, warned), and the key table gains a `tags` row. `content/docs/deployment/cli.mdx`: the keys-read list names `tags` (sent as written) in place of `tagline`, and says a top-level `tagline` is used only as a missing description. - The hosted bounds (20 tags, 64 characters each) are cloud's numbers and are not exported in this repository, so per the triage ruling the CLI does not pre-check them. ## Verification All readings below are at head `083edf170d` (fix `fadb51292f` plus one merge of `origin/main`), except the red leg and the ablation, which name their commit. <details><summary>Red first (test commit f2c48fc, source unchanged)</summary> ``` Test Files 1 failed (1) Tests 9 failed | 5 passed (14) ``` Failing: the three tags-sent cases, the three tagline cases, the platform-decided wording, the quiet case, and the partition. </details> <details><summary>Green</summary> ``` publish suites (5 files): Test Files 5 passed (5) · Tests 59 passed (59) pnpm --filter @objectstack/cli exec vitest run --project unit: Test Files 349 passed (349) · Tests 5063 passed (5063) · VERDICT command-exit 0 pnpm --filter @objectstack/cli typecheck: VERDICT command-exit 0 (test layer: 28 errors / 6 signatures held in test-typecheck-debt.json, unchanged) eslint --no-inline-config packages/cli: exit 0 ``` The `integration` layer is declared to CI (the diff touches no spawn entry). </details> <details><summary>Ablation (scripts/ablation-replace.mjs, at fadb512)</summary> ``` anchor "if (m.tags !== undefined) pkgBody.tags = m.tags;" x1 -> x0, replacement x0 -> x1 blob 4bc0cb2 -> d0cced9bda65 Tests 3 failed | 11 passed (14): the three tags-sent cases restored: blob == HEAD (4bc0cb2) and git diff HEAD is empty ``` The test imports the command from `src/`, so no build sits between the mutation and the run. The tagline and wording changes are evidenced by the red leg against the unchanged source. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01L6bhgXcN9HaZYSEUdJWEjw)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01L6bhgXcN9HaZYSEUdJWEjw --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6663a66 commit 882d4e4

5 files changed

Lines changed: 295 additions & 84 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
fix(cli): `os package publish` sends the manifest's listing `tags`, says `publisher` and `isStarter` are decided by the platform, and uses a top-level `tagline` only as a missing description
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The hosted publish route validates and stores a package's listing tags, so `os package publish` now carries them:
11+
12+
- **`tags`.** When `objectstack.manifest.json` declares `tags`, publish sends them as written. The hosted route owns the shape and the bounds and refuses a bad list with a 400, which publish prints as the server wrote it; the CLI applies no limit of its own. A file with no `tags` leaves the stored tags alone, and `"tags": []` clears them. The warning no longer names `tags`.
13+
- **`publisher` and `isStarter`.** The warning now says the platform decides them and an author's value has no effect. Publish still never sends either.
14+
- **Top-level `tagline`.** The hosted catalog has no tagline column; `description` is the short listing text. When neither `--description` nor the manifest's `description` is given, publish sends the tagline as the description. Otherwise it no longer sends the tagline, and a warning says so. A per-locale `translations.<locale>.tagline` is sent as before.
15+
- **`preview` and `visibility`.** Unchanged: `preview` is still named as not carried by the hosted catalog yet, and `visibility` is still set only by `--visibility`.

‎content/docs/deployment/cli.mdx‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2952,9 +2952,13 @@ and rebuild.
29522952

29532953
**`objectstack.manifest.json` supplies the listing fields.** When that file is
29542954
present in the working directory, publish reads `manifestId`, `displayName`,
2955-
`description`, `category`, `tagline`, `iconUrl`, `homepageUrl`, `license`,
2955+
`description`, `category`, `tags`, `iconUrl`, `homepageUrl`, `license`,
29562956
`readmePath` and a `translations` map from it, so a listing need not be retyped
2957-
as flags on every publish. **CLI flags always win.** A per-locale `readme` entry
2957+
as flags on every publish. **CLI flags always win.** `tags` has no flag and is
2958+
sent as written: the control plane judges the list, and a refusal is its 400,
2959+
printed as it is. A top-level `tagline` is used only as the description, when
2960+
neither `--description` nor the file's `description` gives one; otherwise it is
2961+
not sent, and a warning says so. A per-locale `readme` entry
29582962
may be inlined markdown or a path resolved against the manifest's own directory
29592963
(`README.zh-CN.md`). Publishing without the file is fully supported — it stays
29602964
flag-driven.

‎content/docs/deployment/marketplace-handbook.mdx‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ the declared keys below are the ones publish actually sends.
3535
| `displayName` | The name on the card | `--display-name` |
3636
| `description` | The short description | `--description` |
3737
| `category` | The catalog category slug (`crm`, `hr`, `finance`, …) | `--category` |
38+
| `tags` | The listing tags, sent as written. The catalog validates and stores them and refuses a bad list with a 400, which publish prints as it is; `[]` clears the stored tags, and leaving the key out keeps them | — |
3839
| `iconUrl` | A public http(s) icon URL | `--icon-url`, or `--icon-file` to upload one |
3940
| `homepageUrl` | Your project or docs URL | `--homepage-url` |
4041
| `license` | An SPDX identifier | `--license` |
@@ -83,10 +84,12 @@ columns for a missing key. The per-locale shape is the
8384
**Two things the file can say that never reach the listing.** Publish uploads no
8485
screenshot images, and the Console's marketplace pages render none: a
8586
`preview.screenshots` list stays on your disk, and `screenshotCaptions` travels
86-
with no image to caption. The schema also accepts `tags`, `publisher` and
87-
`isStarter`, but `os package publish` does not send them. Put the icon, the
88-
description, the tagline and the README where the table above says, and nothing
89-
else.
87+
with no image to caption. The schema also accepts `publisher` and `isStarter`,
88+
but the platform decides both: an author's value has no effect, so
89+
`os package publish` never sends them and prints a warning naming each one the
90+
file declares. `tags` is different: it is sent as written (see the table above).
91+
Put the icon, the description, the tags, the tagline and the README where the
92+
table above says, and nothing else.
9093
</Callout>
9194

9295
## The README the listing depends on

‎packages/cli/src/commands/package/publish.ts‎

Lines changed: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -258,17 +258,21 @@ async function tryLoadTemplateManifest(
258258

259259
/**
260260
* Keys `objectstack.manifest.json` accepts that this command does NOT carry to
261-
* the control plane. `tags`, `publisher`, `isStarter` and `visibility` are
262-
* inherited from `CreatePackageRequestSchema`; `preview` is a scaffold-side
263-
* extra (publish has no screenshot upload path).
261+
* the control plane. `publisher`, `isStarter` and `visibility` are inherited
262+
* from `CreatePackageRequestSchema`; `preview` is a scaffold-side extra
263+
* (publish has no screenshot upload path).
264264
*
265-
* They are not sent because nothing in this repo shows the hosted
266-
* `POST /cloud/packages` route writes them: the request schema declares them,
267-
* but the read-side row stores `tags` as a string, and `publisher` is a trust
268-
* tier that gates listing privileges, so a value self-declared in an author's
269-
* file is a privilege claim only the control plane can judge. ⛔ Do not start
270-
* sending them before {@link CATALOG_SEND_ISSUE} answers what the hosted side
271-
* accepts. `visibility` is flag-only on purpose (see the `--visibility` flag).
265+
* - `publisher` and `isStarter` are {@link PLATFORM_DECIDED_MANIFEST_KEYS}:
266+
* the hosted control plane decides both and never reads an author's value
267+
* (`publisher` is a trust tier that gates listing privileges; `isStarter` is
268+
* honoured only for a first-party seed). ⛔ Never send them: a value
269+
* self-declared in an author's file is a claim only the platform can judge.
270+
* - `preview` is not carried by the hosted catalog yet; that is tracked at
271+
* {@link CATALOG_SEND_ISSUE}.
272+
* - `visibility` is flag-only on purpose (see the `--visibility` flag).
273+
*
274+
* `tags` is not here: the hosted route validates and stores it, so it is sent
275+
* (see the request body in `run()`).
272276
*
273277
* Dropping an authored key silently is the defect, so each one the file
274278
* declares is named in a warning. The warning does not fail the publish: the
@@ -279,9 +283,12 @@ async function tryLoadTemplateManifest(
279283
* `test/package-publish-uncarried-manifest-keys.test.ts`, so a key the schema
280284
* gains later goes red there until publish either sends it or lists it here.
281285
*/
282-
export const UNCARRIED_MANIFEST_KEYS = ['tags', 'publisher', 'isStarter', 'preview', 'visibility'] as const;
286+
export const UNCARRIED_MANIFEST_KEYS = ['publisher', 'isStarter', 'preview', 'visibility'] as const;
287+
288+
/** The {@link UNCARRIED_MANIFEST_KEYS} the platform decides, whatever the file says. */
289+
const PLATFORM_DECIDED_MANIFEST_KEYS: readonly string[] = ['publisher', 'isStarter'];
283290

284-
/** Where carrying the catalog keys to the hosted control plane is tracked. */
291+
/** Where carrying the remaining catalog keys to the hosted control plane is tracked. */
285292
export const CATALOG_SEND_ISSUE = 'https://github.com/objectstack-ai/objectstack/issues/27348';
286293

287294
/** The {@link UNCARRIED_MANIFEST_KEYS} that `manifest` declares, in list order. */
@@ -291,19 +298,30 @@ export function uncarriedManifestKeys(manifest: Record<string, unknown>): string
291298

292299
/**
293300
* The warnings for the {@link uncarriedManifestKeys} a manifest declares: one
294-
* for the catalog keys (declared, not yet carried by the hosted catalog) and
295-
* one for `visibility` (carried by the flag instead). Empty when none.
301+
* for the platform-decided keys (an author's value has no effect), one for the
302+
* catalog keys the hosted side does not carry yet, and one for `visibility`
303+
* (carried by the flag instead). Empty when none.
296304
*/
297305
export function uncarriedManifestWarnings(manifest: Record<string, unknown>): string[] {
298306
const declared = uncarriedManifestKeys(manifest);
299-
const catalog = declared.filter((key) => key !== 'visibility');
307+
const named = (keys: string[]) => keys.map((key) => `\`${key}\``).join(', ');
308+
const platform = declared.filter((key) => PLATFORM_DECIDED_MANIFEST_KEYS.includes(key));
309+
const catalog = declared.filter((key) => key !== 'visibility' && !PLATFORM_DECIDED_MANIFEST_KEYS.includes(key));
300310
const warnings: string[] = [];
311+
if (platform.length > 0) {
312+
const one = platform.length === 1;
313+
warnings.push(
314+
`objectstack.manifest.json declares ${named(platform)}, which ${one ? 'is' : 'are'} decided by the `
315+
+ `platform: an author's value has no effect, so \`os package publish\` does not send ${one ? 'it' : 'them'}.`,
316+
);
317+
}
301318
if (catalog.length > 0) {
319+
const one = catalog.length === 1;
302320
warnings.push(
303-
`objectstack.manifest.json declares ${catalog.map((key) => `\`${key}\``).join(', ')}, which the `
321+
`objectstack.manifest.json declares ${named(catalog)}, which the `
304322
+ 'hosted catalog does not carry yet: `os package publish` does not send '
305-
+ (catalog.length === 1 ? 'it' : 'them')
306-
+ `, so the published package will not show ${catalog.length === 1 ? 'it' : 'them'}. See ${CATALOG_SEND_ISSUE}.`,
323+
+ (one ? 'it' : 'them')
324+
+ `, so the published package will not show ${one ? 'it' : 'them'}. See ${CATALOG_SEND_ISSUE}.`,
307325
);
308326
}
309327
if (declared.includes('visibility')) {
@@ -599,11 +617,33 @@ export default class PackagePublish extends Command {
599617
// `''` would turn "declares no namespace" into a value the gate has to
600618
// interpret.
601619
if (namespace) pkgBody.namespace = namespace;
620+
// A top-level `tagline` has no column on the hosted side: `description`
621+
// is the catalog's short listing text. So a tagline stands in for the
622+
// description only when neither the flag nor the file gives one, and is
623+
// otherwise not sent — and the author is told, not dropped silently.
624+
// The per-locale `translations.*.tagline` below is a different path.
625+
const tagline = typeof m.tagline === 'string' && m.tagline.trim() ? m.tagline.trim() : undefined;
602626
const desc = flags.description ?? (typeof m.description === 'string' ? m.description : undefined);
603-
if (desc) pkgBody.description = desc;
627+
if (desc) {
628+
pkgBody.description = desc;
629+
if (tagline) {
630+
printWarning(
631+
'objectstack.manifest.json declares a top-level `tagline`, which the hosted catalog does not store, '
632+
+ 'and a description is already given, so `os package publish` does not send it. The catalog\'s '
633+
+ 'short listing text is `description`.',
634+
);
635+
}
636+
} else if (tagline) {
637+
pkgBody.description = tagline;
638+
}
604639
const cat = flags.category ?? (typeof m.category === 'string' ? m.category : undefined);
605640
if (cat) pkgBody.category = cat;
606-
if (typeof m.tagline === 'string' && m.tagline.trim()) pkgBody.tagline = m.tagline.trim();
641+
// Listing tags, as the author wrote them. The hosted route owns the
642+
// shape and the bounds (it trims, de-duplicates and caps them) and
643+
// answers a 400 that is printed as it is; this command keeps no copy of
644+
// those numbers. An absent key leaves the stored tags alone; `[]` clears
645+
// them.
646+
if (m.tags !== undefined) pkgBody.tags = m.tags;
607647
if (flags.org) pkgBody.owner_org_id = flags.org;
608648
if (flags['icon-url'] && flags['icon-file']) {
609649
printError('Pass either --icon-url or --icon-file, not both.');

0 commit comments

Comments
 (0)