deploy-toolkit

module
v0.0.0-...-a6e3fde Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 4, 2026 License: Apache-2.0

README

Deploy Toolkit

Deploy Toolkit is a release-promotion and deployment engine.

It answers one question:

Should this exact release become the state of this environment, and did it deploy correctly?

It does not decide whether code is good enough to merge — that is CI Toolkit's job. It does not provision infrastructure — that is Pulumi's job. It does not know how your application works internally — your project declares that through a deployment contract.

               SOURCE / CHANGE CONTROL
                       │
                  CI Toolkit
                       │
               Human merge to main
                       │
                       ▼
                 immutable release
                       │
                       ▼
              ┌─────────────────┐
              │ Deploy Toolkit  │
              │                 │
              │ release         │
              │ promotion       │
              │ deployment      │
              │ rollback        │
              └────────┬────────┘
                       │
                deployment target
                       ▲
                       │
                    Pulumi
                       │
                infrastructure

The four-tool architecture

System Fundamental question
CI Toolkit Should this change be allowed toward main?
Deploy Toolkit Should this release become the state of this environment, and did it work?
Pulumi What infrastructure should exist for that environment?
Your app What application are we actually running?

Compactly:

CI Toolkit       change → release
Deploy Toolkit   release → environment
Pulumi           specification → infrastructure

Core concepts

Deploy Toolkit deliberately has a small vocabulary:

Concept Meaning
Project Something that can be deployed, e.g. example-service
Release Immutable application version + artifacts
Environment Desired deployment state, e.g. production
Target A machine/environment capable of receiving a release
Deployment Contract Project-specific instructions for applying a release
Promotion Human authorization to make an environment use a release
Deployment Attempt to make the target match that promoted state
Observed State What the target is actually running

The most important distinction is:

Release ≠ Deployment

A release can exist without ever reaching production. Promotion is a human act; deployment is a deterministic machine act.

Status

Pre-v0.1. Consumer Contract v1 is a candidate, not yet frozen. It remains candidate until it has been exercised end to end by a real external consumer in a production-shaped deployment rehearsal. The schemas are defined and deployctl validate enforces them; see docs/consumer-contract-v1.md and the v0.1 scope in docs/architecture.md.

Current CLI

deployctl validate <manifest.yaml>...      validate project/release/environment/target manifests
deployctl release create [flags]           run eligibility and create an immutable release manifest
deployctl promotion propose <env> [flags]  open the human-authorization PR for a release
deployctl promotion check [flags]          verify a PR diff against the Promotion Diff Policy
deployctl promotion classify [flags]       classify a transition: PROMOTION, ORDINARY, INVALID or ERROR
deployctl version                          print version

Manifests pass through one authoritative pipeline: header check → JSON Schema → strict typed decoding → semantic invariants. The schemas are the structural contract; validate is the definitive validator.

release create resolves the branch head, proves the candidate is reachable from it, verifies every required check on the exact SHA, resolves artifact digests via the SHA discovery tag, builds the deterministic bundle from the exact Git tree, and writes .deploy/releases/<project>-<version>.yaml — idempotently, refusing to overwrite a different release with the same version. It stops at the Release boundary.

promotion propose re-verifies the release against current eligibility policy, reads the environment from trusted main, and opens a machine-generated authorization PR containing one atomic commit: the release file plus an environment change limited to spec.release. It is idempotent, detects conflicting proposals, and stops at the open PR — merging is the human authorization act (see docs/promotion-diff-policy.md).

Requires GITHUB_TOKEN for the GitHub API; registry auth uses the standard OCI keychain, independent of GITHUB_TOKEN.

The trust split

No production deployment process holds both repository authority and the production target credential. The deployment path is split around an immutable, target-credential-free prepared artifact (prepared.deployment/v1): exact manifest bytes, the canonical bundle, and an integrity manifest binding both (docs/prepared-artifact-v1.md).

deployctl prepare production --out prepared/              # repository authority, zero target credential
deployctl deploy-prepared --prepared prepared/            # target credential, zero source access
deployctl rollback-prepared --from a/ --to b/             # two verified artifacts, no Git

The deploy side re-verifies the artifact fully — digest web, identity cross-checks, re-parse through the validation pipeline — before it contacts the target. deploy-prepared has no --repo-dir flag at all, and its tests prove deployment succeeds with no Git on PATH. A verification failure is a refusal before any production contact. rollback-prepared enforces the identical typed-confirmation authorization gate as ordinary manual rollback: prepared material changes where the bytes come from, never who authorizes recovery.

Deploy, rollback, status

The deployment side is a thin, deliberate wrapper over the engine: every decision — locks, staging, contract verification, marker discipline, state transitions — lives in the lifecycle engine; the CLI loads manifests, prepares bundles from the pinned revisions, connects with the target's declared transport, and renders the report.

deployctl deploy production                      # deploy the release the environment pins
deployctl rollback production --to 0.1.16        # emergency recovery; typed confirmation
deployctl status production                      # desired vs observed + all recovery facts
deployctl recovery resolve production attempt <id> --confirm "..."   # authorize resolution of an unresolved marker

Merging the promotion PR is the authorization for deploy; running the command is not. rollback requires typing exactly rollback <env> to <version> — deliberate friction for the emergency path. Normal rollback of a healthy deployment is an ordinary promotion with a reverse diff, not rollback.

Operational exit codes (deploy, prepare, deploy-prepared, rollback, rollback-prepared, status, recovery resolve): 0 success, 1 reported outcome failure (determined — history records what happened), 2 usage/configuration error, 3 infrastructure failure. The failure report — never the exit code alone — distinguishes pre-execution failures (nothing ran), bookkeeping failures after a committed state, and uncertain outcomes (an attempt/recovery marker is unresolved). Automation keys on the reported recovery state, not on exit 3.

Every operational command accepts the bare --json flag and then writes exactly one deployctl.result/v1 document to stdout on every terminal path — success, failure, refusal, uncertainty, infrastructure, or usage error — with no prose. Machine mode is non-interactive (--confirm required; stdin is never read). The frozen machine contract — envelope, outcome vocabulary, per-command data shapes and versioning policy — is docs/cli-v1.md.

For SSH targets, the manifest names environment variables and the environment holds values: credentialFrom and hostKeyFrom name variables whose values are paths to the private-key file and to the pinned host-key file (authorized_keys format). status fails closed: unreadable recovery evidence is DEGRADED EVIDENCE, never a silent HEALTHY, and a held lock dominates marker guidance.

For consumers

A consuming repository declares itself under .deploy/:

.deploy/
├── project.yaml                  what the project is and how a release is applied
├── releases/                     generated, immutable release manifests
│   └── my-app-0.1.17.yaml
├── environments/
│   └── production.yaml           human-approved desired state
└── targets/
    └── production-primary.yaml   where and how releases are deployed

Promotion is a one-line diff:

 spec:
-  release: .deploy/releases/my-app-0.1.16.yaml
+  release: .deploy/releases/my-app-0.1.17.yaml

Merge = authorize production. Rollback is the same diff in reverse.

Deployment runs through the reusable workflow, pinned to a full commit SHA — that pin is the single machinery trust anchor: both jobs build deployctl from the workflow's own commit, and the workflow refuses to run when invoked by anything else:

jobs:
  deploy:
    permissions:
      contents: read   # prepare: repository checkout
      actions: read    # deploy: run-artifact download
    uses: magtheo/deploy-toolkit/.github/workflows/deploy.yml@<full-sha>
    with:
      environment: production
    secrets:
      target_host: ${{ secrets.TARGET_HOST }}
      target_ssh_key: ${{ secrets.TARGET_SSH_KEY }}
      target_host_key: ${{ secrets.TARGET_HOST_KEY }}

Caller permissions are a ceiling — the reusable workflow cannot elevate past them, so permissions: {} on the calling job breaks deployment at run time. The two read scopes above are the documented floor (see docs/consumer-contract-v1.md).

Its prepare job holds repository authority and is not given the target credentials; its deploy job holds the credential and never checks out consumer source. The prepared commit is the invoking caller's commit — the caller is responsible for invoking deployment only from the trusted, promoted branch state. The workflow exposes the machine result as result (one deployctl.result/v1 document) and exit_code, whatever the outcome.

Start from templates/ and read docs/consumer-contract-v1.md; what a deployment target must provide — provider-neutrally — is docs/target-prerequisites.md.

A second reusable workflow, promotion.yml, classifies each transition as PROMOTION | ORDINARY | INVALID | ERROR so application CI runs only for work that needs it: a valid promotion-only PR (the two-line diff above) skips the rebuild/publish jobs, while anything else — including a mixed PR touching deployment state and source — gets full CI and fails the promotion gate. Promotion merge commits are deliberately unreleasable: they never publish artifacts, so they can never be mistaken for qualified source revisions.

Design invariants

  • Releases are immutable: source revision + exact artifact digests + bundle + deployment contract + migration semantics, one canonical manifest, no latest.
  • The deployment contract comes from the promoted release, never from main's current state. Old app + new deploy procedure is a bug class, not a feature.
  • The prepare/deploy trust split. No process holds both repository authority and the target credential; the deploy side consumes a fully verified prepared artifact and never touches source.
  • Strict host verification. Targets pin their SSH host key; insecure host-key options are never shipped.
  • argv, not shell strings. Lifecycle hooks are argv vectors or versioned scripts, never arbitrary shell programs.
  • AI never decides that production changes. Deterministic eligibility, human promotion, deterministic deployment.
  • No environment branches. main is the only branch; environments live in .deploy/environments/.
  • Consumers pin full SHAs. Readable releases are published above immutable SHAs.

See docs/trust-model.md for the full trust model and AGENTS.md for contributor/agent guidance.

Governance

Branches: main (permanent), plus fix/*, docs/*, chore/*, feature/* (with phase/* prefixes for substantial work). Substantial features get a plan first. Deterministic tests are required. AI review is advisory; humans hold merge authority.

License

Apache-2.0.

Directories

Path Synopsis
cmd
deployctl command
internal
lifecycle
Package lifecycle is the deployment state machine: it consumes the target substrate and turns a promoted, human-authorized release into the observed state of an environment.
Package lifecycle is the deployment state machine: it consumes the target substrate and turns a promoted, human-authorized release into the observed state of an environment.
oci
prepared
Package prepared implements the prepared deployment artifact — the immutable, verifiable, target-credential-free material boundary between the prepare stage (repository authority, zero target credential) and the deploy stage (target credential, zero source access).
Package prepared implements the prepared deployment artifact — the immutable, verifiable, target-credential-free material boundary between the prepare stage (repository authority, zero target credential) and the deploy stage (target credential, zero source access).
target
Package target is the server-side substrate the deployment state machine consumes: deterministic target paths, crash-aware release staging, the observed-state snapshot and the durable history log.
Package target is the server-side substrate the deployment state machine consumes: deterministic target paths, crash-aware release staging, the observed-state snapshot and the durable history log.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL