Auto-publish on push to main is wired via .github/workflows/publish.yml.
This doc covers what the workflow does, the per-template metadata it
requires, and how to test or backfill locally.
Publishing is explicit — there is no auto-publish on push. Two ways to trigger:
1. Actions UI → Run workflow (manual, supports dry-run)
2. GitHub Release published (tag e.g. todo-v0.2.0) (release-driven)
│
├─ pnpm install
├─ pnpm -r build (every template emits dist/objectstack.json)
└─ node scripts/publish-template.mjs
│
├─ POST /api/v1/cloud/packages (idempotent upsert)
└─ POST /api/v1/cloud/packages/:id/versions
└─ 409 (dup version) → treated as success
When triggered via a release whose tag matches <name>-v<semver> (e.g.
todo-v0.2.0), the workflow narrows the publish to that one template.
Otherwise (or via manual dispatch with an empty filter) it walks every
package — but each is idempotent, so unchanged versions return 409 and
the script reports skipped.
In Repo Settings → Secrets and variables → Actions:
| Type | Name | Value |
|---|---|---|
| Variable | OS_CLOUD_URL |
https://cloud.objectos.app (prod control plane) |
| Secret | OS_CLOUD_API_KEY |
Service token — copy OS_CLOUD_API_KEY from cloud repo apps/cloud/.env.cloudflare.secrets |
The workflow guards if: github.repository == 'objectstack-ai/templates'
so PRs from forks never see the secret.
The publish script reads packages/<name>/package.json and expects an
objectstack.marketplace block:
Marketplace policy (see marketplace-policy.ts in cloud) enforces:
display_name3–64 charsdescription30–512 charsreadme≥ 200 chars (markdown)icon_urlhttps(s)categorynon-empty
Bundle size cap: 50 MB (MAX_MARKETPLACE_BUNDLE_BYTES).
Reserved prefixes (require platform-admin to publish):
app.objectstack.*com.objectstack.*com.objectos.*sys.*
Our service token (OS_CLOUD_API_KEY) authenticates in service mode,
which the publish route treats as implicitly trusted — first-party
app.objectstack.template.* templates publish without an admin review
round-trip.
The version POSTed comes straight from package.json.version. To ship a
new release of an existing template:
- Bump
packages/<name>/package.jsonversion(semver). - Optionally set
objectstack.marketplace.releaseNotesto a short string. - Commit + push to
main(nothing publishes yet — the workflow no longer triggers on push). - Cut the release. Two equivalent options:
- Actions → Publish to marketplace → Run workflow — optionally tick
dry_runfirst to inspect the payload, then run again without it. - GitHub Releases → Draft a new release with tag
<name>-v<version>(e.g.todo-v0.2.0). When the release is published the workflow fires automatically and narrows the publish to that template via the tag pattern.
- Actions → Publish to marketplace → Run workflow — optionally tick
If the version wasn't bumped, the version POST returns 409 and the script
reports skipped — safe to ignore.
# Inspect what would be POSTed, with no network calls
pnpm publish:templates:dry-run
# Filter to one template
PUBLISH_TEMPLATES=@objectlab/todo pnpm publish:templates:dry-runexport OS_CLOUD_URL=https://cloud.objectos.app
export OS_CLOUD_API_KEY=… # from apps/cloud/.env.cloudflare.secrets
pnpm -r build
pnpm publish:templatesFor staging:
export OS_CLOUD_URL=https://staging-cloud.objectos.app
export OS_CLOUD_API_KEY=… # staging service tokenActions → Publish to marketplace → Run workflow:
templates: leave empty to publish all, or comma-separated subset.dry_run: tick to print payloads without writing.
GitHub Releases → Draft a new release:
- Tag
<template>-v<version>(e.g.todo-v0.2.0) → the workflow narrows the publish to@objectlab/<template>. - Any other tag shape → workflow runs but evaluates every template (each is idempotent — unchanged versions are skipped).
- Use the release body as a public changelog. It is not posted to the
marketplace listing; set
objectstack.marketplace.releaseNotesinpackage.jsonif you want notes attached to the published version row.
| HTTP | Meaning | Action |
|---|---|---|
| 400 | Marketplace policy violation (display name too short, missing icon, etc.) | Fix package.json.objectstack.marketplace, re-push. |
| 401 | OS_CLOUD_API_KEY missing/wrong |
Rotate the secret, ensure repo setting points at the live key. |
| 403 | Trying to publish a reserved-prefix manifestId without service-mode auth |
Should not happen with the canonical workflow. Check the auth header chain. |
| 409 | (package, version) already exists |
Bump package.json.version — or accept the skipped summary and move on. |
| 503 | Control plane control-driver unavailable (cold start race or outage) | Re-run the workflow. |
mkdir packages/<name>/and scaffold it (seepackages/todo/as the reference layout).- Add the
objectstack.marketplaceblock to itspackage.jsonwith a freshmanifestId(e.g.app.objectstack.template.helpdesk). - Ensure
pnpm buildemitsdist/objectstack.json. - Cut a release tagged
<name>-v0.1.0(or run the workflow manually). The script auto-discovers any directory underpackages/that has apackage.jsonwith a marketplace block.
{ "name": "@objectlab/todo", "version": "0.1.0", "license": "Apache-2.0", "objectstack": { "marketplace": { "manifestId": "app.objectstack.template.todo", "displayName": "Todo", // 3–64 chars "description": "Universal task & project management starter…", // 30–512 chars "category": "starter", // shown in catalog facets "iconUrl": "https://cdn.objectos.app/icons/todo.svg", "homepageUrl": "https://github.com/objectstack-ai/templates/tree/main/packages/todo", "readmePath": "README.md" // file read at publish time (≥ 200 chars) } } }