Repository navigation
feat: git code sources for Teploy-home apps and the single poll admitter (D4) - #44
Merged
Merged
Conversation
…ll} and build_secrets A Teploy-home app may build from a git repository instead of a pre-built image: git: names the repository (ssh://, scp-style, https://, or file:// as a privileged same-server source), the branch the admitter polls, an optional monorepo path filter, and the poll interval (>= 1m, or "off"). With git: set, image: becomes optional — and setting both is refused, as is git: in a folder app (the folder loader rejects it), static + git, submodules: true (not implemented), embedded URL passwords, and unsafe transports (http://, git://). build_secrets: names age-store keys the repository build passes as docker build --secret. confighome.Load drops the image-required rule when git: is set and keeps refusing static apps with a code source. Changing to or from a file:// repository counts as a privileged change (it reads server-local paths with the deploy user's rights). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mitter
One server-side python helper (over the app's executor, like the
confighome/events helpers) owns everything git:
- credentials per app, age-encrypted OUTSIDE /deployments
(/etc/teploy/apps/<app> as root, ~/.config/teploy/apps otherwise):
an ed25519 deploy key (public half + fingerprint out, private half
age-encrypted, idempotent until --rotate), an HTTPS token read
from stdin (never argv, used via GIT_ASKPASS, credential helpers
disabled), and the forge host key pinned via ssh-keyscan (with
optional --expected-fingerprint verification). Fetches run with
StrictHostKeyChecking=yes against that pin; the age identity is
tried at /etc/teploy/age-key then /deployments/.age-key (W2's
move) and decrypted keys live in a 0600 tmpfs file for exactly
one git invocation.
- a per-app bare mirror under .teploy/apps/<app>/mirror.git holding
only code, with watermark refs (refs/teploy/wm/<branch>) keeping
ancestry-checkable commits reachable across force-pushes, and
git archive export of the exact admitted commit plus remote
Dockerfile/submodule detection.
- admissions state under admissions/ (flock'd, atomic): Poll is the one
admitter — dedup by (app, ref, commit), per-ref watermark with an
ancestry check, ONE recorded refusal per force-push (refused until
ResetRef), changed paths diffed on the server and filtered by
git.paths defaulting to context:, failed commits retried at most
twice (RetryClear re-arms), the newest admitted head always
deployed, repository changes voiding stale watermarks, and
manual-deploy helpers (ResolveHead, RefusedHead,
RecordManualDeploy) so follows the same
rules.
Integration tests drive the real helper over a LocalExecutor against
file:// bare-repo fixtures: admit/dedup/supersede+hint, interval and
poll: off, force-push refusal + reset, path filter, retry budget,
repository change, branch loss, watermark recording, and the pure path
matcher.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…imer, git builds
- teploy config source key --type ssh|token (token from stdin),
host-key (pin + verify), show --json (public facts only) and remove;
creating a credential offers the server poll timer (best-effort).
- teploy trigger poll (--app | --all) [--now] [--hint] is the one
admitter's surface: it holds the app's fenced lock, refuses nothing
quietly (JSON reason vocabulary: deployed, up-to-date, not-due,
poll-off, no-relevant-changes, force-push, retry-limit,
branch-missing, failed, locked), and deploys through the normal
engine. trigger reset clears a ref's watermark; trigger retry re-arms
a failed commit; install-timer/uninstall-timer manage an idempotent
systemd pair (teploy-trigger-poll.{service,timer}) running
'trigger poll --all' every minute as the connecting user, with the
resident binary proven to support 'trigger poll' first (or a dev
upload via --binary).
- Deploying a Teploy-home app with git: (teploy deploy --app ...) now
resolves the branch head under the lock, exports the commit from the
mirror into the attempt's build dir, builds server-side (build_secrets
decrypted to 0600 files outside the context, mounted with
DOCKER_BUILDKIT --secret, removed right after), records the commit as
source_revision, honors force-push refusals and advances the
watermark; poll reuses the same locked path. remove --purge forgets
the credential (it lives outside /deployments).
- events: trigger.admit gains a reason detail; new source.connect kind
(action/type/fingerprint). doctor gains a git-trigger check (timer
present + binary capable when git: apps exist). Capability tokens
git-source-v1 and trigger-poll-v1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New artifacts (schemas + GENERATED fixtures from the real DTOs): source-show-envelope (config source show --json), source-key-envelope (config source key --json, ssh + token) and trigger-poll-envelope (trigger poll --json result: one object per app, reason vocabulary). version-handshake advertises git-source-v1 and trigger-poll-v1 (regenerated mi2.json). README gains a "Code from a git repository" section (works with GitHub, GitLab, Forgejo/Gitea, Cursor Origin, any git host over SSH/HTTPS; static + git still refused; submodules pending) and the CHANGELOG records the feature. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nary A server that never ran autodeploy or heal has no /deployments/.bin, and installServerBinary's flock on <destination>.install.lock then fails with ENOENT — found in the infra-home live check (install-timer on a fresh server). mkdir -p the directory first for both the release download and the --binary dev upload. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ic+git, repository authority) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rigger D4 corpus renumbered to rev 16 (D2 = 14, D3 = 15). Restores the rev 13 MANIFEST row (config-merge-envelope, v0.1.44) that the D3/D2 merge commit cd981fe dropped from main, and corrects the D3 CHANGELOG entry to rev 15. Hand-resolved: doctor check lists and tests (git-trigger alongside backup-cron and age-identity), capability tokens, event-kind allowlist, contracts golden test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Decision D4 of the P3 plan (CLI side): Teploy-home apps can build from a
git repository — GitHub, GitLab, Forgejo/Gitea, Cursor Origin, or any git
host over SSH/HTTPS — with one server-side admitter.
git: {repository, branch, paths, poll, submodules}(Teployhome only; folder loader refuses it). With
git:set,image:becomesoptional (and setting both is refused).
context:/dockerfile:arerelative to the repo root.
build_secrets: [KEY]passes age-store valuesas
docker build --secret— never in config, layers, argv or logs.teploy config source key --app … --host … [--type ssh|token]): ed25519 deploy key generated on the server (publichalf printed, private half age-encrypted outside /deployments at
/etc/teploy/apps/<app>as root,~/.config/teploy/appsotherwise), oran HTTPS token read from stdin (used via
GIT_ASKPASS, never inURLs/
.git/config/argv).config source host-keypins the forge's SSHhost key (ssh-keyscan server-side, fingerprints shown,
--expected-fingerprintverification); fetches runStrictHostKeyChecking=yesagainst it. The age identity is tried at/etc/teploy/age-keythen/deployments/.age-key(aligns with W2'smove).
config source show --json(no secrets) andremove.(
/deployments/.teploy/apps/<app>/mirror.git, code only) → fetch withthe app credential →
git archive <commit>into the attempt build dir →existing builder. Watermark refs keep ancestry-checkable commits
reachable across force-pushes. Submodules off:
.gitmodulespresent →explicit error (
submodules: truerefused at validation).teploy trigger poll --app X --host S [--now] [--hint <sha>],--allfor the timer): fenced app lock; ls-remote withthe credential; hint only wakes (remote head wins); dedup by
(app, ref, commit); per-(app,ref) watermark + ancestry check on the
mirror; force-push → ONE recorded refusal per (ref, head), refused until
teploy trigger reset --app X --ref R; changed paths computed on theserver (
git diff --name-only) filtered bygit.pathsdefaulting tocontext:; failed commit retried at most 2×, then only a new commit,teploy trigger retry, or a redeploy; superseding always deploys thenewest admitted head. Records under
/deployments/.teploy/apps/<app>/admissions/(flock), eventstrigger.admit/trigger.refusewith{ref, commit, reason}.--json→{app, ref, head, admitted, reason?, deployed_release?}.teploy trigger install-timer --host Sinstalls anidempotent systemd pair running
/deployments/.bin/teploy trigger poll --allevery minute (per-app intervals honored via admissionstimestamps); offered after
config source key(best-effort);doctorgains agit-triggercheck (timer present, resident binarysupports
trigger poll). Nothing per-app in crontab.teploy deploy --app Xwithgit:builds thebranch head from the mirror under the lock, records the commit as
source_revision(release provenance + deploy event), honors the sameforce-push refusal, and advances the watermark.
git-source-v1,trigger-poll-v1; contracts corpus rev 14(source-show-envelope, source-key-envelope, trigger-poll-envelope,
regenerated version-handshake); events:
trigger.admitreason detail,new
source.connectkind.Deliberate deviations (documented in README/CHANGELOG): static
Teploy-home apps with
git:stay refused;git.submodules: trueisrefused pending implementation;
file://repositories are accepted as aprivileged config change (same-server sources; also what the tests
use);
image:+git:together is refused rather than choosing one.Testing
go vet ./...,go test -race ./...green locally.LocalExecutor against local bare-repo fixtures: poll admit/dedup,
supersede + stale hint, interval/
poll: off, force-push refusal +reset, path filter, retry budget +retry, repository change,branch loss, manual-deploy watermark, path matcher, config/confighome
validation, build
--secretcommand shape, doctor check, contractsgoldens.
w4probethrowaway app (cleaned upafter): ssh deploy-key generation, host-key pin, poll admit + deploy
(whoami image), second commit poll, force-push refusal + reset, path
filter, timer install/uninstall.
🤖 Generated with Claude Code