Skip to content

Releases: ersinkoc/SimpleDeploy

v0.1.1

Choose a tag to compare

@github-actions github-actions released this 31 Jul 10:08

Patch release for the post-release verification fixes:

  • The Docker-backed E2E test now waits for asynchronous webhook deploy goroutines
    before cleanup, preventing leaked qd-e2eprobe containers 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 contractwriteCaddyfile and an atomic
    writer emit byte-identical files, so only a real mount tells them apart.
  • The fresh-install path — every other state test points InitState at 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, CurrentImage still 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 every stop/restart/
    remove/redeploy of 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 with go 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 --port is 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 and remove. Locks are
    now released on SIGINT/SIGTERM.
  • A webhook push was answered 200 Deploy triggered and then dropped when a
    hand-run redeploy/remove held 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.
  • stop was still silently undone by a concurrent redeploy. UpdateApp
    protected the other fields but not Status, and neither stop nor restart
    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),
    so ps 1 on the host looked like a crash leftover.
  • An aborted init reconfigure 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 before printWebhookHelp, which is
    the only place either value is ever shown.
  • Branch deletions triggered deploys. GitHub delivers them as push events
    with a normal refs/heads ref plus deleted: true; the new non-branch gate
    did not catch them, so deleting the deployed branch kicked off a redeploy that
    then failed at git fetch. All-zero after (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 sets stop_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-Signature carries a bare
    hex HMAC-SHA256 digest, but verification required GitHub's sha256= 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-urlencoded delivery mode sends payload=<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 as push
    events with refs/tags/... and hit the same hole. Both are now handled: form
    bodies are parsed, and a non-branch ref is acknowledged without deploying.
  • WebhookEnabled was never enforced. An app deployed with push-to-deploy
    declined still auto-deployed on every push; list displayed Webhook: 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 (response 202).
  • Oversized payloads were reported as bad signatures. The 10 MB read cap
    truncated silently, so the HMAC was computed over partial bytes. Now 413.

Concurrency and state integrity

  • A removed app could be resurrected. remove during a webhook redeploy was
    undone when the redeploy's final save re-inserted its minutes-old copy of the
    record; a concurrent stop likewise had its status overwritten. Commands that
    own only part of a record now use the new state.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.
    ListenAndServe returns as soon as Shutdown is 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
    redeploy during a webhook-triggered deploy of the same app ran fully
    concurrently with it: both git 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
    and remove now 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 up after the image buil...
Read more

v0.0.8

Choose a tag to compare

@github-actions github-actions released this 03 May 09:35

Full Changelog: v0.0.7...v0.0.8

v0.0.7 - Bug Fixes & Security

Choose a tag to compare

@ersinkoc ersinkoc released this 03 Apr 08:16

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 GetApp to 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)
  • GenerateSecret now produces correct entropy (was producing half)
  • yamlQuote now escapes dangerous chars instead of rejecting them
  • Restart/Stop commands no longer load state twice

Performance

  • Restart/Stop: single state load instead of double
  • .env file now deterministically sorted for reproducible deployments

Code Quality

  • Dead code removal in detectNodePort

[0.0.6] - 2026-04-02

Security

  • Path traversal protection in .env file 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, ContainerExists wrapper, GetShortHash, DetectBranch, IsRepo, ParseGitHubEvent
  • Dead struct fields removed: Container, Port, ConnEnvKey from DatabaseConfig
  • 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

Choose a tag to compare

@ersinkoc ersinkoc released this 01 Apr 08:23

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

Choose a tag to compare

@ersinkoc ersinkoc released this 30 Mar 12:09

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 vet clean

Full Changelog: v0.0.2...v0.0.3

v0.0.2

Choose a tag to compare

@ersinkoc ersinkoc released this 30 Mar 10:08

SimpleDeploy v0.0.2

Security

  • Webhook JSON parsing: Replaced fragile string-based JSON extraction with proper encoding/json unmarshaling
  • 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_URL being overwritten when multiple SQL databases are provisioned (now uses per-type URLs like MYSQL_URL, POSTGRESQL_URL)
  • Decryption failures in deploy/redeploy now warn instead of silently ignoring
  • RunLogs now properly returns errors instead of swallowing them
  • Image cleanup goroutines now have panic recovery
  • Removed dead loop in git.go that did nothing
  • config.Init() is now called from main.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 vet clean

Full Changelog: v0.0.1...v0.0.2

v0.0.1 — Initial Release

Choose a tag to compare

@ersinkoc ersinkoc released this 29 Mar 23:37

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 deploy

Build from Source

git clone https://github.com/ersinkoc/SimpleDeploy.git
cd SimpleDeploy
go build -o simpledeploy .

🤖 Generated with Claude Code