evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
Created by Kelly Michels · dev@evomedia.net
Licensed under the MIT License. See LICENSE.

Changelog
=========

Notable changes to the evomedia.net Token Savers.

Unreleased
----------

Changed
-------

- **zec2 and zec2online comments now say what they mean without
  naming private detail.** The container-side version read is described
  by what it is - an endpoint that is not public on every project - rather
  than by a product's own wording, and zec2 records why it needed its
  own ssh: the read referenced three variables the script never defined,
  and because it sits inside a try/catch the failure was silent and looked
  exactly like a service that could not be reached.

v1.0.0.0.26 - 2026-09-14
------------------------

Fixed
-----

- **zdeploy on a docker stack built from source shipped nothing after the
  first deploy.** The docker kind ran docker compose pull and then
  docker compose up -d, which is right for a stack of published images and
  wrong for one built from a Dockerfile in the tree: there is nothing to
  pull, and up -d builds only when the image is missing. So the first
  deploy worked and every one after it uploaded the new code, started the old
  image, and reported success — worse than an error, because the deploy is
  green and the container is healthy. A project now opts into building with
  "deploy": { "build": true }, which runs docker compose build --pull so
  the base image is refreshed at the same time. Stacks that pull are
  unaffected.

v1.0.0.0.25 - 2026-09-12
------------------------

Added
-----

- zmerge — merge every pull request across the org that is genuinely
  ready (MERGEABLE / CLEAN, not a draft), re-checking each one immediately
  before and after every merge, because merging into a default branch can
  conflict a sibling PR in the same repository. Dry run by default;
  -Execute (or -e) merges.
- zpull — zmerge, then git pull --ff-only in every checkout the
  merges affected. Skips a checkout that is dirty or is not on its default
  branch rather than guessing.

Changed
-------

- -e is an alias for -Execute on both of the above, the way -s
  already works for -Scan.
- zmerge discovers repositories instead of listing them. It asked a
  hand-kept list, which had fallen well behind the org - so a scan covered
  about half of it and reported "Nothing open to merge" while a ready pull
  request sat in a repository the list had never heard of. It now asks GitHub,
  and throws rather than returning an empty list if that fails: a tool that
  quietly scans nothing prints the same reassuring line as one that scanned
  everything, and the two must not be confusable.

v1.0.0.0.24 - 2026-09-08
------------------------

Added
-----
- zdeploy can lay the release tag it already knows the number for.
  A versioning scheme that asks every release to carry an annotated tag needs
  something to enforce it, and for a project whose build number lives outside
  git - in a database, say - nothing did: one project reached thirty-eight
  builds with four tags, and the missing ones were unrecoverable because the
  number had never existed anywhere else. With deploy.tagOnDeploy, the
  deployed commit is tagged with its build number and pushed, but only after
  the live build has been verified - a tag is a claim about what is running.
  Opt-in, because a project that already tags releases through a pull request
  must not also collect a tag per deploy. It can never fail a deploy: an
  existing tag is left alone, a failed push keeps the tag local and prints the
  command to finish it, and a missing repo just says so.

Fixed
-----
- The mirror stopped publishing current product names. Its own denylist
  never saw the current spellings (a dot or hyphen breaks the word, an
  underscore hides the boundary), so twelve references went out while the
  suite ran green. The patterns learn the spellings, planted cases prove it,
  and the references read generically now.
- zstart no longer aborts on a pull that succeeded. git reports
  ordinary fetch progress (From https://...) on stderr, and under Windows
  PowerShell 5.1 the script's 2>&1 turned that into a terminating error -
  so a pull that had worked stopped the dev server from starting, before
  the script's own "Auto-pull skipped" branch could run. The pull now lives
  in Invoke-StartGitPull, which never throws, never switches branch, and
  never touches a dirty tree: it fast-forwards when it can, reports when it
  can't, and zstart carries on either way - the opposite failure mode
  from Invoke-DeployGitPull, on purpose. Fourteen tests drive real git
  under Stop on 5.1, the host the defect lives on (#130).

v1.0.0.0.23 - 2026-08-31
------------------------

Changed
-------
- Every release since 1.0.0 has its own section again - 21 entries had
  piled up under Unreleased while 22 builds shipped, so this file said
  nothing had been released since 1.0.0 and a reader at any tag found no
  section for the version they were holding. Which release carried which
  entry was derived from git, not guessed: for every line, the commit that
  introduced it, then the earliest tag containing that commit. No entry text
  changed - only headings were added and whole entries moved under the
  release that carried them.
- **scripts/readme_txt.py is now scripts/plaintext_twins.py and covers
  every markdown file that owes a twin**, CHANGELOG.txt included. It was
  kept by hand, so it drifted the moment this file was reorganised. The
  renderer also drops <!-- --> markers, which are invisible in markdown
  and read as stray punctuation in a text file.

Fixed
-----
- The --check that keeps the twins honest is actually run now -
  readme_txt.py shipped one and its docstring claimed "the test suite runs
  --check", but nothing invoked it, so a twin could disagree with its
  markdown indefinitely. tests/PlainTextTwins.Tests.ps1 runs it, and
  reports inconclusive rather than passing when python is unavailable.

v1.0.0.0.22 - 2026-08-31
------------------------

Changed
-------
- The sanitization denylist moved to tests/sanitization-patterns.psd1,
  so the test suite here and the publisher in the private toolkit read one
  list instead of keeping two. They had two, and they disagreed: the
  publisher's scan looked only for secrets, while these rules are about
  identity — internal project names, product domains, private-only script
  names, operator paths. It therefore reported "clean" on files this suite
  rejects. No rule changed; only where they live.

v1.0.0.0.21 - 2026-08-31
------------------------

Added
-----
- tests/VerifyPlan.Tests.ps1 — the verification channel-selection rules
  are pure functions in ZHelpers.ps1 (Get-VerifyAttempts,
  Get-VerifyTimeout) and Pester pins them, including "a project with no
  domain must never produce an edge attempt" and the PowerShell 5.1
  one-element-unroll trap.

- scripts/readme_txt.py and README.txt — a generated plain-text twin
  of the README for terminals and pagers. README.txt is generated, never
  edited by hand.

Changed
-------
- **zdeploy verification picks its channel on every retry, and never asks
  the bare IP** — the check used to choose its channel once, before the wait
  loop, by probing; the probes raced the app restart the check exists to wait
  through, so the whole window went to the edge fallback. For a project with
  no domain that fallback had no Host header, and the proxy can then only
  answer from its default vhost — a different product: that is how one
  deploy's check compared another app's build number against its own. Now
  channels are re-resolved each retry in trust order (docker-network
  viaProxy → localhost:<port> → edge with the project's Host), the edge
  is skipped entirely when there is no host to route by, and a project with
  no trustworthy channel is reported as unverifiable instead of guessed at.
  The final warning also says which failure happened: a version that never
  matched (stale/failed build) reads differently from channels that never
  answered (probably still booting). verify.timeoutSeconds joins the
  config so a project that is slow to boot — e.g. one that runs database
  migrations in its entrypoint — can widen its own window instead of
  warning on every routine success.

- An interrupted deploy can no longer destroy server-side .env files —
  the preserve/restore of operator files is transactional: the restore comes
  from a tarball taken before the tree is replaced, so a deploy that dies
  mid-flight leaves the previous files in place instead of an empty
  directory.

- A successful deploy no longer reports failure — `docker compose
  restart` writes routine progress to stderr, which PowerShell 5.1 turns
  into a terminating error under $ErrorActionPreference = 'Stop'; four ssh
  calls bypassed the wrapper that flattens this. All remote steps now run
  through it and are judged by exit code alone.

v1.0.0.0.20 - 2026-08-30
------------------------

Changed
-------
- **zversion bump is once per release, not once per PR** — the usage text
  and the bump help line both said "one per PR, one per defect fix". The
  build number names something that shipped, so a release carrying five PRs
  moves it by one; PRs that never shipped on their own were never separate
  builds. Help text only here, but it is the wording people follow: it stamped
  a single evo.www release as two builds. The historical entry below, which
  records what the rule was when zversion shipped, is deliberately left as
  written.

v1.0.0.0.19 - 2026-08-30
------------------------

Added
-----
- **Read a live build from inside the docker network, not through the public
  proxy** — zdeploy, zec2 and zec2online now prefer
  docker exec <viaProxy> curl http://<upstream>/api/build-version when a
  project sets verify.viaProxy and verify.upstream.

  Two problems it closes. A build stamp is something many sites deliberately
  do not serve publicly, and a checker that reads it over the public URL stops
  working the moment that endpoint is blocked — reporting "unknown", which is
  indistinguishable from "could not reach it". And the proxy answers from
  whichever vhost matches the Host header, so a container with no public route
  was getting another site's version back and failing deploys that had
  worked.

  Reading it from a container on the shared network also exercises the real
  HTTP path, so it proves the app is serving rather than that its database
  knows a version. Purely additive: projects without those two keys behave
  exactly as before.

v1.0.0.0.14 - 2026-08-20
------------------------

Fixed
-----
- scp no longer receives an ssh-only flag — the stdin-hang fix added
  -n to Get-Ec2SshOpts, and the deploy path splats that same array into
  scp as well as ssh. OpenSSH's scp has no -n: it exits 1 with
  unknown option -- n and prints its usage block, so every upload failed.
  Get-Ec2ScpOpts now supplies the shared connection options with -n
  filtered out, derived from Get-Ec2SshOpts rather than duplicated so the
  timeouts cannot drift apart between the two transports. All four scp
  call sites use it, including the recursive directory upload.
  The upload failure message also asserted "Likely server disk space"
  without checking; it now points at scp's own output, where the real
  diagnosis already was.

v1.0.0.0.8 - 2026-08-12
-----------------------

Fixed
-----
- Deploys can no longer hang forever on an ssh prompt — every
  deploy-path ssh/scp now carries BatchMode=yes plus connect and
  keepalive timeouts (Get-Ec2SshOpts in ZHelpers.ps1). Without
  BatchMode, ssh prompts for a passphrase or password and waits
  indefinitely; because the deploy pipes stderr through the pipeline, the
  prompt never reaches the screen and the run just stops under whatever
  step label printed last, with no explanation. Now it fails immediately —
  there is no prompt on this path worth answering. ServerAlive* bounds a
  session that dies mid-command (dropped VPN, sleeping laptop, rebooting
  host) to about a minute instead of hanging.

- Vendored archives survive the archive filter — files under a
  vendor/ directory are exempt from the "no archives in the zip" rule.
  A project that vendors a dependency as vendor/*.tgz needs it in the
  deploy zip; dropping it makes a Dockerfile's COPY vendor ./vendor
  fail at image build, a confusing way to learn the filter ate a build
  input.

- unzip install is idempotent — the remote step ran
  apt-get update && apt-get install -y unzip on every deploy; it now
  checks command -v unzip first and skips the apt round-trip when the
  binary is already there.

- zdeploy edge kind now ships asset subdirectories (#42) — the edge
  deploy uploaded top-level files only, so a project self-hosting assets
  in folders (fonts/, vendor/) lost them on every deploy: docker
  created empty root-owned mount points and nginx served 404s from them,
  which shows up as fonts silently falling back and vendored JS never
  loading. Every subdirectory except nginx-logs/ and .git/ now ships
  recursively, and the ensure edge dir chown is recursive so scp into
  docker-created root-owned dirs cannot fail.

v1.0.0.0.0 - 2026-07-28
-----------------------

Added
-----
- Versioned releases: zversion, zrelease, releases/ — the toolkit now
  carries one version in a 5-segment scheme,
  v{major}.{rc}.{beta}.{alpha}.{build}. zversion bump (one per PR / defect
  fix) and zversion bump-stage release|rc|beta|alpha (zeroes every lower
  segment) rewrite build-version.json, stamp # Version: into every script
  header — so a lone copied script still says which release it came from — and
  regenerate CHECKSUMS.txt in the same step. zrelease packages the current
  version as releases/zscripts-<version>.zip with a .sha256 beside it: one
  hash verifies the download, the bundled CHECKSUMS.txt verifies the
  extracted contents, so nobody needs to clone the repo to get a verifiable
  copy. Released zips are immutable — zrelease refuses to overwrite one.

- zchecksums + CHECKSUMS.txt — a SHA-256 manifest covering every .ps1
  and .cmd, so a download can be verified before anything is run. zchecksums
  checks them; zchecksums -Update regenerates after an intentional edit. The
  manifest is sha256sum format, so sha256sum -c CHECKSUMS.txt works on
  Linux/macOS/WSL too, and the hashes match on every platform because
  .gitattributes pins these files to CRLF everywhere. Flags changed files,
  missing files, and scripts present on disk but absent from the manifest.
  It's an integrity check, not a signature — the manifest sits in the same repo
  as the code, so it catches corruption and accidental drift, not a compromised
  repo. A Pester test fails if the manifest ever goes stale.

- Test suite (Pester) — the toolkit now has automated coverage of its own
  pure logic: Get-ArchiveExcludes (including the deploy-vs-backup rule that
  keeps .env/uploads out of deploys but in backups), config and project
  lookups, remote.composeDir fallback, EC2 target composition, and build-label
  formatting. Run with Invoke-Pester .\tests (Pester 5+). Verified by mutation
  testing — reintroducing each historical bug turns the suite red.

- ZCONFIG environment variable — overrides the path to zconfig.json, so
  a run can target an alternate config. Also gives the test suite a seam for
  injecting a fixture.

- zec2_rotatekeys — safely rotate/reset server-side secrets — a new
  tool for when a secret leaks or a deploy overwrites a production .env
  with dev values. -Rotate KEY regenerates a key on the server
  (openssl rand -hex 32) so the new value never leaves the box; -Set KEY
  takes an operator-known value (e.g. DATABASE_URL, ADMIN_EMAIL) from a
  masked prompt and streams it over SSH stdin — never a command argument,
  never echoed. Backs the server .env up to a timestamped .bak first,
  updates the key atomically (matches or appends), auto-detects
  backend/.env from deploy.preserve, and with -Restart recreates
  the container (up -d --force-recreate, so the new values actually load —
  a plain restart keeps the old environment). -WhatIf previews the plan
  without touching anything.

- zkill all — zkill now accepts all, stopping the dev server of
  every project that has a ports.dev (edge/docker stacks with no local dev
  server are skipped). Brings it in line with zdeploy all / zbackup all;
  the one-shot "stop everything I've got running locally".

- zdeploy server-side health verification (verify block) — projects
  not published through the edge proxy can declare
  "verify": { "port": ..., "path": "/health", "expect": "..." } and the
  deploy is checked from the server itself (curl localhost:<port><path>
  over SSH) instead of hitting the public IP. Fixes a false PASS where the
  proxy's default vhost answered for apps that never started; projects
  with neither domain nor verify are now reported as NOT verified.

- zdeploy optional deploy.gitPull — git pull --ff-only in the
  project root before zipping. zdeploy zips the working tree and doesn't
  otherwise pull, so a checkout left behind origin after a merged PR would
  deploy stale code while still bumping the build number — success that
  changes nothing. A failed pull aborts the deploy instead.

- Per-project start config block — zstart honors optional pre-start
  steps from zconfig.json: "gitPull": true runs git pull --ff-only in
  the project root before starting (never boot a stale checkout), and
  "env": { ... } sets environment variables for the dev-server process.
  Example added to zconfig.example.json.

- Switch-style argument tolerance — a leading dash on a project key is
  ignored everywhere (zdeploy -myapp == zdeploy myapp), for hands that
  grew up on per-project switches.

Changed
-------
- zbackup / zbackup_and_sync require an explicit target — running
  them bare now shows usage instead of quietly backing up every project;
  all does what bare invocation used to (matching zdeploy). The
  scheduled task created by setup_backup_schedule.ps1 passes all —
  re-run it if your task was registered before this change.

- zbackup parses more DATABASE_URL styles — double/single-quoted
  values (Prisma convention), postgres:// and postgresql+driver://
  schemes, and URLs without an explicit port (defaults to 5432) all work;
  previously these skipped the Postgres dump with "Could not parse
  DATABASE_URL".

- zkill / port cleanup kills the whole process tree — listeners on a
  project's port are now terminated children-first. Auto-reloading servers
  (uvicorn/watchfiles, nodemon) spawn workers that inherit the listening
  socket; killing only the parent left orphans serving stale code.

- zbackup finds DATABASE_URL in backend\.env too — projects with a
  frontend/backend split get their Postgres dump bundled without needing a
  root-level .env.

Fixed
-----
- zdeploy no longer deletes operator-managed files on deploy (#2) —
  the project-directory replacement preserved only ./.env, silently
  destroying every other server-side file (.env.db, staged signing
  keys, certs) on every deploy. All .env* files at the project root are
  now preserved by default, plus anything listed in the new
  deploy.preserve array (files or directories); the vite kind, which
  previously preserved nothing, gets the same protection. Found the hard
  way: a first production deploy of an auth service wiped its staged DB
  credentials and RSA signing keys.

1.0.0
-----

Initial public release: zstart / zkill / zrestart (local dev servers),
zdeploy (zip → upload → compose build → live build-version verification,
with handlers for python / vite / nextjs / edge / docker project kinds),
zec2 / zec2online / zrepair (health checks and recovery), zbackup /
zbackup_ec2 / zsync (local, server-side, and offsite backups), all driven
by a single gitignored zconfig.json.
