Skip to content

Latest commit

 

History

History
executable file
·
229 lines (174 loc) · 53.7 KB

File metadata and controls

executable file
·
229 lines (174 loc) · 53.7 KB

Changelog

v1.3.12

  • Nested Agent And DLUX Commands: Replaced flat agent/DLUX/self routes with agent check/update/restart/off/watch/run/enable, dlux check/update/rollback, self update, and executor run/enable; leading -f/-d still route to nested commands, generated service commands use agent run and executor run, and wrapper history advances to version 2.

v1.3.11

  • Rollback Chose The Wrong Release: staged_versions() ordered release directories as strings, so 1.8.10 sorted below 1.8.9 — the first two-digit patch would have sent a rollback to the wrong release and made prune_releases drop the wrong one. Ordering is numeric now (version_sort_key, mirroring the candidate sort in dlux_release_source). The rollback target is also restricted to releases strictly below the active one, which is what the function always claimed: taking the newest staged release that merely differed rolled forward onto the release the deployment had just stepped back from. 4 tests.
  • The Executor Socket Reports When It Is Actually Listening: _bind() creates the socket file at bind() and only starts accepting at listen(), so anything waiting on the path alone called it ready during the window in between, where a connect is refused outright. Executor.wait_until_listening() blocks on the real thing; the server test waited on os.path.exists and failed in CI as ConnectionRefusedError.

v1.3.10

  • dlux-update Works From The Project Root: ./start.sh dlux-update failed on every correctly deployed stack with "No DjangoLux runtime volume at /opt/dlux-runtime" — that path is a container mount, and the wrapper's composer has none. Composer now finds the volume behind the runtime root in the merged compose config (<project>_dlux_runtime, or an explicit name:), refuses to continue if Docker does not already have it (docker run -v would create an empty one), and re-runs itself in a sibling container with that volume, the project directory, the Docker socket and the deployment's secrets attached. The deployer CLI is the only place with both outbound network and Docker authority; the child carries --no-delegate so it can never loop. 22 tests.
  • Inline Updates Without Putting The Executor On The Internet: composer-executor ran the package-update trigger but sits alone on the internal: true docker_proxy network, so it could never fetch a wheel — the automated path could not work at all. The two halves now split it: composer-agent owns package-update-request.json, resolves the release, verifies the attestation and digest and stages the wheel in downloads/ on the runtime volume, then sends dlux_package_apply (version, filename, sha256) over the private socket; the executor re-hashes the staged bytes, re-reads the wheel's manifest, activates, restarts and health-gates it entirely offline. The digest travels over the socket, not in a file beside the wheel, because celery mounts that volume read-write too. dlux_package_rollback needs no staging. Exit 3 still means "needs a human". New --staged-wheel/--staged-sha256 also give an operator an air-gapped apply. Update the resident pair together, as agent-update already does.
  • Schema-2 Releases Could Not Be Activated: DluxRuntime.verify_release read inline_safe straight off the wheel's manifest, but schema 2 derives it from install.inline + migrations.effect + rollback_compatible and never carries the key — so every current release failed at the last step of the swap, after being fetched, verified and staged ("Release 1.8.7 does not declare inline_safe"). It now defers to the same normalize_manifest both halves of the update path use.
  • The Image Can Verify A Release Again: dlux_release_source mirrors django-lux[updater]'s refusal to install an unverified wheel, but the image never shipped pypi-attestations — so every dlux-update --check, --dry-run and apply failed closed with "attestation verification is unavailable". The image now installs it at the same floor as the DjangoLux extra (>=0.0.29, +95MB).

v1.3.9

  • check --fix Retires Local DLUX Tools Wiring: The guarded Compose transform now rewrites retired tools.dlux_runtime_supervisor and tools.smtp_relay module commands to dlux.updater.supervisor and dlux.smtp_relay, removes generated local tools/ bind mounts targeting /app/tools or the scoped SMTP relay path, and still adds the dlux_reconcile guard for surviving dlux-updater services. The fix is idempotent, uses the existing docker compose config validation plus .xpose/ backup path, and gates the rewrite on the highest packaged DLUX module floor it needs (1.6.2 for the supervisor, 1.7.0 for the SMTP relay).
  • check --fix Covers Current Scaffold Drift: Legacy composer-updater stacks now converge to the hardened composer-executor topology in one run, dlux-updater retirement strips native web.post_start hooks instead of converting them to stale labels, generated services gain missing org.dlux.restart labels, and check -d --fix normalizes compose.dev.yml so the dev override cannot reintroduce dlux-updater, force Celery's dlux_runtime mount read-only, or leave inline updates enabled in development.

v1.3.8

  • DjangoLux Manifest Schema 2: Inline package checks now understand schema-2 install, migration, and service requirements, reject unsupported deployment contracts, and enforce the manifest's minimum Composer version.
  • Release Test Dependency: CI and tag workflows install pinned PyYAML before full test discovery, so Compose transform tests run in clean release environments.

v1.3.7

  • check --fix Retires dlux-updater On Existing Stacks: new guarded compose transform that removes the service, moves its runtime reconcile and migrations into Compose init containers (pre_start) on celery, strips the now-orphan depends_on edges, grants celery write access to the runtime volume and staticfiles, and drops web's org.dlux.post-start migrator hook — which would otherwise be a second, redundant run now that the same work happens before start. Named volumes and their releases are kept. Double-gated, because either mistake is destructive: it refuses when the host's Compose predates 5.3.0 (it would ignore pre_start and boot the stack unmigrated) and when the project image still ships DjangoLux below 1.8.0 (that service is then the deployment's only update path). Handles a project whose scaffold block markers were hand-edited away, and removes a depends_on mapping left with no children rather than emitting invalid YAML. Idempotent. 22 transform tests + 6 gating tests, all mutation-verified, with the migrated file validated by docker compose config itself rather than only parsed.
  • Compose 5.3.0 Enforced For DjangoLux Stacks: generated projects run their runtime reconcile and migrations as Compose init containers (pre_start), so check FAILs a host whose Compose plugin predates 5.3.0 — an older plugin ignores the key rather than rejecting it, which would start the stack with no migrations applied and every gated service waiting forever. Version parsing tolerates a v prefix, prerelease suffixes and short forms (5.3 means 5.3.0, which the bare tuple comparison would otherwise refuse). Composer's only remaining part in the boot chain is supplying DLUX_MIGRATOR_FLAGS through the compose environment, so a deploy's -mm/-nm/-a still reach a step that is static in the file. 8 tests.
  • Composer Is Now A Required DjangoLux Service: DjangoLux 1.8.0 hands inline updates to Composer, so a Composer service belongs in the deployment, not only on the deploying machine. check FAILs a DjangoLux stack that has none, and check --fix installs the hardened trio (docker-socket-proxy, composer-executor, composer-agent) with their volumes and the docker_proxy network, deriving image and labels from the project's own web service. Projects generated by the 1.8.0 scaffold already ship the block, so this targets stacks generated before it. Idempotent, refuses anything it does not recognize (no dlux_runtime volume, no web service, an orphan socket proxy), and goes through the existing docker compose config validation + .xpose/ backup + atomic write path. 20 tests.
  • check Reports Update-Path Readiness: the dlux-updater-executor check was rewritten after verifying what the service actually does. It also runs dlux_reconcile and migrator, web declares depends_on: dlux-updater: condition: service_healthy, and dlux_update_worker is the only caller of UpdateService.process_next() — the queue drainer that writes the hand-off. So the service is never retired; only the executor code inside it is, in DjangoLux 1.9.0. An earlier entry here claimed otherwise. The check now verifies a composer-side loop exists and mounts dlux_runtime (FAIL if not — it would see no requests and publish no availability, silently), and tells a pre-1.8.0 stack that nothing needs changing yet.
  • Composer Publishes DjangoLux Availability: composer dlux-update --check resolves the newest release, verifies its attestation, reads inline_safe from the manifest inside the wheel and publishes state/package-available.json — the document DjangoLux 1.8.0 reads instead of polling PyPI itself. composer watch, the agent and the executor's watch loop publish on the existing check cadence and re-publish immediately after a swap, so the panel is never left showing the version it just installed. Failures are published as reports too: "could not check" is correct where a stale "up to date" is dangerous.
  • FIXED Package Requests Never Executed On Agent-Only Stacks: a stack with composer-agent but no composer-executor processed the image trigger but not the package trigger, so a DjangoLux 1.8.0 update request would sit unacknowledged forever — and DjangoLux refuses to queue a second operation while one is pending, making it a permanent wedge rather than a slow update. The agent now owns the package trigger when no executor is configured; an image deploy still takes precedence within a tick. Mutation-verified.
  • composer dlux-update And Its Trigger: new subcommand (apply | rollback, --version, --dry-run, --status-file) wires the health-gated orchestration to real Docker work — a scoped restart of the services that load DjangoLux (web, celery by default; dlux-updater is deliberately excluded) and the same health wait composer update uses. Exit 3 is reserved for "rollback also unhealthy", so a caller that retries on failure does not retry a deployment that needs a human. The executor and watcher now watch a second trigger, package-update-request.json, beside the image one — separate files because the two have different blast radii — serialized on the same op lease, acked by token so a request runs once and a failed or unstartable child can never wedge the loop. The agent observes the package ack under its own marker, so an image deploy and a package swap cannot mask each other's completion. 13 tests.
  • Health-Gated Inline DjangoLux Updates: new composer/dlux_package_update.py fetches and verifies a release, stages and activates it, restarts, and then decides whether it stands. If the deployment does not come back healthy the previous release is restored, the bad one is quarantined with the health failure recorded as its reason, and the services are restarted again. A rollback that is also unhealthy is reported as critical rather than as a tidy failure. A failed restart counts as unhealthy; re-applying the active release is a no-op that does not restart anything; a fetch/verification failure never touches the running deployment. prune_releases() keeps a rollback target and never removes the active or protected release. restart and health_check are injected, so the module is Docker-free and testable. 12 tests, weighted toward the failure branches.
  • Composer Fetches And Verifies DjangoLux Releases: new composer/dlux_release_source.py reads the PyPI simple index, pins or picks the newest stable release, verifies the PyPI Trusted Publisher attestation, checks the SHA-256 from the index fragment, reads the manifest carried inside the wheel and refuses anything declaring inline_safe: false, then unpacks with path-traversal rejection. Trust decisions are mirrored from dlux/updater/manifest.py — approved hosts only, bounded reads, and fail-closed attestation: a missing verifier is a refusal, never a pass. 20 tests, weighted toward the refusal paths.
  • Composer Writes The DjangoLux Runtime Volume: new composer/dlux_runtime.py stages a release into releases/<version>/, verifies it against the manifest the wheel carries, flips state/active.json and bumps generation — all atomic, all validated. Verification runs before the pointer moves, so a bad artifact is inert rather than fatal; quarantine() moves a failed release out of releases/ and restore() returns to the previous one (or to the image copy, when the first-ever volume release is rolled back). composer-executor already mounts dlux_runtime:/opt/dlux-runtime:rw, so no Compose change is needed. 17 tests, including an interop check that DjangoLux's own RuntimeStore reads back what Composer wrote (skipped when dlux is not importable).
  • dlux.package_update Agent Action: new bridged action for inline DjangoLux package updates, mirroring dlux.image_update. The payload is typed and bounded — mode (apply|rollback, deliberately no default), target_version (pattern-checked because it becomes a directory name under releases/; empty means latest eligible) and backup_mode (data|full|skip). It routes through the dlux bridge like the other dlux actions, so DjangoLux still records the run and takes its pre-update backup, and Composer performs the staging. Step 1 of the updater consolidation — see docs/updater-consolidation.md in the django-lux repo.

v1.3.6

  • Missing Migrator Declaration No Longer Means No-Op: DLUX stacks containing dlux-updater + web but neither org.dlux.post-start nor native post_start now run the standard supervised migrator compatibility command; check --fix installs the missing label with its existing validation, backup, and atomic-write path.
  • Post-Start Failures Are Fatal And Visible: Hook execution uses non-interactive docker compose exec -T with streamed progress; unhealthy targets and nonzero migrators now make Composer exit nonzero instead of printing a false “Environment ready.”
  • Direct Migrator Subcommand: Added composer migrate [-s SERVICE] [-f FILE] [-d] [MIGRATOR_ARGS...], defaulting to web, preferring its configured post-start migrator, and forwarding options such as -mm, -nm, and -a APP with attached output and exit status.

v1.3.5

  • The Migrator Ran Twice, Concurrently: The generated compose.yml declared the migrator as a native Compose post_start hook, which Compose runs itself the moment web starts — unflagged — while composer separately scraped the same block out of the YAML and re-exec'd it after health, with -mm. Two overlapping runs, one collectstatic --clear wiping STATIC_ROOT while the other collected into it. The declaration moved to an org.dlux.post-start service label that Compose ignores and composer reads, leaving exactly one runner.
  • Post-Start Discovery Reads Resolved Compose Config: parse_post_start_labels() uses the new compose_config_json() (config --format json) instead of regex-scraping each active compose file, so overrides merge once — under -d a dev file repeating the block used to queue the command twice. Native post_start blocks still run as a legacy fallback, announced with a check --fix hint so -mm keeps working on un-migrated deployments.
  • -nm No Longer Means Two Things: -nm now runs the hook and passes -nm to the migrator (skip makemigrations + migrate, still collect static). The internal "run no hooks at all" case that agent-update needs moved to skip_post_start, so replacing the resident pair no longer looks like a user migration flag. -mm and -nm are mutually exclusive at the CLI.
  • check --fix Migrates The Hook: enable_post_start_label folds a native post_start into the label through the existing guarded transform (dry-run default, docker compose config validation, .xpose/ backup, atomic write, idempotent). Refuses hooks holding more than one command.

v1.3.4

  • Versioned Launcher Wrappers: start.sh/start.ps1 carry a # composer-wrapper: N marker, and composer check compares them against the copies now baked into /app/wrappers/ in the image — no registry call, so it works air-gapped. wrappers-history.json records each published version's sha256, which separates a stale-but-pristine wrapper (check --fix updates it) from one with local edits (reported, replaced only after the confirmation names it). A marker newer than the image reports the image as behind and points at update-self instead, so --fix can never downgrade a wrapper. --fix archives to .xpose/composer-check/<stamp>/ and swaps via os.replace, so a start.sh executing the very check that replaces it keeps reading its original inode; a non-executable start.sh is repaired rather than preserved. Composer owns both files (the DLUX scaffold writes them once and refuses to overwrite); DLUX's scaffold_templates/project/ copies are mirrors, pinned by a scaffold test.
  • Wrappers Were Excluded From The Image: .dockerignore listed start.sh/start.ps1 under "sensitive files", so the COPY for the new reference directory failed the build outright. They are neither sensitive nor optional now.
  • Piped Input Reached Nothing: start.sh/start.ps1 added -i only as part of -it, gated on stdin and stdout both being TTYs, so echo yes | ./start.sh run -m web migrate gave the container no stdin at all and exec_in_service's non-interactive -T path could never receive the piped data. Both wrappers now attach -i unconditionally and add -t only when a terminal exists ([Console]::IsInputRedirected on Windows).
  • start.sh Failed On macOS Without A Secrets File: "${secret_flags[@]}" on an empty array is an unbound-variable error under set -u in bash 3.2 — the /bin/bash macOS ships — so ./start.sh aborted immediately in any project with no .env/secrets/.env/.secrets/.env. Guarded with the ${arr[@]+"${arr[@]}"} form. Covered by two new test_wrapper_secrets.py cases (piped vs pty argv), suite 286.

v1.3.3

  • Pull Bar Names The Right Image: The status line used to show whichever service Compose mentioned last — "postgres Pulling" while web's layers were actually downloading — because Compose interleaves layers and never attributes one to a service. PullProgress.scope() now names every image still in flight and drops each as Compose reports it pulled (postgres, webweb), falling back to the Pulling from repository for a plain docker pull. Reused layers are counted as (n cached), so "did it re-download everything?" is answerable from the line itself.
  • Services In Flight Stop Showing Green: New SERVICE_UPDATING state (🔵) applied by mark_services_updating() when a pull, recreate, or restart starts — scoped to the targeted services, never the excluded ones. A green circle beside a container being replaced reported health measured on the container that was going away; health monitoring resolves each service back to 🟡/🟢/🔴.
  • Image Pull Progress Bar: Long pulls no longer look like a hang. composer/progress.py aggregates the per-layer phases docker pull and docker compose pull report (Pulling fs layerDownloadingDownload completeExtractingPull complete, plus the byte counts) into one in-place bar: ████████░░░░ 62% · 2/4 layers · 144MB/240MB. Wired into update-self (now streamed instead of handed to a bare docker pull), pull_images, and launch_containers. Progress never moves backwards, non-pull output keeps the existing status line, and a detached run logs coarse summaries.
  • Wrapper Stops Pulling Silently: start.sh/start.ps1 probed the installed version with docker run ... cat /app/VERSION 2>/dev/null, which pulls a missing image with all progress sent to the discarded stderr — the "waiting forever with no output" on ./start.sh --update. They now check with docker image inspect (never pulls) and announce a first-run fetch with a visible docker pull before docker run.
  • Stale "Update Available" After A Deployer Update: The availability document was only re-published by the agent's own update path or its hourly --check-interval, so an update deployed from the project root (composer update), by the executor in the hardened topology, or by a manual docker compose pull kept advertising an update that was already installed. WatchRuntime now tracks the local digest behind each published entry and polls it every 30s (LOCAL_DIGEST_PROBE_SECONDS); a moved digest forces a re-publish. An unreadable digest stays "unknown" and never re-publishes, so a transient Docker error can't flip the flag. The agent also forces a re-check after observing an executor .ack.
  • Runs Survive A Closed Terminal: A hangup no longer aborts a deploy midway. composer/session.py installs a SIGHUP guard (plus ignored SIGTTIN/SIGTTOU), redirects stdout/stderr to composer-detached.log (COMPOSER_DETACH_LOG, or a configured COMPOSER_LOG_FILE) and stdin to /dev/null, and the run finishes in the background; render/progress lines drop cursor-control escapes once detached. start.sh traps HUP so the inherited disposition keeps the docker run client alive too.
  • Compose Children Detached From The Terminal: run_command/run_command_streaming start Compose with start_new_session=True, so a terminal hangup can't reach it; Ctrl+C is relayed explicitly (_interrupt_child: SIGINT to the child's group, then terminate/kill) and still exits 130. run, log, and logs stay terminal-bound and restore default hangup handling in the child.

v1.3.2

  • check --fix Migrates The dlux-updater Runtime: composer check --fix now surgically migrates a deployed project's dlux-updater command to the packaged runtime (python -m dlux.updater.supervisor) and adds the pre-migration dlux_reconcile guard — marked block only, idempotent, never touching your services. Version-gated on the dlux the image actually ships (probed with dlux --version via docker compose run --no-deps, which works even when dlux-updater is crash-looping): when the image is older than 1.6.2 it reports "update the image first" and changes nothing, so the compose never points at modules the image lacks. A pulled deployment has no requirements.txt, so the image is the only authoritative signal.
  • Deployer Can Read Project Secrets: The deploying role (composer-executor, and composer-agent in the agent-only topology) adds cap_add: DAC_READ_SEARCH on top of cap_drop: ALL, so the uncapped UID-0 process can read the project's 0600 .secrets/.env to deploy. Fixes inline deploys failing the secrets guard with a Permission-denied error and needing a manual setfacl. Read-only override only; the network-facing agent in the hardened topology keeps no file caps.
  • check --fix Repairs the Cap In Place: composer check --fix now heals an already-hardened stack that lacks the read cap — a targeted insert (not a full re-render, so the dlux-scaffold and composer-generated blocks are both safe) that adds cap_add: DAC_READ_SEARCH to the deployer only. enable-agent/enable-executor self-heal the same way instead of no-opping on an already-migrated stack.

v1.3.1

  • agent-check Compose Fallback: With no IMAGE argument and no COMPOSER_CHECK_IMAGE/WEB_IMAGE, agent-check now discovers the watched images from the deployment's own compose file (--check-image entries and WEB_IMAGE in the composer-agent/executor/updater block, with ${VAR:-default} resolution); added -f/--file to scope discovery.

v1.3.0

  • Docker Authority Isolated In composer-executor: New executor role holds docker.sock and does every Docker write (trigger-watched image update + typed restart/recovery_deploy over a private unix socket). The composer-agent keeps read-only proxy access only and delegates writes.
  • enable-executor + check --fix: composer enable-executor migrates a composer-agent stack to the hardened topology (.xpose/ backup, docker compose config validate, atomic write; idempotent); composer check --fix runs it automatically. agent-update/agent-restart/agent-off now target the resident pair.

v1.2.9

  • Clear Update Command Vocabulary: Added pull, update-self, agent-update, agent-restart, and agent-off; retained -u plus legacy one-argument --update, and retired -uo, update -o, and the ambiguous long application --update.
  • Agent Image Availability Check: Added agent-check with tagged-image or WEB_IMAGE discovery, human/JSON output, atomic file publication, and explicit unknown-registry failure semantics using the agent digest/version/manifest contract.

v1.2.8

  • Latest-Only Snapshot Relay: Coalesces pending snapshots to the newest value and collapses existing snapshot backlogs on startup, preventing stale runtime versions from appearing after first enrollment.

v1.2.7

  • Pinned Agent Control Origin: Persisted the normalized control URL after enrollment, rejected conflicting pairing requests and startup overrides while credentials remain active, and allowed replacement only after revocation/re-enrollment or local state reset.
  • No Credentialed Redirects: Control-plane and registry requests reject HTTP redirects; registry token challenges also require an HTTPS realm.
  • Protocol Input And Redaction Hardening: Recovery force now requires a JSON boolean, and full Bearer authorization values are removed from relayed logs.
  • Legacy Proxy Cleanup: check --fix archives and removes recognized pgAdmin routes, reloads Caddy/direct Nginx configs, and restarts plus verifies template-backed Nginx so its rendered configuration is refreshed.
  • Mixed Topology Guard: check now fails when composer-agent and composer-updater coexist instead of treating the agent-first branch as healthy.

v1.2.6

  • Obsolete DLUX Service Cleanup: composer check now warns on pgadmin, db-backup, and db_backup; guarded check --fix validates the candidate, archives originals, runs targeted docker compose rm -sf only for those services, applies their service-block removal, and verifies the services are gone while pre-existing named volumes remain.

v1.2.5

  • Typed Confirmation Guard For Destructive Actions: New composer/confirmation.py confirm() requires a literal y/yes before -v/--volumes and -p/--purge run, printing the exact consequences first. -y/--yes (or COMPOSER_ASSUME_YES=1) skips the prompt; a non-interactive stdin without either fails closed instead of destroying data.
  • stop Subcommand: composer stop [-v] [-p] [-y] [-f FILE] [-d] [service...] is dispatched before the flat parse like run/restart, via configure_stop(); down_containers() forwards named services so a single service can be stopped. The project-wide -v/-p flags are rejected alongside service names. composer down is an alias and composer --down still works.
  • update Subcommand: composer update [-o] [-b] [--force] [-nm] [-mm] [-a APP] [--status-file PATH] [service...] runs the -u pipeline through configure_update(), with -o/--only covering -uo. scoped_service_list() in composer/service_selection.py lets pull_images(), launch_containers(), compose_config_images(), and the panel labels accept several service names instead of a single string; the flat -u/-uo flags are unchanged.
  • log Subcommand: composer log|logs [-n N|all] [-F] [-t] [--since] [--until] [--no-color] [service...] streams docker compose logs attached to the terminal through stream_service_logs(), defaulting to --tail 50; -n all/-n 0 lifts the limit and --no-color is forced when stdout is not a TTY.
  • check Doctor Subcommand: New composer/checkup.py CheckupMixin adds composer check [--fix] [-y] [--deep] [--json], verifying Docker/Compose v2, compose config resolution, secrets source, unmet required compose vars, topology mode, and deployer↔resident version drift. --fix migrates a legacy composer-updater topology through enable_agent() behind confirm(); --deep relays a configurable in-container doctor (python manage.py dlux_doctor in web by default). One evolving command replaces per-change enable-* one-offs: composer owns the outside checks and delegates the rest to the container.
  • Distinct Resident Agent Version: publish_agent_status() now writes composer_version (the resident composer-agent binary's own version) into agent-status.json alongside the back-compat agent_version. It is distinct from the COMPOSER_VERSION env a panel sees, which reflects the deploying composer that last recreated the stack — the two can drift and both are now reportable.

v1.2.4

  • enable-agent Runs On Deploy Hosts: enable_agent() no longer requires manage.py with a Generated with django-lux banner. Deploy directories hold only compose.yml, .proxy/, and .secrets/, so the source-tree gate blocked every real migration; project identity already comes from the Compose name:, services:, and the # Composer-as-updater markers _transform_compose() verifies.
  • Non-Blocking Bridge Check Without A Manifest: _dlux_readiness_warning() returns (message, blocking). A missing requirements.txt/pyproject.toml reports an advisory warning and --apply proceeds; a declared DjangoLux below 1.5.0 or an unparseable pin still blocks unless --allow-unverified-dlux is passed.

v1.2.3

  • Baked DjangoLux Version In Availability Manifest: _release_manifest_from_label() in composer/watcher.py passes through an optional baked_dlux_version (string, capped at 32 chars) so the published availability document carries the candidate image's baked framework version alongside the project version and highlights. Additive under schema_version: 1; absent fields stay absent, and the version label the preflight gate reads is untouched.

v1.2.2

  • enable-agent Migrates Pre-1.5 Scaffolds: _agent_stack() no longer derives networks, COMPOSER_VERSION_LABEL, or WEB_IMAGE from the Compose name:; new _legacy_topology() carries the replaced composer-updater/docker-socket-proxy values forward verbatim. Projects generated before the DjangoLux 1.5 scaffold (egress/docker_proxy, deployment-specific baked-version label) emitted references to undeclared dlux_update_egress/<slug>_docker_proxy networks plus a wrong label, so docker compose config rejected the migration.
  • Undeclared Network Preflight: _transform_compose() checks carried network names against top-level networks: keys and fails naming the missing ones instead of deferring to an opaque Compose error; the agent end marker is emitted at service indentation.

v1.2.1

  • UI-Driven Agent Pairing: composer agent now accepts a DjangoLux-written enroll-request.json bridge file ({control_url, pairing_code}), redeems the code through the existing /api/agent/v1/enroll/ endpoint, persists the control URL in the durable store so restarts rebuild the client without COMPOSER_CONTROL_URL, and writes agent-status.json (enrolled/connection state) for the DLUX Control Panel tile. Enrollment is idempotent per operation_id; env-var bootstrap remains a fallback.

v1.2.0

  • Outbound Composer Agent: Added composer agent with HTTPS enrollment/long polling, SQLite command and replay durability, typed update/backup DLUX spool relay, operation-aware status, safe restart allowlists, two-phase rotation, fresh-token re-enrollment after revocation, and local watch compatibility.
  • Composer-Owned Agent Migration: Added dry-run-first composer enable-agent to diff, Compose-validate, preserve, and atomically migrate recognized DLUX updater scaffolds; DjangoLux now keeps only a temporary forwarding alias.
  • Protocol Security: Added strict schema-v1 fields/timestamps and payload bounds, UUID deduplication, monotonic events, inherited-secret/log redaction, capability reporting, read-only project support, and stateful-service exclusions through the existing Docker socket proxy.

v1.1.15

  • Restart Subcommand: Moved restart into composer restart [-f FILE] [-d] [--status-file PATH] [service] with dedicated help and early dispatch like composer run; leading -r/--restart aliases retain the same restart-and-health pipeline.
  • Private Secrets Handoff To Resident Updater: start.sh/start.ps1 now pass the selected plaintext file through Docker --env-file with a key manifest, and Composer's mode-0600 runtime override forwards those inherited values only to composer-updater. Resident image updates validate and reuse that environment instead of reopening the host bind-mounted file, so mode-0600 secrets work without ACLs or added capabilities. Direct legacy containers retain strict fail-before-pull behavior plus mapped-UID ACL diagnostics.

v1.1.14

  • Secrets Never Fall Through To Compose Defaults: SecretsMixin.resolve_secrets() now refuses any env candidate that exists but is unreadable (permissions/userns) or yields no values, and parse_env_file() no longer swallows read errors. Because required_compose_vars() excludes every ${VAR:-default} interpolation, a defaults-heavy compose previously let an unreadable .secrets/.env vacuous-succeed and deploy on admin/admin_pass; the run now fails loudly at the secrets stage before any pull or recreate.

v1.1.13

  • Quote-Safe Project Manifest Labels: Extended watcher._release_manifest_from_label() to decode bounded base64:<URL-safe-base64-JSON> image labels before applying the existing schema-1 normalization. Raw JSON labels remain supported, and malformed encoded or raw metadata is still omitted without affecting digest availability or optional version fallback.

v1.1.12

  • Optional Project Image Release Manifest: Added one-pass remote image-label discovery through registry.remote_image_labels() and extended watcher.check_availability() to publish independently optional version and normalized manifest fields for digest-detected updates. COMPOSER_RELEASE_MANIFEST_LABEL defaults to org.dlux.project.release-manifest; missing, malformed, oversized, empty, or unsupported manifest JSON is omitted without changing digest availability, version fallback, or deployment behavior.

v1.1.11

  • Project-Mount-Independent Runtime Overrides: Changed DockerComposeMixin.sync_runtime_compose_override() to create its atomic .composer-runtime-*.compose.yml through Python's verified writable system temporary directory instead of the current project directory. Resident updaters now work with host-owned mode-0755 or read-only project mounts while retaining cap_drop: [ALL]; temp-file creation failures become Composer diagnostics instead of uncaught PermissionError tracebacks.
  • Guaranteed Terminal Watcher Failure: Changed composer watch so every non-zero child exit atomically publishes a token-matched failed deploy status before writing the request ack, preserving any detailed child error and appending a generic failure to deploy-log.txt. Child spawn errors now terminalize with exit 127 instead of crashing the watcher, giving downstream maintenance controllers both status and ack signals even when the one-shot launcher fails before its first phase.
  • Read-Only Runtime Regression Gates: Added standard-library unit coverage for override placement, content, cleanup, and creation-failure diagnostics, plus an image smoke test that runs with a read-only image, a read-only mounted Compose project, --cap-drop ALL, and writable /tmp only, then executes a real merged docker compose config through the generated override. CI and release workflows now execute the unit suite before building or publishing images.

v1.1.10

  • Resident Watcher Self-Exclusion: Added COMPOSER_EXCLUDE_SERVICES filtering across service discovery, generated runtime overrides, bulk pulls, version-gate image resolution, bulk up -d, health state tracking, and diagnostics; composer watch now exports COMPOSER_EXCLUDE_SERVICES=composer-updater by default (override with COMPOSER_WATCH_SELF_SERVICE) so an in-compose updater does not recreate the container supervising its own app update after the v1.1.9 watchcomposer -u change.

v1.1.9

  • Pull-Only Update Flag (-uo): Changed DockerComposeLauncher so -uo/--update-only [service] now runs only the pull_images() phase (scoped by pull_service when a service is named), writes the new pulled status, renders only the secrets/pull rows, and exits before preflight_version_gate(), launch_containers(), health checks, or post-start hooks. composer watch now shells python -m composer -u for the full update pipeline, preserving the existing deploy/update behavior and service-scoped -u <service> recreate semantics.

v1.1.8

  • Availability Check Publishes The Target Version (composer watch): The registry availability check now includes the newer image's own version in image-available.json, so a downstream reader (e.g. the dlux admin panel) can show "update available to v2.4.0" instead of only a digest. New registry.remote_image_version(ref, token, label) reads the image's OCI version label (org.opencontainers.image.version by default, override with COMPOSER_VERSION_LABEL — the same env the preflight version gate uses, so the surfaced version matches the gated version) by fetching the tag manifest, descending into a concrete image manifest for multi-arch indexes, and reading .config.Labels from the image config blob — reusing the existing Bearer-token challenge flow via a new _fetch_bytes helper. watcher.check_availability adds "version" per image best-effort and only when an update exists (avoids an extra registry round-trip on every poll); any failure (older/private/unsupported registry, missing label, network error) is silently omitted so the check never breaks and readers fall back to the digest. Fully backward compatible with the existing image-available.json shape.

v1.1.7

  • Update Console Log (composer watch --log-file): watch now records a clean, ANSI-free console for each update run so a resident proxy can show a live console during the recreate window (when the app itself is down). The -uo child appends progress to COMPOSER_LOG_FILE (set by the watcher; default deploy-log.txt beside --status-file, truncated per run) via a new OutputUtilsMixin.append_console() — reusing the already-sanitized emit_progress/emit_status text (no escape codes, no panel redraw noise) plus — <phase> — markers and the failure detail from write_status(). New --log-file PATH flag; the terminal panel and docker logs are unchanged. Opt-in (no-op without a log path).

v1.1.6

  • Registry Availability Check (composer watch): watch can now poll a registry for a newer image and publish the result, so another process (e.g. a Django admin) can surface "update available" without registry access of its own. New flags --check-image IMAGE (repeatable), --check-interval SECONDS (default 3600, min 60), and --availability-file PATH. On each check it compares the remote tag digest (new composer/registry.py — a minimal registry v2 client doing the standard Bearer-token challenge flow; COMPOSER_REGISTRY_TOKEN for private repos) against the locally-pulled digest (docker image inspect … RepoDigests, via the socket) and writes {available, checked_at, images:[{image, remote_digest, local_digest, update_available}]} atomically. Unreadable remote = "unknown" (never a false positive). The check runs immediately on start, on the interval, and is forced right after an applied update so the signal clears. Availability polling is opt-in (needs both --check-image and --availability-file); the trigger-file watch is unchanged.

v1.1.5

  • Resident Updater (composer watch): New watch subcommand turns composer into a trigger-driven, in-compose updater. composer watch --trigger-file PATH [--interval N] [--status-file PATH] [-f FILE] [-d] [--once] watches a trigger file and, on each new request (a changed token field, or the file's mtime when there is no token), shells the existing one-shot composer -uo pipeline (pull → version gate → recreate → health → post_start) in a child process — so all one-shot behavior/exit codes stay intact. It records the processed token in <trigger-file>.ack (atomic write) so a request is applied exactly once and survives a restart of the watcher container. Clean ownership split: the child writes COMPOSER_STATUS_FILE; the watcher owns the ack. Implemented as an early argv[1] == "watch" intercept → composer/watcher.py:run_watch() with cli.parse_watch_args(); documented in the main --help epilog and composer watch --help.
  • Deploy Status File (--status-file / COMPOSER_STATUS_FILE): New opt-in StatusWriterMixin writes an atomic JSON deploy-status.json (temp-file + os.replace) so an external reader (Django admin panel, dashboard, health probe) can observe the run without scraping the terminal UI. DockerComposeLauncher.run() writes lifecycle states through the pipeline — startingpullingrecreatingmigratingready, and failed (with a truncated error) at every abort; the restart branch reports restarting/ready/failed. Payload includes status, updated_at (UTC ISO-8601), composer_version, compose_files, and (when the version gate ran) target_images/target_version/active_version. No file is written unless configured; write failures never abort a deploy.
  • Preflight Version Gate (--force): New opt-in VersionGateMixin refuses an update (-u/-uo) that would recreate onto an image whose version label is older than the deployment's currently-active version — the one move a generic pull-and-restart can't safely undo (old code against a forward-migrated schema). Runs after pull (target label is local by then) and before recreate. Reads the target version from an image label (COMPOSER_VERSION_LABEL, default org.opencontainers.image.version) via docker image inspect, and the active version from a JSON file+key (COMPOSER_ACTIVE_VERSION_FILE + COMPOSER_ACTIVE_VERSION_KEY, default version — e.g. the dlux runtime active.json). Fully generic and disabled unless an active-version source is configured; missing labels/metadata pass with a note; --force overrides a block. Ships a dependency-free PEP440/semver-lite parse_version (no packaging needed).
  • Removed SOPS/AGE Encryption: Dropped the optional SOPS/AGE encrypted-secrets path entirely; secrets now resolve exclusively from a plaintext env file (.envsecrets/.env.secrets/.env). SecretsMixin lost decrypt_secrets_raw(), encrypt_secrets_raw(), encrypted_secrets_path(), the ENCRYPTED_CANDIDATES list, parse_dotenv_text(), and the enc_file attribute; resolve_secrets() no longer takes args, drops the encrypted fallback + AGE key prompt, and stores self.secrets_source as the plaintext path string. Removed the -k/--key, --encrypt, --decrypt, -i/--input, -o/--output, and positional key_positional CLI arguments (composer/cli.py) and the encrypt/decrypt branches in DockerComposeLauncher.run(). RenderingMixin.render() now always shows the 🔓 PLAINTEXT <path> source flag (the 🔐 DECRYPTED variant is gone).
  • Slimmer Image & Entrypoint: Dockerfile no longer installs the age apt package or downloads the sops binary; entrypoint.sh drops the keygen/encrypt/decrypt/sops routes and now execs python -m composer "$@" directly. scripts/smoke-test.sh drops the --encrypt/--decrypt flag assertions, the age/sops runnable checks, the keygen route, and the age+sops round-trip; it still gates on version, core flags, the run subcommand, and docker/compose availability.
  • Docs: README.md rewritten to remove the secrets encryption/decryption/keygen workflow and the -k/--encrypt/--decrypt flags.

v1.1.4

  • run Subcommand: Added composer run [-m] [-s] [-F] [-f FILE] [-d] <service> <command...> to run a command inside a Compose service without hand-writing docker exec/docker run. Defaults to docker compose exec <service> <command...>; -m/--manage prepends python manage.py, -s/--shell wraps the command in sh -c, -F/--fresh switches to a one-off docker compose run --rm. TTY is auto-managed (-T added when stdin/stdout aren't a terminal). Implemented as an early argv[1] == "run" intercept in DockerComposeLauncher.run()handle_run()DockerComposeMixin.exec_in_service(), with a new SubprocessRunnerMixin.run_command_interactive() (inherited stdio) and DockerComposeMixin.resolve_compose_cli() (one-shot plugin/legacy probe since interactive runs can't inspect captured output). Compose-file resolution extracted to resolve_active_compose_files() and reused; run honors -f/-d. Documented in the main --help epilog and composer run --help.

v1.1.3

  • Update-Then-Recreate (-u/--update [service]): -u now pulls the latest image(s) and recreates immediately in one step. With a service name it scopes both the pull and the recreate — composer/cli.py keeps nargs="?"/const=True, DockerComposeLauncher now sets up_service alongside pull_service, and DockerComposeMixin.launch_containers() appends the service to up -d so only that service is recreated (Compose still starts its dependencies; recreate is image/config-change driven, no --force-recreate). Native Compose semantics: dependents aren't auto-restarted unless their own image changed.
  • Update-Only (-uo/--update-only [service]): New flag preserving the previous -u behavior — pull (optionally one service) before the normal full up -d startup, without scoping the recreate. Maps to update_images/pull_service without setting up_service.
  • Restart (-r/--restart [service]): New flag that runs docker compose restart [service] instead of --down + start, preserving containers so baked-in env vars survive. Added DockerComposeMixin.restart_containers() and a dedicated launcher branch that resolves secrets, restarts, then monitors health (no post-start/migration hooks). RenderingMixin.render() is restart-aware: shows "Restart Services (svc)", hides the Pull/Post-Start rows, and labels a scoped -u recreate as "Start Compose (svc)".

v1.1.2

  • Skip Commented-Out Var Refs: required_compose_vars() now strips YAML comments before scanning, so a ${VAR} inside a full-line or trailing # … comment is no longer counted as required. A mid-token # (e.g. url#frag) is preserved, so a real ${VAR} after it still counts. Adds ConfigMixin._COMMENT_RE.

v1.1.1

  • Smarter Required-Var Detection: required_compose_vars() no longer produces false "missing variable" failures. It strips $$ escapes before scanning (so shell variables in command/healthcheck scripts like $$attempts are not mistaken for compose interpolations), and subtracts variables the compose already supplies itself — ConfigMixin._compose_env_keys() collects keys an environment: block assigns a concrete literal value to (mapping and list syntax), while bare pass-throughs (- KEY) and interpolated values (KEY: ${KEY}) are still treated as needing a value.

v1.1.0

  • Plaintext-First Secrets Resolution (default): Running with no secrets flags now auto-resolves secrets. SecretsMixin.resolve_secrets() searches plaintext env candidates (.env, secrets/.env, .secrets/.env) and uses the first file that satisfies every variable required by the compose (computed via the new ConfigMixin.required_compose_vars(), which parses ${VAR} interpolations and skips those with a :-/-/:+/+ default). If none qualify, it falls back to an encrypted file (secrets.enc, secrets/secrets.enc, .secrets/secrets.enc), prompting for the AGE private key only when one was not supplied via -k/positional/SOPS_AGE_KEY. Added env helpers parse_env_file(), parse_dotenv_text(), apply_env_values() and source helpers plaintext_env_candidates()/encrypted_secrets_path(); DockerComposeLauncher now tracks secrets_source.
  • Removed -sd/--skip-decrypt: The skip-decrypt flag is obsolete and fully removed from composer/cli.py, launcher.py, rendering.py, and secrets_manager.py (dropped load_secrets()/load_secrets_from_file() and the dev-mode coupling that forced it). -d/--dev is now purely the two-compose-file override mode and no longer dictates the secrets source.
  • Dev Mode Forces Debug: -d/--dev now always turns debug on regardless of the project's DEBUG/DEBUG_STATUS value (or its absence). DockerComposeMixin.sync_runtime_compose_override() injects DEBUG: "True" and DEBUG_STATUS: "True" into every service's environment in the last-applied override file (overriding any compose declaration), build_compose_env() exports DEBUG=True/DEBUG_STATUS=True for ${DEBUG}/${DEBUG_STATUS} interpolation, and the launcher forces the debug_mode UI flag. Both names are added to the injected set so they never count as missing required secrets.
  • UI Refresh: Reworked the status panel in RenderingMixin.render() — lighter rules replacing the solid block bars, a bold title, the compose-file list on its own 📂 line, and a secrets-source flag (🔐 DECRYPTED <path> / 🔓 PLAINTEXT <path>) replacing the old ⚠️ BYPASS DECRYPTION indicator. The first step is relabeled Load Secrets and shows the resolved source path.

v1.0.1

  • Runtime-Gated Image Publishing: Added scripts/smoke-test.sh, which runs the built image and asserts --version matches the VERSION file, --help exposes the core flags (--down/--purge/--volumes/--update/--build/--encrypt/--decrypt), the bundled age/sops/docker/docker compose binaries are runnable, the keygen entrypoint route emits an AGE key, and an end-to-end age+sops encrypt/decrypt round trip succeeds. .github/workflows/release.yml now builds the amd64 image with load: true and runs the smoke tests before the multi-arch Docker Hub push, so a runtime-broken image can no longer be published. .github/workflows/ci.yml runs the same smoke tests on every push/PR to main.

v1.0.0

  • Composer Rebrand: Relaunched under the Composer name, replaced the old Decrypter branding, removed obsolete passphrase-based encryption/decryption support, and improved the modular package structure (composer/ mixins) and single-status-line terminal UI.
  • Purge Flag (-p/--purge): Added a --down child flag in composer/cli.py driving a full compose teardown in DockerComposeMixin.down_containers() — appends -v (implies volume removal even without -v), --rmi local to drop built untagged images, and --remove-orphans. Adds DockerComposeMixin.prune_build_cache() running docker builder prune -f for dangling BuildKit cache (not compose-scopeable). Wired through down_volumes/purge on DockerComposeLauncher.
  • Tag-Driven Release Pipeline: Added .github/workflows/release.yml triggered by v* tags — verifies the tag matches the VERSION file, builds the multi-arch (linux/amd64,linux/arm64) image with Buildx, pushes debeski/composer:<version> and debeski/composer:latest to Docker Hub, and publishes a GitHub Release using the matching CHANGELOG.md section. Added .github/workflows/ci.yml running compileall + CLI smoke and a no-push Docker build on pushes/PRs to main.
  • Changelog Renormalized: Folded the pre-release v1.0.0v2.0.0 history into the v0.1.x series so the first GitHub-Actions-published image starts a clean v1.0.0.

v0.1.13

  • Improved compose file reporting by listing all active filenames in the UI and debug logs. Standardized compose file resolution (including docker-compose.yml fallback) across all orchestration steps.

v0.1.12

  • Added --update flag to wrapper scripts (start.sh, start.ps1) to explicitly update the Docker image. Removed automatic image pull on every run.

v0.1.11

  • Separated progress messages from state circles to prevent terminal output overwrites, and added dynamic waiting/failing status output during the health check loop to clearly identify stuck containers.

v0.1.10

  • Added --decrypt and --encrypt flags for standalone crypto operations. Added -i/--input and -o/--output to customize file paths for encrypt/decrypt.

v0.1.9

  • Updated start templates for bash and powershell.

v0.1.8

  • Fixed a visual bug where the end result erased previous terminal output.

v0.1.7

  • Passed the launcher version into Compose and automatically injected it into all launched services via a generated runtime override, so deployed projects can read the Composer version without per-project compose edits.

v0.1.6

  • Fixed launcher UI redraw issues that could repeat header lines, kept compose/pull progress on a single in-place status line, improved compose startup diagnostics, and accepted quoted DEBUG_STATUS values such as "True" when parsing compose config.

v0.1.5

  • Streamed Docker Compose build/pull progress during startup, improved failure diagnostics for compose health/post-start errors, and treated running services without healthchecks as ready instead of hanging.

v0.1.4

  • Added --down flag to stop containers and -v flag to remove volumes when stopping.

v0.1.3

  • Added -u / --update flag to force pull container images. Support for specific service targeting (e.g., -u web).

v0.1.2

  • Shifted core target pattern to Docker Compose (:compose tag default). Removed container-internal web reachability checks in favor of native health states.

v0.1.1

  • Added MIT License, detailed project .gitignore, and clarified multi-platform Windows (.ps1) usage.

v0.1.0

  • Initial release: Core orchestration for SOPS age encryption and Docker deployment setups.