Jordach MakayaOperations leader · Backend & systems engineeringCasablanca · FR / EN
All tools
Hard Machine Labs · Developer tool

Shipset

Deterministic delivery for TypeScript projects.

An open-source CLI and TypeScript library, extracted from a delivery pipeline that ran in production. It does not replace GitHub Actions, GitLab CI or Dokploy. It adds a small decision layer between a repository change and the deployment call.

Extracted from productionOpen source · MITCLI + TypeScript library
The whole product, in one line
  1. 01Git diff
  2. 02Impacted services
  3. 03Architecture check
  4. 04Image
  5. 05Deploy
  6. 06Health
Version
v0.1.0
Status
In preparation · npm planned
Core
4 pure functions + 1 adapter
Adapter
Dokploy · one or several VPS
Walkthrough · 6:43French audio
Shipset walkthrough, narrated in French · 720p · 6 min 43 sOpen the video file ↗
01The familiar start

You copy the last pipeline, rename a few things, and ship.

A new project reaches the CI/CD step. You rarely start from zero. You open a previous project and reuse what already works.

  1. 01Open a previous project.
  2. 02Copy its pipeline.
  3. 03Rename a few services.
  4. 04Update paths, registry and secrets.
  5. 05Ship.
the usual bootstrap
old-project/
.github/workflows/
deploy.yml
new-project/
.github/workflows/
deploy.yml ← copied, edited

It works. That is exactly why the habit sticks. The trouble does not show up on day one.

02Months later

The repository moves on. The pipeline does not.

New services appear. Packages move. A shared contracts package shows up. The copied pipeline still carries a partial model of the older system.

Repository reality
  • billing
  • claims
  • portal
  • notifications
  • shared-contracts
What the pipeline assumes
  • billing
  • claims
  • portal
  • notifications?
  • shared-contracts?
This gap is drift: the project changed, and part of the automation did not follow.
03The problems

Drift turns into a familiar list of problems.

  • 01

    Hand-maintained service inventories drift from the repository.

  • 02

    CI rebuilds more than the change requires.

  • 03

    Architecture rules live in documentation, where nothing enforces them.

  • 04

    Deployment configuration becomes inconsistent across targets.

  • 05

    A successful deployment trigger is taken as proof of a healthy service.

  • 06

    Secret handling is too easy to get wrong.

So the question changes

How do I deploy this project?

How does the pipeline know what needs to be deployed, whether the change respects the architecture, and whether the result is healthy?
04What Shipset is

A small decision layer between a change and a deployment.

Your CI still runs the jobs. Dokploy still deploys the containers. Shipset sits between them and makes the decision explicit. It answers six questions, in order, before and after the deployment call.

Your CI
GitHub Actions or GitLab CI runs the jobs
Shipset
Decides what to build, check, deploy and verify
Dokploy
Deploys the image on the VPS
  1. 01What exists in the repository?discoverWorkspace
  2. 02What changed?git diff
  3. 03What is impacted?shipset detect
  4. 04Is the architecture still valid?shipset lint-arch
  5. 05What exactly should be deployed?shipset plan
  6. 06Did the deployed service become healthy?shipset deploy

Under the hood: four pure functions (discoverWorkspace, detectImpactedServices, lintArchitecture, planDelivery) and one Dokploy adapter. One validated config file, shipset.config.yml. No daemon, no dashboard, no state store.

05Where it comes from

Extracted from a pipeline that already ran in production.

The original was a set of shell scripts for a pnpm + Turborepo monorepo, deployed to several VPS through Dokploy. It had the right operational ideas. Too many of them lived in hand-maintained files.

The extraction kept the ideas and replaced the assumptions with a smaller, typed system.

service-paths.sh
a hand-written service map
discoverWorkspace
The map is derived from the repository instead of maintained next to it.
ci-detect.sh
diff to services, with fallbacks
detectImpactedServices
Prefix matching made sibling-safe: api no longer matches api-v2.
lint-zero-trust.mjs
layer rules hardcoded
lintArchitecture
Rules come from config. It still blocks the merge.
ci-deploy.sh
webhooks and health checks
planDelivery + Dokploy adapter
Planning is pure and testable. Execution is a separate step.
deploy.targets.json
tokens stored in the repository
delivery.targets + tokenEnv
The repository names a CI variable. The value stays in CI.

The goal was never a new CI platform. It was a decision path that is explicit and testable.

06Replay the same project

Back to Acme, one step at a time.

Acme is a fictional insurance platform: a pnpm + Turborepo monorepo with a shared contracts package, two product services and a web portal. Same repository as before. This time, the workflow starts from the repository itself.

acme/monorepo
acme/
pnpm-workspace.yaml
packages/
shared/
contracts/
product/
billing/
claims/
web/
portal/
package.json
  1. Step 01shipset init

    Initialize once.

    init writes shipset.config.yml and copies the CI templates and a Dockerfile into the repository. They are plain files you own and can edit.

    The config is validated on every command (schema shipset/v1). A mistake stops the run with a line that says how to fix it.

    terminal
    $ pnpm dlx shipset init✓ shipset.config.yml✓ .pipeline/ci/gitlab/✓ .pipeline/Dockerfile
    shipset.config.ymlyaml
    project:  name: acme  structure: monorepo
  2. Step 02shipset.config.yml

    Architecture as config

    You describe the boundaries, not a copy of the service list. Here: web may import product and shared; product may import shared.

    A reverse dependency such as product → web can be rejected. Business code should not start depending on the web interface.

    shipset.config.ymlyaml
    layers:  - name: shared    path: packages/shared    shared: true    allow: []   - name: product    path: packages/product    allow: [shared]   - name: web    path: packages/web    allow: [shared, product]
    Allowed imports
    web
    product
    shared · shared: true

    product → web · rejected

  3. Step 03discoverWorkspace

    Shipset derives the workspace from the repository.

    For each package it reads the name, path, version, layer, framework and dependencies straight from the repository and its package.json files.

    The service map follows the repository instead of being copied from a previous project. Packages in a shared layer are not deployed on their own.

    Discovered workspace
    ServiceLayerDeployable
    billingproductyes
    claimsproductyes
    portalwebyes
    contractssharedno
  4. Step 04shipset detect

    A developer changes only Billing.

    detect maps each changed path to the discovered workspace. One file changed in Billing, so Billing is the only service to build and ship.

    The decision comes from git changes and repository structure, not from semantic similarity. The same input always gives the same answer.

    git diff
    M packages/product/billing/src/invoice.service.ts
    • billingImpacted
    • claimsNot impacted
    • portalNot impacted
  5. Step 05shipset lint-arch

    Check that the change did not erode the architecture.

    TypeScript can compile and tests can pass while the architecture quietly degrades. lint-arch checks every internal dependency between packages against the declared layers.

    A violation stops the merge request, with a hint on how to fix it.

    Allowed
    • web → product
    • web → shared
    • product → shared
    Blocked
    • product → web
    terminal
    $ shipset lint-arch✗ product → web  hint: use an API, an event        or a shared contractexit 1
  6. Step 06shipset plan vps-prod

    Plan before side effects.

    The plan says what will be deployed, where it goes and how its health will be checked. The image tag comes from the version in package.json.

    Planning is pure: nothing is called and nothing is built. You can read the decision before anything happens.

    terminalsimplified
    $ shipset plan vps-prodservice: billingimage:   registry/acme/billing:1.4.2target:  vps-prodhealth:  /health
  7. Step 07shipset deploy vps-prod

    Deploy through Dokploy.

    Shipset sends the image reference to each configured Dokploy webhook. One target is one VPS; a second VPS is a second target, on the same code path.

    The token never appears in the config. tokenEnv names a CI variable. The value is read from CI only when the request is made, and never logged.

    1. Shipset
    2. Dokploy webhook
    3. New container
    shipset.config.ymlyaml
    webhooks:  - url: https://...    tokenEnv: DOKPLOY_TOKEN
  8. Step 08health check

    Deployed does not mean healthy.

    A webhook that answers 200 only means the request was accepted. The container can still crash, or never become ready.

    So Shipset checks the configured health endpoint, with a retry budget (20 attempts, 5 seconds apart by default). A failed webhook skips the health check, and the job exits with an error.

    After the webhook
    1. webhook 2xx
    2. GET /health · retry until the budget runs out
    3. healthy → successnot healthy → failed

Outputs are simplified for reading. The CLI prints JSON for detect and plan.

07When shared code changes

A change in shared code has a wider blast radius.

Replay the same repository, but change a shared contract instead of Billing. The shared layer is marked shared: true, so Shipset deliberately widens the impact to every deployable service.

Local change
git diff
M packages/product/billing/src/invoice.service.ts
  • billingImpacted
  • claimsNot impacted
  • portalNot impacted
Shared change
git diff
M packages/shared/contracts/src/customer.ts
  • billingImpacted
  • claimsImpacted
  • portalImpacted

A Billing change can stay local. A shared contract change can affect everything. Shipset shows that blast radius before the deployment, not after it.

08On a monolith

The same workflow on a monolith.

Not every project is a monorepo. With a single-package repository there is only one deployable unit, so impact detection is simple. The validated config, the explicit plan, the secret handling and the health check stay just as useful.

claims-api/monolith
claims-api/
src/
claims/
customers/
payments/
package.json
Dockerfile
terminalsimplified
$ pnpm dlx shipset init$ shipset detect→ claims-api$ shipset plan vps-prod→ registry/acme/claims-api:2.1.0$ shipset deploy vps-prod→ webhook 2xx→ health OK
shipset.config.ymlyaml
project:  name: claims-api  structure: monolithic delivery:  registry: registry.example.com/acme  targets:    - name: vps-prod      adapter: dokploy      webhooks:        - url: https://...          tokenEnv: DOKPLOY_TOKEN
09Agent skill

The same procedure, written for coding agents.

Shipset ships with a skill for coding agents: an operating manual the agent reads before it touches the repository. It covers adopting Shipset in a project and working on Shipset itself.

A developer can ask:

prompt
Add Shipset to this repository. Billing and Claims deploy to our Dokploy VPS.Do not commit secrets.

The skill then walks the agent through an explicit procedure:

  1. 01Inspect the repository.
  2. 02Decide whether it is a monorepo or a monolith.
  3. 03Initialize Shipset.
  4. 04Configure the project, layers and delivery targets.
  5. 05Reference secrets through tokenEnv names, never values.
  6. 06Run detect, lint-arch and plan.
  7. 07Wire the matching CI assets.
  8. 08Verify the result before proposing the change.

The point is not automation for its own sake. The human documentation and the agent skill describe the same system, so the agent has less to guess.

10Scope — intentionally small

What it does, and what it does not try to be.

It does

  • Discover the workspace structure
  • Detect impacted services from a git diff
  • Enforce architecture rules
  • Plan delivery before side effects
  • Deploy through Dokploy (one or several VPS)
  • Verify health after deployment
  • Provide GitHub Actions and GitLab CI assets
  • Provide a skill for coding agents

It does not try to be

  • A dashboard
  • A database
  • Kubernetes
  • A secrets manager
  • A migration engine
  • A canary or blue-green orchestrator
  • A generic AWS / GCP / Azure abstraction

The current path

  1. repository
  2. CI
  3. Shipset
  4. Dokploy
  5. VPS
  6. health

The first adapter is Dokploy because that is where the pipeline came from. The rule for anything else: one adapter at a time, validated in production before the next.

11Design principles

Six rules behind the code.

  • 01

    Derive instead of duplicate.

    If the repository already contains a fact, derive it instead of maintaining another inventory.

  • 02

    Make boundaries executable.

    Architecture documentation is useful. Architecture rules that can fail CI are harder to ignore.

  • 03

    Separate decision from action.

    The plan should be inspectable before anything with side effects runs.

  • 04

    Prefer deterministic context.

    Impact comes from repository structure and git changes, not from approximate semantic retrieval.

  • 05

    Fail early.

    A configuration error should stop the workflow before deployment, not halfway through it.

  • 06

    Make blast radius visible.

    What changed, what does it affect, and what exactly are we about to ship?

The idea

Make the decision explicit before automating the action.

Calling a deployment webhook is the easy part. What matters is that the pipeline can answer, in order: what exists, what changed, what is impacted, what is allowed, what ships, and whether it became healthy.

My role — Author and maintainer, Hard Machine Labs.Shipset · open source · MIT · v0.1.0

Hear the story

Podcast cover, Episode 00: Make the decision explicit before automating the action. A diagram shows repository sources feeding the Shipset typed decision layer, then deploy, then verify health.
Podcast · Episode 00 · 5:35

Make the decision explicit before automating the action.

Why a copied pipeline drifts, what it costs, and the story of extracting Shipset from a production system — from shell scripts to a typed decision layer.

12Demonstration

Demonstration

A recorded run of the full workflow, from init to a verified health check.

Coming soon

Shipset · demonstration

init → detect → lint-arch → plan → deploy → healthVideo · coming soon

Have a pipeline that drifted?

Questions about Shipset, delivery pipelines, or a problem worth exploring together.

Discuss this tool
Back to Hard Machine Labs