@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"]
  },
  "@git.zone/tsdeploy": {
    "deployment": {
      "declarationComplete": true,
      "dockerfile": "Dockerfile_v##version##",
      "publicDomains": ["app.example.com"],
      "environmentVariableNames": ["NODE_ENV"],
      "secretReferenceIds": ["Secret:production"],
      "containerPorts": [8080],
      "volumeMounts": [],
      "routes": [
        {
          "hostname": "app.example.com",
          "targetPort": 8080,
          "readinessPath": "/readyz",
          "expectedStatusCodes": [200]
        }
      ],
      "releaseEvidencePath": ".nogit/release-evidence.json",
      "releaseEvidenceCommand": ["pnpm", "run", "release:evidence"]
    }
  }
}

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.
  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

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.

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. Successful deployment clears activeOperationId and stores lastSuccessfulOperationId in .nogit/tsdeploy.json.

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);

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,
  });
}

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 1.2 MiB
Languages
TypeScript 99.9%
JavaScript 0.1%