@git.zone/tsdeploy

Fenced immutable Cloudly deployment orchestration for git.zone TypeScript tooling.

Issue Reporting and Security

For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.

Install

Install the CLI globally:

pnpm add -g @git.zone/tsdeploy

Install the library in applications that use the programmatic API:

pnpm add @git.zone/tsdeploy

Cloudly setup

tsdeploy keeps only a small, resumable project pointer in .nogit/tsdeploy.json. Cloudly remains the source of truth for deployment operations, releases, rollout state, runtime digest evidence, and route verification.

Create a Cloudly profile. Deployment mutation requires one scoped API machine identity. Its saved registry username must exactly match the authenticated machine identity name, and the original API-machine token is reused for the operation-bound registry push.

tsdeploy login \
  --profile prod \
  --target cloudly \
  --origin https://cloudly.example.com \
  --username service-deployer \
  --token ...

Saved tokens are encrypted as smartconfig SSH-agent secrets. The matching ssh-ed25519 key must be available when saving and using the token. The default authorized-key hint is ~/.ssh/id_ed25519.pub; select another key with --ssh-public-key-path or TSDEPLOY_SSH_PUBLIC_KEY_PATH.

Link the source checkout to its Cloudly service:

tsdeploy init --profile prod --serviceId Service:example
# Replace an existing pointer:
tsdeploy link --profile prod --serviceId Service:example

Inspect local readiness:

tsdeploy status
tsdeploy doctor
tsdeploy plan

Deployment declaration

Declare every secret-free workload input in .smartconfig.json. publicDomains and routes[].hostname must be the same exact set. Cloudly derives namespaces and Corestore resource names; source code never selects arbitrary cluster resource names.

{
  "@git.zone/tsdocker": {
    "registries": ["registry.example.com"],
    "registryRepoMap": {
      "registry.example.com": "workloads/service-example"
    },
    "platforms": ["linux/amd64", "linux/arm64"],
    "buildSecretEnvMap": {
      "szci_npm": "SZCI_TOKEN_NPM_1"
    }
  },
  "@git.zone/tsdeploy": {
    "deployment": {
      "declarationComplete": true,
      "dockerfile": "Dockerfile_v##version##",
      "publicDomains": ["app.example.com"],
      "environmentVariableNames": ["NODE_ENV"],
      "secretReferenceIds": ["Secret:production"],
      "containerPorts": [8080],
      "volumeMounts": [],
      "requiredCapabilities": ["pushnotification"],
      "routes": [
        {
          "hostname": "app.example.com",
          "targetPort": 8080,
          "proxied": false,
          "readinessPath": "/readyz",
          "expectedStatusCodes": [200]
        }
      ],
      "releaseEvidencePath": ".nogit/release-evidence.json",
      "releaseEvidenceCommand": ["pnpm", "run", "release:evidence"]
    }
  }
}

Set routes[].proxied explicitly for every current deployment route. false publishes direct DNS records; true requests provider proxying. Omission exists only for compatibility with historical declarations, where Cloudly preserves an existing route value when one is available.

requiredCapabilities contains capability identifiers only. Explicit values are combined with database and objectstorage inferred from canonical @git.zone/cli.services entries. pushnotification requires Cloudly to return exactly one enabled, provider-verified ready binding; it never declares a Corestore resource and never carries a provider credential or Web Push secret.

buildSecretEnvMap declares BuildKit secret IDs and their runtime environment variable names. tsdeploy accepts at most 16 entries, validates the same secret-ID shape as tsdocker, and only permits the dedicated SZCI_TOKEN_NPM_<number> and TSDOCKER_BUILD_SECRET_<NAME> variable families. Every declared value must exist when a real image build starts. Those values are forwarded only to the tsdocker build subprocess so tsdocker can use value-free --secret id=...,env=... arguments; they are not forwarded to digest, test, push, release-evidence, or Cloudly requests and never become deployment declaration data.

Consume a declared secret with a BuildKit mount instead of an ARG or ENV:

RUN --mount=type=secret,id=szci_npm,env=SZCI_TOKEN_NPM_1,required=true \
  pnpm install --frozen-lockfile

The optional evidence command runs after the exact OCI index digest is known and receives:

  • TSDEPLOY_SOURCE_REVISION
  • TSDEPLOY_VERSION
  • TSDEPLOY_CONFIGURATION_DIGEST
  • TSDEPLOY_OCI_INDEX_DIGEST
  • TSDEPLOY_RELEASE_EVIDENCE_PATH

The evidence file must be bound to those exact values and contain one reference for each required kind: provenance, SBOM, signature, vulnerability policy, and release authorization. These references are descriptive input; promotion succeeds only when Cloudly independently finds one trusted registry release for the same operation, actor, tag, repository, and digest.

{
  "sourceRevision": "<40-character lowercase Git revision>",
  "version": "1.2.3",
  "configurationDigest": "sha256:<64 lowercase hex characters>",
  "ociIndexDigest": "sha256:<64 lowercase hex characters>",
  "releaseAuthorized": true,
  "attestations": [
    { "kind": "provenance", "statementDigest": "sha256:<64 lowercase hex characters>", "subjectDigest": "sha256:<same OCI index digest>", "verifier": "<trusted verifier>", "verifiedAt": 1 },
    { "kind": "sbom", "statementDigest": "sha256:<64 lowercase hex characters>", "subjectDigest": "sha256:<same OCI index digest>", "verifier": "<trusted verifier>", "verifiedAt": 1 },
    { "kind": "signature", "statementDigest": "sha256:<64 lowercase hex characters>", "subjectDigest": "sha256:<same OCI index digest>", "verifier": "<trusted verifier>", "verifiedAt": 1 },
    { "kind": "vulnerability-policy", "statementDigest": "sha256:<64 lowercase hex characters>", "subjectDigest": "sha256:<same OCI index digest>", "verifier": "<trusted verifier>", "verifiedAt": 1 },
    { "kind": "release-authorization", "statementDigest": "sha256:<64 lowercase hex characters>", "subjectDigest": "sha256:<same OCI index digest>", "verifier": "<trusted verifier>", "verifiedAt": 1 }
  ]
}

Deploy

A dry run performs exactly one mutation-free reserve preflight. It reads package.json, .smartconfig.json, the exact Git revision, and worktree status; it does not run project scripts, build, push, reserve, promote, or route anything.

tsdeploy deploy --dryRun --mode existing-service --organization-id Organization:example

A real deployment performs this fenced sequence:

  1. Reserve preflight.
  2. Project test or build, followed by a fresh source and intent check and final reserve preflight.
  3. Durable operation reservation and immediate persistence of its operation ID.
  4. Pinned @git.zone/tsdocker build, digest, no-build test, and exact-tag no-build registry push, or explicit exact-digest reannouncement of a previously published immutable manifest.
  5. Trusted-release lookup and promote preflight.
  6. Exact-digest immutable rollout and complete replica/runtime verification.
  7. Route preflight, promotion, external route/TLS verification, and final success verification.
tsdeploy deploy \
  --mode existing-service \
  --organization-id Organization:example

When the exact immutable image was published before its Cloudly deployment operation was reserved, or when resuming its exact awaiting-image operation, explicitly reuse it instead of rebuilding:

tsdeploy deploy \
  --mode greenfield \
  --organization-id Organization:example \
  --oci-index-digest sha256:<64-lowercase-hex> \
  --reuse-published-image

Published-image reuse requires the canonical digest assertion and cannot be combined with --dryRun. Supply both --reuse-published-image and the same digest again when resuming. tsdeploy validates the release evidence and unchanged candidate first, then asks pinned @git.zone/tsdocker to read, hash, and reannounce the byte-identical manifest to the same immutable tag. It never rebuilds, runs container-image tests, copies blobs, or moves the tag. Promotion still requires Cloudly to bind that exact digest to the operation and independently return exactly one registry-generated trusted release for the same actor, service, repository, and tag. If the registry response is interrupted after acceptance, tsdeploy continues only when fresh Cloudly state proves the exact operation, candidate, actor, and digest in image-recorded, rolling-out, ready-for-route, or succeeded; every other result remains fatal.

For existing services, tsdeploy discovers the current immutable rollout fence from the last successful operation or from Cloudly's current service plan. Supply --expected-rollout-generation and --expected-current-rollout-id only as assertions after independently verifying that exact fence. An existing service must have deployOnPush disabled and a digest-pinned current runtime before reserve can report GO, with one audited exception: when the runtime is unattestable and the service's most recent deployment operation ended failed or cleaned, Cloudly reports GO with a RECOVERY_REDEPLOY_OVER_FAILED_RUNTIME warning. tsdeploy then relaxes only the runtime-attestation evidence fences, prints the warning as a stderr banner, and keeps every other fence — including deployOnPush and current-reference immutability — fully strict.

Greenfield uses the exact organization/service-slot grant and does not require a current rollout fence:

tsdeploy deploy --mode greenfield --organization-id Organization:example

--version, --release-tag, --platform, and --oci-index-digest are exact assertions, not build overrides. The release tag must equal the package version produced by Dockerfile_v##version##, and Cloudly requires exactly linux/amd64 plus linux/arm64. --release-evidence selects an alternate in-project evidence file; --wait-seconds and --poll-interval-ms bound status polling. Remote profile origins require HTTPS. Plain HTTP is accepted only for loopback development.

Credentials can come from the encrypted profile or explicit runtime inputs:

TSDEPLOY_TOKEN=... tsdeploy deploy ...
tsdeploy deploy --token ... --registry-username service-deployer ...

Username/password credentials can authenticate read-only dry-run inspection, but Cloudly API machines are token-only and deploy-status plus every deployment mutation require the operation's API-machine token.

Resume and recovery

An unfinished operation is resumed automatically when its exact candidate intent and actor still match. Inspect or recover explicitly with a fresh operation revision:

tsdeploy deploy-status --operation-id deploy:example
tsdeploy deploy-retry \
  --operation-id deploy:example \
  --expected-operation-revision 7
tsdeploy deploy-cleanup \
  --operation-id deploy:example \
  --expected-operation-revision 8

Retry and cleanup reject missing or stale revisions. A retried DEPLOYMENT_ROLLOUT_FAILED operation returns to image-recorded for image re-promotion; other retryable failures resume from their recorded progress. Failed and cleaned operations keep their failure record as audit history, and deploy-status renders it. Successful deployment clears activeOperationId and stores lastSuccessfulOperationId in .nogit/tsdeploy.json. If a client exits after Cloudly has recorded exact verified success but before that local pointer update, the next deployment reconciles the same operation ID, actor, service, digest, rollout, replica, runtime, and route evidence before updating only the local pointer and evaluating a new candidate.

Onebox deployment mutation is not implemented until its exact deployment API is available.

Adopt an existing service

Adoption converts a linked legacy deployOnPush service into Cloudly's immutable deployment state without rebuilding it. The scoped API machine needs deployment:status and deployment:adopt-existing for the exact existing service.

tsdeploy adopt \
  --organization-id Organization:example \
  --release-tag 1.2.3 \
  --oci-index-digest sha256:<64-lowercase-hex> \
  --token ...

The OCI index digest is a required target assertion. If the service still uses a mutable tag, Cloudly may retain that source under the requested SemVer tag only when the source already has the exact asserted digest. A service that already points at an independently published immutable target skips retention. No image is rebuilt and no registry password or admin identity is used.

Cloudly validates the exact release, OCI index, runnable linux/amd64 plus linux/arm64 manifests, service ownership, and target scope before creating a generation-one immutable rollout plan. tsdeploy polls the same idempotent bootstrap operation until every required replica is observed, healthy, and digest-verified. It then fetches fresh service state and requires deployOnPush=false, immutable enforcement, the exact tag/digest, and a succeeded rollout. Repeating the command is a verified no-op after success; interrupted rollout bootstrap resumes with the same deterministic operation fence.

--release-tag defaults to package.json version and requires canonical bare OCI-safe SemVer such as 1.2.3 or 1.2.3-rc.1; a v prefix and build metadata are rejected. --oci-index-digest is mandatory and never overrides registry content. A missing, conflicting, or single-platform target is rejected before rollout because adoption cannot manufacture a multi-platform OCI index.

Programmatic API

import { TsDeploy } from '@git.zone/tsdeploy';

const tsDeploy = new TsDeploy({ projectDir: process.cwd() });

await tsDeploy.saveProfile({
  name: 'prod',
  target: 'cloudly',
  origin: 'https://cloudly.example.com',
  username: 'service-deployer',
});

await tsDeploy.writeProjectLink({
  profile: 'prod',
  serviceId: 'Service:example',
});

const dryRun = await tsDeploy.deploy({
  dryRun: true,
  mode: 'existing-service',
  organizationId: 'Organization:example',
  token: process.env.TSDEPLOY_TOKEN,
});
console.log(dryRun.preflight?.decision);

await tsDeploy.deploy({
  mode: 'greenfield',
  organizationId: 'Organization:example',
  token: process.env.TSDEPLOY_TOKEN,
  registryUsername: 'service-deployer',
  reusePublishedImage: true,
  ociIndexDigest: 'sha256:<64-lowercase-hex>',
});

const status = await tsDeploy.deploymentStatus({
  operationId: 'deploy:example',
  token: process.env.TSDEPLOY_TOKEN,
});

if (status.status?.operation.data.state === 'failed') {
  await tsDeploy.retryDeployment({
    operationId: status.operationId,
    expectedOperationRevision: status.operation.revision,
    token: process.env.TSDEPLOY_TOKEN,
  });
}

reusePublishedImage is the programmatic equivalent of --reuse-published-image. It requires the same canonical ociIndexDigest assertion and cannot be combined with dryRun: true.

This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the repository license file.

Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

Trademarks

This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.

Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.

Company Information

Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany

For any legal inquiries or further information, please contact us via email at hello@task.vc.

By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

S
Description
No description provided
Readme
974 KiB
Languages
TypeScript 99.9%
JavaScript 0.1%