Releases: ersinkoc/SimpleDeploy
Release list
v0.1.1
Patch release for the post-release verification fixes:
- The Docker-backed E2E test now waits for asynchronous webhook deploy goroutines
before cleanup, preventing leakedqd-e2eprobecontainers and cross-test output
pollution in the full opt-in integration suite. - The machine-ID fallback test now forces both Linux machine-id reads to miss
before asserting the hostname/user fallback, so CI and local runs exercise the
same branch.
Second audit pass, from a file-by-file review of every production source file
plus four independent adversarial reviews (concurrency, injection surface, CLI
flow logic, webhook protocol correctness against provider documentation) — then
a third pass reviewing the fixes themselves, which found and corrected several
regressions they had introduced.
Test coverage for the gaps that produced these bugs
Every gap CLAUDE.md named as the source of shipped bugs now has a test, each
covering something a unit test provably cannot. The Docker-backed ones are
opt-in (SIMPLEDEPLOY_INTEGRATION=1):
- What a container's environment actually receives — the only way to verify
the$-escaping contract, since Compose interpolates after the YAML parse and
a text assertion cannot see it. - Crash-loop detection against real Docker restart timing — a stub cannot
produce the restarting/running flicker that made a single status read report a
crash-looping deploy as a success. - The single-file bind-mount contract —
writeCaddyfileand an atomic
writer emit byte-identical files, so only a real mount tells them apart. - The fresh-install path — every other state test points
InitStateat a
directory that already exists, which is why "every first-ever init dies with
could not acquire state lock" shipped. - The full chain, end to end — a real deploy (real build, real container
answering HTTP), a real signed push over real HTTP to a real webhook server, a
redeploy that replaces the running version, then a push of a genuinely
crashing build proving the rollback fires: the previous version serves
again,CurrentImagestill names the last working image, and the rolled-back
attempt does not count as a deploy.
Found by running the suite the way CI runs it
Two defects survived a green local run and would have hit CI:
- The per-app lock required root. Lock files were placed under
/opt/simpledeploy/apps, which only root can create, and the lock was taken
before anything else — so for a non-root operator everystop/restart/
remove/redeployof an unknown app reported "mkdir /opt/simpledeploy:
permission denied" instead of "app not found". Locks now live in the state
directory (already a hard dependency of every command, and mounted into the
service container at the same path), and existence is checked before the lock
is taken. Invisible on Windows, where that path resolves somewhere writable. - A leaked test goroutine raced the package-level test doubles. One test
started a CLI command withgo Route(...)and never joined it; the goroutine
outlived the test and ran while other tests swapped the injectable vars. The
race detector caught it on Linux only — the race workflow could have gone red
at any time. The goroutine was never needed (an invalid--portis rejected
during argument parsing), so the call is now synchronous and its error
asserted.
Also: two unit tests failed outright when the docker binary was absent, which
made go test ./... red for anyone without Docker; they now skip instead.
Regressions found by reviewing the fixes (all fixed here)
- Ctrl-C at a wizard prompt locked an app out for 90 minutes. The new
per-app lock is held across the deploy wizard's interactive prompts, and Go's
default signal handling skips deferred functions — so interrupting left a lock
behind with a fresh timestamp, blocking both a retry andremove. Locks are
now released on SIGINT/SIGTERM. - A webhook push was answered
200 Deploy triggeredand then dropped when a
hand-runredeploy/removeheld the app lock: the deploy attempt failed
immediately and nothing retried, which is precisely the bug the in-process
queue was added to prevent. Lock contention is now retried within the deploy
budget. stopwas still silently undone by a concurrent redeploy.UpdateApp
protected the other fields but not Status, and neitherstopnorrestart
took the lock — so a redeploy restarted the container and overwrote the status.
Both commands now take the lock.- A failed lock write made release a no-op, leaving the app locked for 90
minutes; write/close errors now abort the acquisition. Same fix in the state
lock. - The contention message invited deleting a live lock. It reported a pid that
is namespace-local when the holder is the containerised service (typically 1),
sops 1on the host looked like a crash leftover. - An aborted
initreconfigure could leave the server with no proxy running.
The old proxy was stopped before the domain/email validators, which abort with
no retry loop — a typo took every app offline with no hint why. The stop now
happens after the new config is saved. - A crash-looping first deploy never printed the webhook URL or secret. The
new "not running correctly" branch returned beforeprintWebhookHelp, which is
the only place either value is ever shown. - Branch deletions triggered deploys. GitHub delivers them as push events
with a normalrefs/headsref plusdeleted: true; the new non-branch gate
did not catch them, so deleting the deployed branch kicked off a redeploy that
then failed atgit fetch. All-zeroafter(GitLab/Gitea) is handled too. - An unparseable payload deployed unconditionally. Failing to read the body
left the ref empty, which skipped every gate; it is now refused with 400. - The webhook body cap was raised from 10 MB to GitHub's own 25 MB delivery
limit — a lower cap only turned "never deploys" from a misleading 401 into an
honest 413. The generated service compose also setsstop_grace_period, without
which Docker SIGKILLed the container 10 s into a graceful shutdown that is
meant to drain in-flight deploys.
Push-to-deploy correctness
- Gitea signatures were never accepted.
X-Gitea-Signaturecarries a bare
hex HMAC-SHA256 digest, but verification required GitHub'ssha256=prefix.
Deploys only worked because modern Gitea also sends the GitHub-compatible
header. The bare form is now accepted (the prefixed one still tolerated). - The branch filter was silently dead in two cases. GitHub's
application/x-www-form-urlencodeddelivery mode sendspayload=<urlencoded JSON>, which the ref parser could not read — an empty ref skipped the branch
check, so pushes to every branch redeployed. Tag pushes arrive aspush
events withrefs/tags/...and hit the same hole. Both are now handled: form
bodies are parsed, and a non-branch ref is acknowledged without deploying. WebhookEnabledwas never enforced. An app deployed with push-to-deploy
declined still auto-deployed on every push;listdisplayedWebhook: false
the whole time. Such pushes now get 403.- Pushes arriving mid-deploy were dropped but answered
200 Deploy triggered. The provider's delivery log showed success for a commit that was
never deployed. One follow-up run is now queued (response202). - Oversized payloads were reported as bad signatures. The 10 MB read cap
truncated silently, so the HMAC was computed over partial bytes. Now413.
Concurrency and state integrity
- A removed app could be resurrected.
removeduring a webhook redeploy was
undone when the redeploy's final save re-inserted its minutes-old copy of the
record; a concurrentstoplikewise had its status overwritten. Commands that
own only part of a record now use the newstate.UpdateApp, which re-reads
under the lock and fails if the app is gone. - The state lock could be held by two processes at once. Unlocking removed
the lock file unconditionally, so after two waiters recovered the same stale
lock the loser tore down the winner's fresh lock. Unlock is now token-checked
and stale recovery re-stats before removing. - Graceful shutdown could kill a deploy it should have waited for.
ListenAndServereturns as soon asShutdownis called, so the in-flight
deploy wait could run before a handler had registered its deploy. - Two builds in the same second collided on one image tag, making the
deployed image nondeterministic. Tags now carry milliseconds. - Deploys of one app are now serialized across processes. A hand-run
redeployduring a webhook-triggered deploy of the same app ran fully
concurrently with it: bothgit pulled the same source tree, the loser's
rollback could revert the winner's successful deploy, and the winner's image
prune could delete the image the loser had just built.deploy,redeploy
andremovenow hold a per-app lock and fail fast, naming the holder, instead
of interleaving. This also closes the deploy-name race in which two wizard
sessions picking the same name both passed the "already exists" check and the
loser's cleanup deleted the winner's cloned source mid-build. - The webhook flood guard was tight enough to throttle real traffic. Behind
the proxy every delivery shares one bucket key, so the old 60/min was a global
ceiling a busy multi-app server could trip with its own pushes — after which
genuine deliveries got 429 and push-to-deploy silently stopped. Raised to
600/min; see CLAUDE.md for why a stricter failure-only bucket does not help.
Generated-config correctness
$in any value broke or silently changed the deploy. Compose
interpolates$VAR/${VAR}after the YAML parse: a bcrypt hash aborted
docker compose upafter the image buil...
v0.0.8
v0.0.7 - Bug Fixes & Security
Changelog
All notable changes to SimpleDeploy will be documented in this file.
[0.0.7] - 2026-04-03
Security
- YAML injection prevention: repo URL and branch now properly quoted in compose labels
- ACME email validation with regex in Traefik setup
- Environment variable key validation (must match
[A-Za-z_][A-Za-z0-9_]*) - IP extraction panic safety in webhook server
- Deep copy returned from
GetAppto prevent shared mutable state race conditions
Reliability
- MongoDB connection string fixed (missing database name in template)
- Webhook deploy goroutine leak fixed (timeout now waits for inner goroutine)
- Lock timeout increased 5s → 30s to prevent false stale detection on slow I/O
- Caddy block removal now tracks brace depth to correctly handle nested blocks
Bug Fixes
- Node.js Dockerfile now properly fails on build errors (removed
|| true) GenerateSecretnow produces correct entropy (was producing half)yamlQuotenow escapes dangerous chars instead of rejecting them- Restart/Stop commands no longer load state twice
Performance
- Restart/Stop: single state load instead of double
.envfile now deterministically sorted for reproducible deployments
Code Quality
- Dead code removal in
detectNodePort
[0.0.6] - 2026-04-02
Security
- Path traversal protection in
.envfile handling - YAML injection prevention in compose generation (
${,#, special chars blocked) - Caddyfile header value escaping
- Git token sanitization in error output
Reliability
- State file locking with stale-lock detection (cross-platform)
- Deploy lock race condition fix (context-based timeout replacing
time.AfterFunc) - Goroutine leak fix in rate limiter cleanup (ticker + stop channel)
- Proper error propagation in
ContainerStatus - Graceful token decrypt failure in redeploy (warn + continue instead of hard-fail)
Code Quality
- Dead code removal:
BuildImageWithDockerfile,TagImage,PullImage,ContainerExistswrapper,GetShortHash,DetectBranch,IsRepo,ParseGitHubEvent - Dead struct fields removed:
Container,Port,ConnEnvKeyfromDatabaseConfig - Container name helper consolidation (
docker.ContainerName) - Regex pattern consolidation (
state.AppNameRegex) - Go version fixed (1.26.1 → 1.23.0)
CI/CD
- Race detector workflow (
.github/workflows/race.yml) - Security scanner workflow (
.github/workflows/security.yml)
[0.0.5] - 2026-03-30
Changed
- Bump version to 0.0.5
- Remove dead code from codebase
[0.0.4] - 2026-03-28
Changed
- Bump version to 0.0.4
- Add dependency injection for testing across all packages
[0.0.3] - 2026-03-25
Fixed
- Sanitize git pull error output to prevent token leakage
- Use
getProxyDir()/getServiceDir()consistently
v0.0.5
SimpleDeploy v0.0.5
Changes
- Removed dead code (unused Entrypoint field from AppType struct)
- Bumped version to 0.0.5
- Added CLAUDE.md project documentation
- Maintained 100% test coverage across all packages
Test Results
- All 13 packages: 100% coverage
- All tests: PASS
Download
- Linux AMD64:
simpledeploy-linux-amd64 - Linux ARM64:
simpledeploy-linux-arm64 - Darwin AMD64:
simpledeploy-darwin-amd64 - Darwin ARM64:
simpledeploy-darwin-arm64 - Windows AMD64:
simpledeploy-windows-amd64.exe
v0.0.3
SimpleDeploy v0.0.3
Reliability
- Atomic state writes with fsync: State file is now written to a temp file, fsynced, then atomically renamed — survives power loss without corruption
- Post-deploy health verification: After container startup, verifies the container is actually running and warns if not
- Graceful shutdown: Webhook server catches SIGINT/SIGTERM, waits for in-flight deploys to complete before exiting
- Deploy WaitGroup: Server tracks active deploy goroutines and waits for all to finish on shutdown
Security
- Rate limiting: Per-IP rate limiter on webhook endpoint (60 req/min) prevents DoS via HMAC computation abuse
- Automatic stale entry cleanup: Rate limiter cleans up old visitor entries every minute
Stats
- 12/12 test packages passing
- 80.2% test coverage
go vetclean
Full Changelog: v0.0.2...v0.0.3
v0.0.2
SimpleDeploy v0.0.2
Security
- Webhook JSON parsing: Replaced fragile string-based JSON extraction with proper
encoding/jsonunmarshaling - YAML injection fix: All environment variable values in generated docker-compose.yml are now properly quoted
- Domain validation: Caddy proxy rejects malformed domains that could inject into Caddyfile
- Atomic state writes: State file writes are now atomic (write to tmp + rename) to prevent corruption on crash
- Deploy timeout safety: Webhook deploy goroutines release locks after 30 minutes even if hung
Bug Fixes
- Fixed
DATABASE_URLbeing overwritten when multiple SQL databases are provisioned (now uses per-type URLs likeMYSQL_URL,POSTGRESQL_URL) - Decryption failures in deploy/redeploy now warn instead of silently ignoring
RunLogsnow properly returns errors instead of swallowing them- Image cleanup goroutines now have panic recovery
- Removed dead loop in
git.gothat did nothing config.Init()is now called frommain.go(SIMPLEDEPLOY_DIR env var was dead code)
New Features
- CLI commands:
restart,stop,exec— previously listed in help but not implemented - Multi-provider webhooks: GitHub, GitLab, and Gitea push events all supported with proper signature verification
- Per-app deploy locking: Webhook server serializes deploys per-app instead of globally
- Ruby support: Dockerfile template for Ruby apps in buildpack
- Dynamic DB list: Deploy wizard now generates database options from
db.AvailableDatabases()instead of hardcoded list
Cleanup
- Removed unused
internal/compose/templates.go - Removed unused wizard color functions (
Blue,Cyan,Magenta) - Updated README with new commands and multi-provider webhook docs
Stats
- 12/12 test packages passing
- 81.2% test coverage
go vetclean
Full Changelog: v0.0.1...v0.0.2
v0.0.1 — Initial Release
SimpleDeploy v0.0.1 — Initial Release
March 30, 2026
Single-binary PaaS CLI for deploying applications with Docker.
Features
- Interactive init wizard (Traefik / Caddy reverse proxy)
- Deploy applications from Git repositories
- Auto-detect project type (Node, Go, Python, PHP, Ruby, Dockerfile)
- Automatic SSL via Let's Encrypt (ACME)
- Database support: MySQL, PostgreSQL, MariaDB, MongoDB, Redis
- Webhook server for automated redeployment (HMAC-SHA256 verified)
- Systemd service installation
- Application logs, status, and management
Security
- App name validation (path traversal / injection prevention)
- HMAC-SHA256 webhook signature verification
- Docker command timeouts (prevents indefinite hangs)
- AES-256-GCM encrypted credential storage
- Token passed via environment variable (not embedded in scripts)
- Automatic security headers on every application
Binaries
| Platform | File |
|---|---|
| Linux x86_64 | simpledeploy-linux-amd64 |
| Linux ARM64 | simpledeploy-linux-arm64 |
| macOS x86_64 | simpledeploy-darwin-amd64 |
| macOS Apple Silicon | simpledeploy-darwin-arm64 |
| Windows x86_64 | simpledeploy-windows-amd64.exe |
Quick Start
chmod +x simpledeploy-linux-amd64
./simpledeploy-linux-amd64 init
./simpledeploy-linux-amd64 deployBuild from Source
git clone https://github.com/ersinkoc/SimpleDeploy.git
cd SimpleDeploy
go build -o simpledeploy .🤖 Generated with Claude Code