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:
--tokenflag$SKMTC_HUB_TOKEN- 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, storedskmtc login) → exit code 2 with a recipe error on stderr. - The Ink TUI is skipped; publish runs straight through
publishHeadlessand 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:
| Stage | Where the failure happened |
|---|---|
version | No version to publish — no deno.json#version and no --version. Fails before any network call. |
identity | No stack name — deno.json#name is missing or not a scoped @account/slug. Fails before any network call. |
bundle | bundleDeploy — Deno bundling failed locally, or reading the bundle / walking the source tree failed |
publish | POST /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.1Failure
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 holdsclient.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, … }):
| Status | Meaning |
|---|---|
401 | No or invalid token |
403 | Not a writer on the destination account/stack (e.g. not a member of the org in deno.json#name) |
409 | That version is already published — versions are immutable. Bump the version and re-publish. |
422 | Validation — missing version/bundle/root deno.json, composition inconsistent with the uploaded tree, size limits |
Environment variables
| Variable | Purpose | Equivalent flag |
|---|---|---|
SKMTC_HUB_TOKEN | Default PAT | --token |
SKMTC_API_ORIGIN | Default 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
| Code | Meaning |
|---|---|
0 | Success — stack version published with bundle + source |
1 | Operational failure at any publish stage |
2 | Missing required argument (recipe error on stderr) |
Examples
One-shot publish
SKMTC_HUB_TOKEN=$PAT skmtc publish my-apior
skmtc publish my-api --token $SKMTC_HUB_TOKEN --version 3.0.1Agent / CI invocation
SKMTC_HUB_TOKEN=$PAT skmtc publish my-api --json --no-inputCapture 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 —
bundleDeployis invoked internally by publish - overview — shared flags and strict-mode semantics