skmtcdocs

skmtc publish

Build and publish an immutable **version** of a project's stack to skmtc-hub.

publish uploads a local SKMTC project to the hub as a new immutable stack version. Stacks are published packages (like generators); versions are addressed by semver. Projects on the hub pin a stack version and own all execution — deployments, runs, and the production alias live on projects and are driven from the web app, not the CLI.

A stack's identity on the hub is its package name — the project root deno.json#name, in @account/slug form (a stack is a JSR-style package). The @account scope may be a user or an org, so org-owned stacks are reachable: the PAT authenticates you, the hub authorizes you as a writer on that account/stack (org membership included). The name is required — publish fails with a recipe if deno.json#name is missing or isn't a scoped @account/slug.

The version comes from the project root deno.json#version, or from --version. Versions are immutable — re-publishing an existing semver is rejected with a 409. The CLI never invents or auto-bumps a version.

New to the hub? What is skmtc-hub introduces the nouns this page assumes (account, stack, project, PAT).

Synopsis

skmtc publish <project> --token <pat> [--version <semver>] [--origin <url>] [--json] [--no-input]

Arguments

<project>

The local project name under .skmtc/<project>/. Required. The stack destination is not derived from this — it's the project's deno.json#name (@account/slug).

Options

--token <pat>

Personal access token. Resolution order:

  1. --token flag
  2. $SKMTC_HUB_TOKEN
  3. The token stored by skmtc login (~/.skmtc/auth.json)

Mint one in the SPA's account settings (write:releases alone is enough). The PAT authenticates you; the destination account/stack comes from deno.json#name, and the hub authorizes you as a writer on it (so org members can publish to org stacks).

--version <semver>

The version to publish. Defaults to the project root deno.json#version. If neither is present, publish fails fast (before any network call) with a recipe-style error.

--origin <url>

Hub origin (base URL). Defaults to $SKMTC_API_ORIGIN; then — only when the token came from the stored skmtc login file — the host recorded in that file; then https://api.skmtc.dev. The stored-host step keeps token and destination coherent: a token minted against a local dev hub is never silently sent to production.

--no-input / --json

See the overview.

What publish does

1. Resolve the version + stack identity (both pre-network)
     `--version` wins; otherwise the project root deno.json#version.
     Missing → fail at stage `version`. The stack `@account/slug` comes
     from deno.json#name; missing/unscoped → fail at stage `identity`.

2. bundleDeploy(project)
     → <project>/server.js   (one self-contained bundle: the generator
        composition + createServer + @skmtc/server + @skmtc/core, all
        inlined, nothing external)

3. POST /v1/stacks/{account}/{slug}/versions   (multipart, atomic)
     One `version` part (semver) + one `bundle` part (server.js) + N
     `files` parts (one per project source file). The hub writes the
     bundle + source to R2, authorizes you as a `writer` on the
     account/stack, reconciles the generator composition from the
     uploaded deno.json, and returns the complete StackVersion.

There is no metadata-only intermediate state — the publish is atomic (version + bundle + source in one request). Each step has its own failure stage in the result envelope (below).

The hub auto-creates the stack on first publish (the "git push creates the repo" behavior), under the @account from deno.json#name — which may be an org you're a member of.

Strict mode

When --no-input or --json is set:

  • Missing <project>, or no token from any source (--token, $SKMTC_HUB_TOKEN, stored skmtc login) → exit code 2 with a recipe error on stderr.
  • The Ink TUI is skipped; publish runs straight through publishHeadless and prints either a one-line text summary or a single JSON object.

JSON output

Success

{
  "type": "published",
  "projectName": "my-api",
  "bundlePath": ".skmtc/my-api/server.js",
  "bundleBytes": 1228801,
  "bundleSha256": "e3b0c44298fc1c14...",
  "stack": { "account": "ada", "slug": "my-api" },
  "version": "3.0.1",
  "versionUrl": "https://skmtc.dev/ada/stacks/my-api/versions/3.0.1",
  "sourceFileCount": 28,
  "sourceTotalBytes": 96512
}

There is no deploymentId, shortId, or production alias — versions are addressed by semver.

Failure

{
  "type": "failed",
  "projectName": "my-api",
  "reason": "version 3.0.1 is already published for ada/my-api — versions are immutable. ...",
  "stage": "publish"
}

Possible stage values:

StageWhere the failure happened
versionNo version to publish — no deno.json#version and no --version. Fails before any network call.
identityNo stack name — deno.json#name is missing or not a scoped @account/slug. Fails before any network call.
bundlebundleDeploy — Deno bundling failed locally, or reading the bundle / walking the source tree failed
publishPOST /v1/stacks/.../versions failed (commonly: 409 — that version is already published)

Text-mode output

Success

Published "my-api" → ada/my-api@3.0.1
  bundle: .skmtc/my-api/server.js
  bytes: 1228801
  sha256: e3b0c44298fc1c14...
  source: 28 files, 96512 bytes
  version: https://skmtc.dev/ada/stacks/my-api/versions/3.0.1

Failure

Publish failed for "my-api" at publish:
  version 3.0.1 is already published for ada/my-api — versions are immutable. ...

What gets uploaded

Bundle (one file)

<project>/server.js — the compiled CF-Workers entry point produced by bundleDeploy. One self-contained bundle: @skmtc/core and @skmtc/server are inlined, nothing external (~1.2 MB).

Source tree (N files)

Walks <project>/ and uploads every file except:

  • Dotfiles (with the single exception of .settings/, which holds client.json).
  • Root-level derived artefacts: server.ts, server.js, bundle.js, worker.ts.
  • Binary asset extensions: mp4, png, jpg, pdf, archives, etc.

The hub treats deno.json as the source of truth for the stack's generator composition and reconciles it per version: imports starting gen- that resolve to a jsr: specifier become imported; the same prefix resolving to a local path becomes cloned. Mixed states for the same generator name are rejected (422 — composition inconsistency). The root deno.json must be part of the upload.

Network isolation

A published stack server accepts a source URL on /artifacts and /to-v3-json and fetches it server-side. The server rejects URLs whose hostname is a loopback, private, link-local or .internal/.local name, and re-checks every redirect hop — but it inspects the hostname only. A public name with a private DNS record still resolves and is fetched, and the fetched body comes back to the caller as the schema.

The control that actually holds is the runtime's network isolation. On the hub, stack servers run under Deno subhosting, which has no route to an internal network. Running a stack server anywhere else requires an egress policy in front of it — the hostname check is defence in depth, not a substitute.

Errors

All errors use the uniform ApiError envelope ({ code, message, … }):

StatusMeaning
401No or invalid token
403Not a writer on the destination account/stack (e.g. not a member of the org in deno.json#name)
409That version is already published — versions are immutable. Bump the version and re-publish.
422Validation — missing version/bundle/root deno.json, composition inconsistent with the uploaded tree, size limits

Environment variables

VariablePurposeEquivalent flag
SKMTC_HUB_TOKENDefault PAT--token
SKMTC_API_ORIGINDefault hub origin (base URL)--origin

CLI flags always win over env vars; env vars win over the stored skmtc login credential (so CI can override a developer login).

Exit codes

CodeMeaning
0Success — stack version published with bundle + source
1Operational failure at any publish stage
2Missing required argument (recipe error on stderr)

Examples

One-shot publish

SKMTC_HUB_TOKEN=$PAT skmtc publish my-api

or

skmtc publish my-api --token $SKMTC_HUB_TOKEN --version 3.0.1

Agent / CI invocation

SKMTC_HUB_TOKEN=$PAT skmtc publish my-api --json --no-input

Capture the published version in a script

VERSION=$(SKMTC_HUB_TOKEN=$PAT skmtc publish my-api --json | jq -r '.version')
echo "published → $VERSION"

Compared with the previous deployment-based flow

Before the 2026-06 stacks-as-packages refactor, this command was skmtc deploy and created an immutable stack deployment identified by a UUID + 8-char shortId, with a production alias planned on the stack. That model moved: stacks are now published packages whose versions carry the bundle; projects pin a version and own deployments, runs, and the production alias (driven from the web app). The migration in one sentence: deploy → publish (the old command is gone, not aliased), set a version in the project's deno.json (or pass --version), and read version/versionUrl instead of deploymentId/shortId from the JSON output.

See also

  • skmtc bundle — bundleDeploy is invoked internally by publish
  • overview — shared flags and strict-mode semantics

On this page