Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ on:

# Least privilege by default (audit T61): the workflow as a whole is
# read-only; only the publishing job escalates to contents: write.
concurrency:
group: teploy-release-distribution
cancel-in-progress: false

permissions:
contents: read

Expand Down Expand Up @@ -34,4 +38,34 @@ jobs:
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
- name: Check out the Homebrew tap
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
repository: useteploy/homebrew-tap
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
path: distribution/homebrew-tap
- name: Check out the Scoop bucket
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
repository: useteploy/scoop-bucket
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
path: distribution/scoop-bucket
- name: Render distribution manifests from release checksums
env:
RELEASE_TAG: ${{ github.ref_name }}
run: |
python3 scripts/render-distribution.py "$RELEASE_TAG" dist/checksums.txt distribution/homebrew-tap distribution/scoop-bucket
- name: Publish distribution manifests
env:
RELEASE_TAG: ${{ github.ref_name }}
run: |
for repo in distribution/homebrew-tap distribution/scoop-bucket; do
git -C "$repo" config user.name github-actions[bot]
git -C "$repo" config user.email 41898282+github-actions[bot]@users.noreply.github.com
if [ -f "$repo/teploy.rb" ]; then git -C "$repo" add teploy.rb; else git -C "$repo" add teploy.json; fi
if ! git -C "$repo" diff --cached --quiet; then
git -C "$repo" commit -m "Update teploy to $RELEASE_TAG"
git -C "$repo" pull --rebase
git -C "$repo" push
fi
done
40 changes: 5 additions & 35 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ version: 2
builds:
- main: ./cmd/teploy
binary: teploy
flags:
- -trimpath
- -buildvcs=false
env:
# Without this, the linked-against-glibc Linux binary fails to run
# at all inside any musl-based container (Alpine) with a bare "no
Expand Down Expand Up @@ -36,38 +39,5 @@ checksum:
changelog:
sort: asc

# NOTE (2026-05-29): goreleaser 2.16 flags `brews:` as deprecated but does
# not yet ship a 1:1 replacement (homebrew_casks rejects `install:`; no
# homebrew_formulae key exists). Leaving as-is until goreleaser publishes a
# clean migration target; release path still works (deprecation, not error).
brews:
# Pin the formula name to `teploy` (the binary + what users install). Without
# this, goreleaser derives it from the project/repo name — which became
# `teploy-cli` after the repo rename and split the formula (a stale
# `teploy-cli.rb` at 0.1.5 alongside the real `teploy.rb`). Keep it `teploy`.
- name: teploy
repository:
owner: useteploy
name: homebrew-tap
token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
homepage: https://github.com/useteploy/teploy-cli
description: Zero-downtime Docker deploys to any server via SSH
license: MIT
install: |
bin.install "teploy"
# Carried into the generated tap formula so `brew test teploy` (and the
# tap's CI) verifies the installed binary reports the formula's own
# version — the generated formulas previously shipped with no test
# block at all (audit useteploy__homebrew-tap-01).
test: |
assert_match version.to_s, shell_output("#{bin}/teploy version")

scoops:
- name: teploy
repository:
owner: useteploy
name: scoop-bucket
token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
homepage: https://github.com/useteploy/teploy-cli
description: Zero-downtime Docker deploys to any server via SSH
license: MIT
# Distribution manifests are rendered from verified archive checksums by
# scripts/render-distribution.py and published by the release workflow.
89 changes: 89 additions & 0 deletions AUDIT_OPEN.md
Original file line number Diff line number Diff line change
Expand Up @@ -2602,3 +2602,92 @@ Validation: full Go tests and vet; source-selection/rsync argument tests;
private-directory permission tests; real repository context checks for Ship,
Dash and Observe. This change does not remove historical uploaded copies or
rotate potentially exposed credentials; the doctor reports those separately.


## 2026-10-07 campaign reconciliation

Implemented the current CLI/distribution remediation set: CLI-01..09,
LIF-01..15, ADM-01..14, ENG-01..09, RCLI-01..07, CFG-01..03,
STA-01..03, DIST-01..04 and the CLI portion of TSEM-03. This includes
reviewed source snapshots and under-lock admission, durable duplicate webhook
admission/torn-tail repair, DR authority and key validation, compensated
lifecycle/migration, explicit literal environments, bounded secret-safe backup
verification, framed preview authority, and validated binary publication.
Historical checksum transcription was reconciled by rebuilding the exact
six-platform source revision; published v0.1.37 archives were independently
verified. Final integration status is recorded outside this public repository.

CC05 now has a supported additive pre-start `volume_ownership` map (see
`docs/volume-ownership.md`); existing volume maps keep their meanings. The
contract provisions only an empty managed root, preserves matching populated
roots, refuses populated mismatches and never recursively changes data.

Plan record schema 2 requires re-planning schema-1 records. Templates emit
`env_literal` and require a matching CLI revision. Provider enrollment-window
wording preserves deliberate external ACL ownership; it does not claim active
device access expires. Private audit material is not part of this repository.

Real consumer dependencies remain explicit: Nucleus must support
`shell --command-stdin --json` for private KV transport; older images refuse.
Observe can deduplicate stable `source_event_id` metadata for delivery retries.
Live integration and new release publication are separate from source fixes.

### Source correction follow-up

Reviewed-input mode binding, native/private-stdin DR restoration, unique owned
DR validation, retryable preview destruction, authoritative lifecycle selection
and fleet publication-outcome propagation have additional source corrections.
Managed volume admission now precedes preparation effects and uses no-follow
directory descriptors with explicit trusted-administrator ancestor admission.
SSH policy installation quotes the complete script and uses private invocation
inputs. The corresponding regression sources are present; compilation and
runtime acceptance must be completed for these new bytes before closure.

### Trigger convergence and compare-destroy — landing review (2026-10-07, r5)

The round4 mandatory engineering OPEN is implemented and locally validated.
`deploy`, `preview deploy` and `preview destroy` accept `--trigger-stdin
--json`: one bounded (32 KiB) duplicate-key-refusing schema-1 request whose
canonical domain-separated key binds target/source/repository/exact ref/full
commit/private execution binding/action; the target's enrolled identity is
proved from the target, never from a caller alias. Durable per-operation
receipts live target-resident under `/deployments/<app>/.operations` (0600,
O_NOFOLLOW, fsync+rename, completed receipts immutable), are written before
any preparation under the SAME renewable app lease, and replay reconciles
pending or unknown debt against authoritative state, immutable release/image
and binding before new preparation. Results are truthful
`not_started|not_committed|committed|unknown` with reconciliation status, and
the final JSON reaches stdout even on nonzero Cobra exits. Authenticated
webhook deliveries without a usable immutable commit are refused before
durable admission (C02); explicit tip mode can no longer be inferred from an
authenticated malformed event.

Preview records now carry a durable ownership ID, a monotonic per-app
generation and an updated-at token. Destruction compares the admitted
identity against authority re-read under the same lease — including between
prune's listing and the destroy — and refuses renewed previews before any
route, container or record effect; teardown proves route and container
absence (immutable IDs) before unlinking authority, tolerates proven-absent
retries, and never converts unknown inventory into absence. Legacy-era
records compare their all-empty identity triple exactly and remain prunable.

`teploy version --json` advertises `trigger-convergence-v1` and
`preview-compare-destroy-v1` (contracts corpus rev 9). Dash and other
consumers must keep cross-trigger consumption inactive until their own
pinned contract acceptance runs against an exact released artifact — the
capability token is the CLI-side handshake, not a consumer pass.

Executed locally: build, vet (plus the `integration` tag type-check), full
unit and race suites (27/27 packages, no races), gofmt on the round's paths,
and the Linux/Windows/Darwin compile matrix including the static
`CGO_ENABLED=0` Linux binaries the release config builds. Two completion
defects were fixed beyond the interrupted lane's bytes: DR cutover engine
environment now honors `env_literal` database passwords (previously restored
with an empty password), and legacy preview records no longer serialize a
zero `updated_at`. Still open and not claimed: live-DB/SSH integration
suites (env-gated), real Linux/SSH/Docker/Caddy acceptance, native Windows
and release publication, ADM-11 upstream acceptance, migrating the resident
autodeploy deploy path onto the trigger adapter, and scheduler/manual
tip-mode admission plumbing. Private evidence and exact receipts live
outside this repository.

6 changes: 6 additions & 0 deletions contracts/MANIFEST.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Neutron/Nucleus dependency and a public mirror.

| Corpus rev | Emitting CLI | Machine Interface | Notes |
|---|---|---|---|
| 9 | campaign based on 9939eaf, trigger-fix-r5 (uncommitted source patch; exact source digest in private report) | 2 | Additive. version-handshake advertises `trigger-convergence-v1` and `preview-compare-destroy-v1`: the reserved `--trigger-stdin` interface is now implemented (bounded schema1 stdin request, target-resident durable operation receipts reconciled under the app lease before any preparation, truthful committed/not_committed/unknown results with final JSON on nonzero Cobra paths) and preview destruction compares admitted ownership/generation against authority re-read under the same lease (renewal between listing and destroy is refused without effects). preview-state records/list rows may carry optional `ownership_id`, `generation`, `updated_at`, `operation_key`, `execution_binding_digest`, `source_revision`, `image_digest` (all absent on legacy records). Regenerated fixture: version-handshake mi2.json (two new capability tokens). Consumer activation (Dash) remains gated on its own pinned contract acceptance. No MI bump. |
| 8 | campaign based on 9939eaf (uncommitted source patch; exact source digest in private report) | 2 | Plan record schema 2; literal template environment; release/attempt identifier grammar parity; 128-bit preview ID with legacy lookup; additive managed `volume_ownership` for pre-start UID/GID/mode provisioning. |
| 7 | main (L4 cli-defects: ID-created container image resolution) | 2 | Additive. The container object in app-list-envelope and server-status-envelope gains optional `image_id` (full sha256) and `image_tags` (string array), emitted only for containers created by image ID (A52 creates web/worker containers from the immutable ID). For those, `image` now carries the first repo tag instead of the bare 12-hex short ID docker ps reports - restoring the pre-A52 meaning (Ship wave-9: ID-created containers never matched the artifact tag). Name-created containers and existing fixtures are unchanged (no fixture regenerated: the corpus fixtures use name-form images). No MI bump. |
| 6 | main (tailnet preview mode, DELEGATED_DECISIONS §10) | 2 | Additive. preview-state schema gains optional record/list-row fields on both eras (`domain`, `url`, `base_domain`, `http_only`, `allow_ips`) with the invariant url scheme = `http://` iff `http_only` (else `https://`); two valid fixtures GENERATED from the real `preview list --json` row encoder (`previewListRows`, `contracts_golden_test.go`): canonical-list-row (default mode, no exposure keys, https url) and canonical-list-row-tailnet (base_domain + http_only + allow_ips, http url), each wrapped with the artifact's `era`/`app` classification keys (the wire row carries neither). The hand-authored identity fixtures (canonical, legacy, ambiguous) are unchanged. version-handshake gains the `preview-exposure` capability token (additive). No MI bump. |
| 5 | main (X02 S2 tail: server-status fixtures + schema correction) | 2 | server-status-envelope fixtures landed (was "pending live capture"): valid x2 (full healthy observation, partial-caddy-unavailable — the class a target without a caddy container produces) + legacy pre-MI (machine_interface absent, the 42243e2-era shape). Encoder-derived: generated from the REAL `collectServerStatus` via a mock SSH executor (`contracts_golden_test.go`, TEPLOY_UPDATE_CONTRACTS) — synthetic values, real encoder and parse stages; the wire shape was verified against a live `server status --json` run before pinning. Defect fixed in the same commit: the schema had copied the appStatus root since its S2 draft (its own defect-fix commit 08cfb1b said so) and never described the actual serverStatusDTO wire format (server/host/uptime/load/memory/disks/docker/caddy) — rewritten to the real root with strict required-key coverage of the DTO's no-omitempty fields. Additive to consumers (a schema that matched nothing before now matches the wire); no MI bump. |
Expand Down Expand Up @@ -70,3 +72,7 @@ treats id-vanished as ambiguous-legacy requiring explicit re-binding,
never auto-re-mint). Consumers MUST NOT treat a missing
`machine_interface` field as MI 0 — it means "pre-MI producer", the
legacy decode path.

## 2026-10-07 contract update

PlanRecord schema 2 requires re-planning records from schema 1. The private config binding includes execution commands, hook contents and routing values; environment values retain the established exclusion policy. Static plans bind the selected source and exact plan output exclusion. Release and attempt identifier schemas now match runtime release IDs (1–128 grammar-valid characters); attempts append a dot and 16 hex nonce. Templates emit literal `env_literal` maps for rendered environment values, preserving user passwords through deployment.
1 change: 0 additions & 1 deletion contracts/fixtures/attempt-name/invalid/examples.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
[
"deadb17ecafef00d",
"ABC1234.deadb17ecafef00d",
"abc1234.DeadB17eCafef00d",
"abc1234.deadb17ecafef00",
"../escape.attempt0000000"
Expand Down
5 changes: 4 additions & 1 deletion contracts/fixtures/attempt-name/valid/examples.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
[
"abc1234.deadb17ecafef00d",
"9f31c02.0123456789abcdef"
"9f31c02.0123456789abcdef",
"ABC1234.deadb17ecafef00d",
"v1.2.3.deadb17ecafef00d",
"release_1.deadb17ecafef00d"
]
4 changes: 2 additions & 2 deletions contracts/fixtures/plan-record/invalid/tampered-id.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"schema_version": 1,
"plan_id": "e6e098f1e0322e3f",
"schema_version": 2,
"plan_id": "1eefc411f1a95ead",
"app": "myapp",
"server": "srv.example.com",
"server_name": "prod",
Expand Down
4 changes: 2 additions & 2 deletions contracts/fixtures/plan-record/valid/build.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"schema_version": 1,
"plan_id": "f5f4997ca665419e",
"schema_version": 2,
"plan_id": "3d6f4a1a3f9dd67d",
"app": "myapp",
"server": "srv.example.com",
"user": "root",
Expand Down
4 changes: 2 additions & 2 deletions contracts/fixtures/plan-record/valid/prebuilt-digest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"schema_version": 1,
"plan_id": "e6e098f1e0322e3f",
"schema_version": 2,
"plan_id": "1eefc411f1a95ead",
"app": "myapp",
"server": "srv.example.com",
"server_name": "prod",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"era": "canonical",
"expires_at": "2026-09-27T12:00:00Z",
"http_only": true,
"id": "myapp-p-08e81639",
"id": "myapp-p-08e8163960152ff19d4fc9c432d3270b",
"image": "myapp-build-abc1234",
"port": 49200,
"repo": "github.com/example/myapp",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"domain": "preview-feature-login-08e81639.myapp.com",
"era": "canonical",
"expires_at": "2026-09-27T12:00:00Z",
"id": "myapp-p-08e81639",
"id": "myapp-p-08e8163960152ff19d4fc9c432d3270b",
"image": "myapp-build-abc1234",
"port": 49200,
"repo": "github.com/example/myapp",
Expand Down
4 changes: 3 additions & 1 deletion contracts/fixtures/version-handshake/valid/mi2.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@
"plan-apply",
"preview-blue-green",
"preview-canonical-id",
"preview-compare-destroy-v1",
"preview-exposure",
"provenance-records",
"readiness-receipts",
"repair-debt",
"server-rename",
"server-status-machine",
"server-update",
"template-var-stdin"
"template-var-stdin",
"trigger-convergence-v1"
],
"machine_interface": 2,
"version": "v0.0.0-contracts"
Expand Down
4 changes: 2 additions & 2 deletions contracts/schema/attempt-name.schema.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://teploy.github.io/contracts/schema/attempt-name.schema.json",
"title": "deploy attempt directory name grammar: <hash>.<16hex> (F08)",
"title": "deploy attempt directory name grammar: <release>.<16hex>",
"type": "string",
"pattern": "^[a-f0-9]{7}\\.[a-f0-9]{16}$"
"pattern": "^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}\\.[a-f0-9]{16}$"
}
Loading
Loading