mirror of
https://github.com/kellymichels/zscripts-token-savers
synced 2026-10-07 07:18:18 +00:00
docs(changelog): give every shipped release its own section, and generate the twin (#68)
The changelog said nothing had been released since 1.0.0. Twenty-two builds had shipped. Twenty-one entries sat under "## Unreleased" in a file with exactly two headings, so a reader at any tag found no section for the version they were holding. Which release carried which entry is DERIVED, not guessed: for every line in the region, the commit that introduced it, then the earliest tag containing that commit. That yields seven releases - .22, .21, .20, .19, .14, .8 and .0. No entry text changed. A verification pass compares the multiset of non-heading lines before and after and refuses to write if anything was lost, gained or duplicated; entries move under their release, so it compares as a multiset rather than in order. CHANGELOG.txt was kept BY HAND and drifted the moment the .md was reorganised, which is the failure the plain-text-twin rule exists to prevent. readme_txt.py becomes plaintext_twins.py and renders every pair. It also drops <!-- --> markers, invisible in markdown and stray punctuation in a text file - that is the whole of README.txt's diff. The --check that keeps twins honest was never run. readme_txt.py shipped one and its docstring claimed "the test suite runs --check"; nothing invoked it, so a twin could disagree with its markdown indefinitely. tests/PlainTextTwins.Tests.ps1 runs it. That test skipped on its first run while claiming to pass: -Skip is evaluated during DISCOVERY, before BeforeAll, so the python lookup left the flag $null. Resolved in BeforeDiscovery, and when python really is absent the result is INCONCLUSIVE rather than a green tick for a check that never happened. Mutation-checked: appending one line to CHANGELOG.txt fails "every twin is in sync with its markdown", and only that test. tests: 243 passed, 0 failed, 1 skipped (the no-python reporter, correctly).
This commit is contained in:
parent
560a2064a2
commit
135c585c4b
92
CHANGELOG.md
92
CHANGELOG.md
@ -10,6 +10,30 @@ Notable changes to the Evomedia.net Token Savers.
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### 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
|
### Changed
|
||||||
- **The sanitization denylist moved to `tests/sanitization-patterns.psd1`**,
|
- **The sanitization denylist moved to `tests/sanitization-patterns.psd1`**,
|
||||||
so the test suite here and the publisher in the private toolkit read one
|
so the test suite here and the publisher in the private toolkit read one
|
||||||
@ -19,6 +43,18 @@ Notable changes to the Evomedia.net Token Savers.
|
|||||||
names, operator paths. It therefore reported "clean" on files this suite
|
names, operator paths. It therefore reported "clean" on files this suite
|
||||||
rejects. No rule changed; only where they live.
|
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
|
### Changed
|
||||||
- **`zdeploy` verification picks its channel on every retry, and never asks
|
- **`zdeploy` verification picks its channel on every retry, and never asks
|
||||||
@ -38,16 +74,22 @@ Notable changes to the Evomedia.net Token Savers.
|
|||||||
config so a project that is slow to boot — e.g. one that runs database
|
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
|
migrations in its entrypoint — can widen its own window instead of
|
||||||
warning on every routine success.
|
warning on every routine success.
|
||||||
|
|
||||||
- **An interrupted deploy can no longer destroy server-side `.env` files** —
|
- **An interrupted deploy can no longer destroy server-side `.env` files** —
|
||||||
the preserve/restore of operator files is transactional: the restore comes
|
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
|
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
|
mid-flight leaves the previous files in place instead of an empty
|
||||||
directory.
|
directory.
|
||||||
|
|
||||||
- **A successful deploy no longer reports failure** — `docker compose
|
- **A successful deploy no longer reports failure** — `docker compose
|
||||||
restart` writes routine progress to stderr, which PowerShell 5.1 turns
|
restart` writes routine progress to stderr, which PowerShell 5.1 turns
|
||||||
into a terminating error under `$ErrorActionPreference = 'Stop'`; four ssh
|
into a terminating error under `$ErrorActionPreference = 'Stop'`; four ssh
|
||||||
calls bypassed the wrapper that flattens this. All remote steps now run
|
calls bypassed the wrapper that flattens this. All remote steps now run
|
||||||
through it and are judged by exit code alone.
|
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
|
- **`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
|
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
|
build number names something that shipped, so a release carrying five PRs
|
||||||
@ -57,15 +99,9 @@ Notable changes to the Evomedia.net Token Savers.
|
|||||||
records what the rule was when `zversion` shipped, is deliberately left as
|
records what the rule was when `zversion` shipped, is deliberately left as
|
||||||
written.
|
written.
|
||||||
|
|
||||||
|
## v1.0.0.0.19 - 2026-08-30
|
||||||
|
|
||||||
### Added
|
### 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.
|
|
||||||
- **Read a live build from inside the docker network, not through the public
|
- **Read a live build from inside the docker network, not through the public
|
||||||
proxy** — `zdeploy`, `zec2` and `zec2online` now prefer
|
proxy** — `zdeploy`, `zec2` and `zec2online` now prefer
|
||||||
`docker exec <viaProxy> curl http://<upstream>/api/build-version` when a
|
`docker exec <viaProxy> curl http://<upstream>/api/build-version` when a
|
||||||
@ -84,6 +120,7 @@ Notable changes to the Evomedia.net Token Savers.
|
|||||||
knows a version. Purely additive: projects without those two keys behave
|
knows a version. Purely additive: projects without those two keys behave
|
||||||
exactly as before.
|
exactly as before.
|
||||||
|
|
||||||
|
## v1.0.0.0.14 - 2026-08-20
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- **`scp` no longer receives an ssh-only flag** — the stdin-hang fix added
|
- **`scp` no longer receives an ssh-only flag** — the stdin-hang fix added
|
||||||
@ -98,6 +135,8 @@ Notable changes to the Evomedia.net Token Savers.
|
|||||||
without checking; it now points at `scp`'s own output, where the real
|
without checking; it now points at `scp`'s own output, where the real
|
||||||
diagnosis already was.
|
diagnosis already was.
|
||||||
|
|
||||||
|
## v1.0.0.0.8 - 2026-08-12
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- **Deploys can no longer hang forever on an ssh prompt** — every
|
- **Deploys can no longer hang forever on an ssh prompt** — every
|
||||||
deploy-path `ssh`/`scp` now carries `BatchMode=yes` plus connect and
|
deploy-path `ssh`/`scp` now carries `BatchMode=yes` plus connect and
|
||||||
@ -109,16 +148,19 @@ Notable changes to the Evomedia.net Token Savers.
|
|||||||
there is no prompt on this path worth answering. `ServerAlive*` bounds a
|
there is no prompt on this path worth answering. `ServerAlive*` bounds a
|
||||||
session that dies mid-command (dropped VPN, sleeping laptop, rebooting
|
session that dies mid-command (dropped VPN, sleeping laptop, rebooting
|
||||||
host) to about a minute instead of hanging.
|
host) to about a minute instead of hanging.
|
||||||
|
|
||||||
- **Vendored archives survive the archive filter** — files under a
|
- **Vendored archives survive the archive filter** — files under a
|
||||||
`vendor/` directory are exempt from the "no archives in the zip" rule.
|
`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
|
A project that vendors a dependency as `vendor/*.tgz` needs it in the
|
||||||
deploy zip; dropping it makes a Dockerfile's `COPY vendor ./vendor`
|
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
|
fail at image build, a confusing way to learn the filter ate a build
|
||||||
input.
|
input.
|
||||||
|
|
||||||
- **`unzip` install is idempotent** — the remote step ran
|
- **`unzip` install is idempotent** — the remote step ran
|
||||||
`apt-get update && apt-get install -y unzip` on every deploy; it now
|
`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
|
checks `command -v unzip` first and skips the apt round-trip when the
|
||||||
binary is already there.
|
binary is already there.
|
||||||
|
|
||||||
- **`zdeploy` edge kind now ships asset subdirectories** (#42) — the edge
|
- **`zdeploy` edge kind now ships asset subdirectories** (#42) — the edge
|
||||||
deploy uploaded top-level files only, so a project self-hosting assets
|
deploy uploaded top-level files only, so a project self-hosting assets
|
||||||
in folders (`fonts/`, `vendor/`) lost them on every deploy: docker
|
in folders (`fonts/`, `vendor/`) lost them on every deploy: docker
|
||||||
@ -127,15 +169,8 @@ Notable changes to the Evomedia.net Token Savers.
|
|||||||
loading. Every subdirectory except `nginx-logs/` and `.git/` now ships
|
loading. Every subdirectory except `nginx-logs/` and `.git/` now ships
|
||||||
recursively, and the `ensure edge dir` chown is recursive so scp into
|
recursively, and the `ensure edge dir` chown is recursive so scp into
|
||||||
docker-created root-owned dirs cannot fail.
|
docker-created root-owned dirs cannot fail.
|
||||||
- **`zdeploy` no longer deletes operator-managed files on deploy** (#2) —
|
|
||||||
the project-directory replacement preserved only `./.env`, silently
|
## v1.0.0.0.0 - 2026-07-28
|
||||||
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.
|
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- **Versioned releases: `zversion`, `zrelease`, `releases/`** — the toolkit now
|
- **Versioned releases: `zversion`, `zrelease`, `releases/`** — the toolkit now
|
||||||
@ -149,6 +184,7 @@ Notable changes to the Evomedia.net Token Savers.
|
|||||||
hash verifies the download, the bundled `CHECKSUMS.txt` verifies the
|
hash verifies the download, the bundled `CHECKSUMS.txt` verifies the
|
||||||
extracted contents, so nobody needs to clone the repo to get a verifiable
|
extracted contents, so nobody needs to clone the repo to get a verifiable
|
||||||
copy. Released zips are immutable — `zrelease` refuses to overwrite one.
|
copy. Released zips are immutable — `zrelease` refuses to overwrite one.
|
||||||
|
|
||||||
- **`zchecksums` + `CHECKSUMS.txt`** — a SHA-256 manifest covering every `.ps1`
|
- **`zchecksums` + `CHECKSUMS.txt`** — a SHA-256 manifest covering every `.ps1`
|
||||||
and `.cmd`, so a download can be verified before anything is run. `zchecksums`
|
and `.cmd`, so a download can be verified before anything is run. `zchecksums`
|
||||||
checks them; `zchecksums -Update` regenerates after an intentional edit. The
|
checks them; `zchecksums -Update` regenerates after an intentional edit. The
|
||||||
@ -159,15 +195,18 @@ Notable changes to the Evomedia.net Token Savers.
|
|||||||
It's an integrity check, not a signature — the manifest sits in the same repo
|
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
|
as the code, so it catches corruption and accidental drift, not a compromised
|
||||||
repo. A Pester test fails if the manifest ever goes stale.
|
repo. A Pester test fails if the manifest ever goes stale.
|
||||||
|
|
||||||
- **Test suite (Pester)** — the toolkit now has automated coverage of its own
|
- **Test suite (Pester)** — the toolkit now has automated coverage of its own
|
||||||
pure logic: `Get-ArchiveExcludes` (including the deploy-vs-backup rule that
|
pure logic: `Get-ArchiveExcludes` (including the deploy-vs-backup rule that
|
||||||
keeps `.env`/`uploads` out of deploys but *in* backups), config and project
|
keeps `.env`/`uploads` out of deploys but *in* backups), config and project
|
||||||
lookups, `remote.composeDir` fallback, EC2 target composition, and build-label
|
lookups, `remote.composeDir` fallback, EC2 target composition, and build-label
|
||||||
formatting. Run with `Invoke-Pester .\tests` (Pester 5+). Verified by mutation
|
formatting. Run with `Invoke-Pester .\tests` (Pester 5+). Verified by mutation
|
||||||
testing — reintroducing each historical bug turns the suite red.
|
testing — reintroducing each historical bug turns the suite red.
|
||||||
|
|
||||||
- **`ZCONFIG` environment variable** — overrides the path to `zconfig.json`, so
|
- **`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
|
a run can target an alternate config. Also gives the test suite a seam for
|
||||||
injecting a fixture.
|
injecting a fixture.
|
||||||
|
|
||||||
- **`zec2_rotatekeys` — safely rotate/reset server-side secrets** — a new
|
- **`zec2_rotatekeys` — safely rotate/reset server-side secrets** — a new
|
||||||
tool for when a secret leaks or a deploy overwrites a production `.env`
|
tool for when a secret leaks or a deploy overwrites a production `.env`
|
||||||
with dev values. `-Rotate KEY` regenerates a key **on the server**
|
with dev values. `-Rotate KEY` regenerates a key **on the server**
|
||||||
@ -180,10 +219,12 @@ Notable changes to the Evomedia.net Token Savers.
|
|||||||
the container (`up -d --force-recreate`, so the new values actually load —
|
the container (`up -d --force-recreate`, so the new values actually load —
|
||||||
a plain restart keeps the old environment). `-WhatIf` previews the plan
|
a plain restart keeps the old environment). `-WhatIf` previews the plan
|
||||||
without touching anything.
|
without touching anything.
|
||||||
|
|
||||||
- **`zkill all`** — `zkill` now accepts `all`, stopping the dev server of
|
- **`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
|
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`;
|
server are skipped). Brings it in line with `zdeploy all` / `zbackup all`;
|
||||||
the one-shot "stop everything I've got running locally".
|
the one-shot "stop everything I've got running locally".
|
||||||
|
|
||||||
- **`zdeploy` server-side health verification (`verify` block)** — projects
|
- **`zdeploy` server-side health verification (`verify` block)** — projects
|
||||||
not published through the edge proxy can declare
|
not published through the edge proxy can declare
|
||||||
`"verify": { "port": ..., "path": "/health", "expect": "..." }` and the
|
`"verify": { "port": ..., "path": "/health", "expect": "..." }` and the
|
||||||
@ -191,16 +232,19 @@ Notable changes to the Evomedia.net Token Savers.
|
|||||||
over SSH) instead of hitting the public IP. Fixes a false PASS where the
|
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
|
proxy's default vhost answered for apps that never started; projects
|
||||||
with neither `domain` nor `verify` are now reported as NOT verified.
|
with neither `domain` nor `verify` are now reported as NOT verified.
|
||||||
|
|
||||||
- **`zdeploy` optional `deploy.gitPull`** — `git pull --ff-only` in the
|
- **`zdeploy` optional `deploy.gitPull`** — `git pull --ff-only` in the
|
||||||
project root before zipping. `zdeploy` zips the working tree and doesn't
|
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
|
otherwise pull, so a checkout left behind `origin` after a merged PR would
|
||||||
deploy stale code while still bumping the build number — success that
|
deploy stale code while still bumping the build number — success that
|
||||||
changes nothing. A failed pull aborts the deploy instead.
|
changes nothing. A failed pull aborts the deploy instead.
|
||||||
|
|
||||||
- **Per-project `start` config block** — `zstart` honors optional pre-start
|
- **Per-project `start` config block** — `zstart` honors optional pre-start
|
||||||
steps from `zconfig.json`: `"gitPull": true` runs `git pull --ff-only` in
|
steps from `zconfig.json`: `"gitPull": true` runs `git pull --ff-only` in
|
||||||
the project root before starting (never boot a stale checkout), and
|
the project root before starting (never boot a stale checkout), and
|
||||||
`"env": { ... }` sets environment variables for the dev-server process.
|
`"env": { ... }` sets environment variables for the dev-server process.
|
||||||
Example added to `zconfig.example.json`.
|
Example added to `zconfig.example.json`.
|
||||||
|
|
||||||
- **Switch-style argument tolerance** — a leading dash on a project key is
|
- **Switch-style argument tolerance** — a leading dash on a project key is
|
||||||
ignored everywhere (`zdeploy -myapp` == `zdeploy myapp`), for hands that
|
ignored everywhere (`zdeploy -myapp` == `zdeploy myapp`), for hands that
|
||||||
grew up on per-project switches.
|
grew up on per-project switches.
|
||||||
@ -211,19 +255,33 @@ Notable changes to the Evomedia.net Token Savers.
|
|||||||
`all` does what bare invocation used to (matching `zdeploy`). The
|
`all` does what bare invocation used to (matching `zdeploy`). The
|
||||||
scheduled task created by `setup_backup_schedule.ps1` passes `all` —
|
scheduled task created by `setup_backup_schedule.ps1` passes `all` —
|
||||||
re-run it if your task was registered before this change.
|
re-run it if your task was registered before this change.
|
||||||
|
|
||||||
- **`zbackup` parses more `DATABASE_URL` styles** — double/single-quoted
|
- **`zbackup` parses more `DATABASE_URL` styles** — double/single-quoted
|
||||||
values (Prisma convention), `postgres://` and `postgresql+driver://`
|
values (Prisma convention), `postgres://` and `postgresql+driver://`
|
||||||
schemes, and URLs without an explicit port (defaults to 5432) all work;
|
schemes, and URLs without an explicit port (defaults to 5432) all work;
|
||||||
previously these skipped the Postgres dump with "Could not parse
|
previously these skipped the Postgres dump with "Could not parse
|
||||||
DATABASE_URL".
|
DATABASE_URL".
|
||||||
|
|
||||||
- **`zkill` / port cleanup kills the whole process tree** — listeners on a
|
- **`zkill` / port cleanup kills the whole process tree** — listeners on a
|
||||||
project's port are now terminated children-first. Auto-reloading servers
|
project's port are now terminated children-first. Auto-reloading servers
|
||||||
(uvicorn/watchfiles, nodemon) spawn workers that inherit the listening
|
(uvicorn/watchfiles, nodemon) spawn workers that inherit the listening
|
||||||
socket; killing only the parent left orphans serving stale code.
|
socket; killing only the parent left orphans serving stale code.
|
||||||
|
|
||||||
- **`zbackup` finds `DATABASE_URL` in `backend\.env` too** — projects with a
|
- **`zbackup` finds `DATABASE_URL` in `backend\.env` too** — projects with a
|
||||||
frontend/backend split get their Postgres dump bundled without needing a
|
frontend/backend split get their Postgres dump bundled without needing a
|
||||||
root-level `.env`.
|
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
|
## 1.0.0
|
||||||
|
|
||||||
Initial public release: `zstart` / `zkill` / `zrestart` (local dev servers),
|
Initial public release: `zstart` / `zkill` / `zrestart` (local dev servers),
|
||||||
|
|||||||
206
CHANGELOG.txt
206
CHANGELOG.txt
@ -9,80 +9,133 @@ Notable changes to the Evomedia.net Token Savers.
|
|||||||
|
|
||||||
Unreleased
|
Unreleased
|
||||||
----------
|
----------
|
||||||
|
|
||||||
Changed
|
Changed
|
||||||
- The sanitization denylist moved to tests/sanitization-patterns.psd1, so
|
-------
|
||||||
the test suite here and the publisher in the private toolkit read one
|
- 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
|
list instead of keeping two. They had two, and they disagreed: the
|
||||||
publisher's scan looked only for secrets, while these rules are about
|
publisher's scan looked only for secrets, while these rules are about
|
||||||
identity - internal project names, product domains, private-only script
|
identity — internal project names, product domains, private-only script
|
||||||
names, operator paths. It therefore reported "clean" on files this suite
|
names, operator paths. It therefore reported "clean" on files this suite
|
||||||
rejects. No rule changed; only where they live.
|
rejects. No rule changed; only where they live.
|
||||||
|
|
||||||
|
v1.0.0.0.21 - 2026-08-31
|
||||||
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.
|
|
||||||
- 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.
|
|
||||||
|
|
||||||
Added
|
Added
|
||||||
- tests/VerifyPlan.Tests.ps1 - the verification channel-selection rules
|
-----
|
||||||
|
- tests/VerifyPlan.Tests.ps1 — the verification channel-selection rules
|
||||||
are pure functions in ZHelpers.ps1 (Get-VerifyAttempts,
|
are pure functions in ZHelpers.ps1 (Get-VerifyAttempts,
|
||||||
Get-VerifyTimeout) and Pester pins them, including "a project with no
|
Get-VerifyTimeout) and Pester pins them, including "a project with no
|
||||||
domain must never produce an edge attempt" and the PowerShell 5.1
|
domain must never produce an edge attempt" and the PowerShell 5.1
|
||||||
one-element-unroll trap.
|
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
|
- 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.
|
edited by hand.
|
||||||
- Read a live build from inside the docker network, not through the public
|
|
||||||
proxy — zdeploy, zec2 and zec2online now prefer
|
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
|
docker exec <viaProxy> curl http://<upstream>/api/build-version when a
|
||||||
project sets verify.viaProxy and verify.upstream.
|
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.
|
|
||||||
|
|
||||||
|
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
|
Fixed
|
||||||
|
-----
|
||||||
- scp no longer receives an ssh-only flag — the stdin-hang fix added
|
- 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
|
-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
|
scp as well as ssh. OpenSSH's scp has no -n: it exits 1 with
|
||||||
@ -95,6 +148,8 @@ Fixed
|
|||||||
without checking; it now points at scp's own output, where the real
|
without checking; it now points at scp's own output, where the real
|
||||||
diagnosis already was.
|
diagnosis already was.
|
||||||
|
|
||||||
|
v1.0.0.0.8 - 2026-08-12
|
||||||
|
-----------------------
|
||||||
|
|
||||||
Fixed
|
Fixed
|
||||||
-----
|
-----
|
||||||
@ -108,16 +163,19 @@ Fixed
|
|||||||
there is no prompt on this path worth answering. ServerAlive* bounds a
|
there is no prompt on this path worth answering. ServerAlive* bounds a
|
||||||
session that dies mid-command (dropped VPN, sleeping laptop, rebooting
|
session that dies mid-command (dropped VPN, sleeping laptop, rebooting
|
||||||
host) to about a minute instead of hanging.
|
host) to about a minute instead of hanging.
|
||||||
|
|
||||||
- Vendored archives survive the archive filter — files under a
|
- Vendored archives survive the archive filter — files under a
|
||||||
vendor/ directory are exempt from the "no archives in the zip" rule.
|
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
|
A project that vendors a dependency as vendor/*.tgz needs it in the
|
||||||
deploy zip; dropping it makes a Dockerfile's COPY vendor ./vendor
|
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
|
fail at image build, a confusing way to learn the filter ate a build
|
||||||
input.
|
input.
|
||||||
|
|
||||||
- unzip install is idempotent — the remote step ran
|
- unzip install is idempotent — the remote step ran
|
||||||
apt-get update && apt-get install -y unzip on every deploy; it now
|
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
|
checks command -v unzip first and skips the apt round-trip when the
|
||||||
binary is already there.
|
binary is already there.
|
||||||
|
|
||||||
- zdeploy edge kind now ships asset subdirectories (#42) — the edge
|
- zdeploy edge kind now ships asset subdirectories (#42) — the edge
|
||||||
deploy uploaded top-level files only, so a project self-hosting assets
|
deploy uploaded top-level files only, so a project self-hosting assets
|
||||||
in folders (fonts/, vendor/) lost them on every deploy: docker
|
in folders (fonts/, vendor/) lost them on every deploy: docker
|
||||||
@ -126,15 +184,9 @@ Fixed
|
|||||||
loading. Every subdirectory except nginx-logs/ and .git/ now ships
|
loading. Every subdirectory except nginx-logs/ and .git/ now ships
|
||||||
recursively, and the ensure edge dir chown is recursive so scp into
|
recursively, and the ensure edge dir chown is recursive so scp into
|
||||||
docker-created root-owned dirs cannot fail.
|
docker-created root-owned dirs cannot fail.
|
||||||
- zdeploy no longer deletes operator-managed files on deploy (#2) —
|
|
||||||
the project-directory replacement preserved only ./.env, silently
|
v1.0.0.0.0 - 2026-07-28
|
||||||
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.
|
|
||||||
|
|
||||||
Added
|
Added
|
||||||
-----
|
-----
|
||||||
@ -149,6 +201,7 @@ Added
|
|||||||
hash verifies the download, the bundled CHECKSUMS.txt verifies the
|
hash verifies the download, the bundled CHECKSUMS.txt verifies the
|
||||||
extracted contents, so nobody needs to clone the repo to get a verifiable
|
extracted contents, so nobody needs to clone the repo to get a verifiable
|
||||||
copy. Released zips are immutable — zrelease refuses to overwrite one.
|
copy. Released zips are immutable — zrelease refuses to overwrite one.
|
||||||
|
|
||||||
- zchecksums + CHECKSUMS.txt — a SHA-256 manifest covering every .ps1
|
- zchecksums + CHECKSUMS.txt — a SHA-256 manifest covering every .ps1
|
||||||
and .cmd, so a download can be verified before anything is run. zchecksums
|
and .cmd, so a download can be verified before anything is run. zchecksums
|
||||||
checks them; zchecksums -Update regenerates after an intentional edit. The
|
checks them; zchecksums -Update regenerates after an intentional edit. The
|
||||||
@ -159,15 +212,18 @@ Added
|
|||||||
It's an integrity check, not a signature — the manifest sits in the same repo
|
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
|
as the code, so it catches corruption and accidental drift, not a compromised
|
||||||
repo. A Pester test fails if the manifest ever goes stale.
|
repo. A Pester test fails if the manifest ever goes stale.
|
||||||
|
|
||||||
- Test suite (Pester) — the toolkit now has automated coverage of its own
|
- Test suite (Pester) — the toolkit now has automated coverage of its own
|
||||||
pure logic: Get-ArchiveExcludes (including the deploy-vs-backup rule that
|
pure logic: Get-ArchiveExcludes (including the deploy-vs-backup rule that
|
||||||
keeps .env/uploads out of deploys but in backups), config and project
|
keeps .env/uploads out of deploys but in backups), config and project
|
||||||
lookups, remote.composeDir fallback, EC2 target composition, and build-label
|
lookups, remote.composeDir fallback, EC2 target composition, and build-label
|
||||||
formatting. Run with Invoke-Pester .\tests (Pester 5+). Verified by mutation
|
formatting. Run with Invoke-Pester .\tests (Pester 5+). Verified by mutation
|
||||||
testing — reintroducing each historical bug turns the suite red.
|
testing — reintroducing each historical bug turns the suite red.
|
||||||
|
|
||||||
- ZCONFIG environment variable — overrides the path to zconfig.json, so
|
- 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
|
a run can target an alternate config. Also gives the test suite a seam for
|
||||||
injecting a fixture.
|
injecting a fixture.
|
||||||
|
|
||||||
- zec2_rotatekeys — safely rotate/reset server-side secrets — a new
|
- zec2_rotatekeys — safely rotate/reset server-side secrets — a new
|
||||||
tool for when a secret leaks or a deploy overwrites a production .env
|
tool for when a secret leaks or a deploy overwrites a production .env
|
||||||
with dev values. -Rotate KEY regenerates a key on the server
|
with dev values. -Rotate KEY regenerates a key on the server
|
||||||
@ -180,10 +236,12 @@ Added
|
|||||||
the container (up -d --force-recreate, so the new values actually load —
|
the container (up -d --force-recreate, so the new values actually load —
|
||||||
a plain restart keeps the old environment). -WhatIf previews the plan
|
a plain restart keeps the old environment). -WhatIf previews the plan
|
||||||
without touching anything.
|
without touching anything.
|
||||||
|
|
||||||
- zkill all — zkill now accepts all, stopping the dev server of
|
- 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
|
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;
|
server are skipped). Brings it in line with zdeploy all / zbackup all;
|
||||||
the one-shot "stop everything I've got running locally".
|
the one-shot "stop everything I've got running locally".
|
||||||
|
|
||||||
- zdeploy server-side health verification (verify block) — projects
|
- zdeploy server-side health verification (verify block) — projects
|
||||||
not published through the edge proxy can declare
|
not published through the edge proxy can declare
|
||||||
"verify": { "port": ..., "path": "/health", "expect": "..." } and the
|
"verify": { "port": ..., "path": "/health", "expect": "..." } and the
|
||||||
@ -191,16 +249,19 @@ Added
|
|||||||
over SSH) instead of hitting the public IP. Fixes a false PASS where the
|
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
|
proxy's default vhost answered for apps that never started; projects
|
||||||
with neither domain nor verify are now reported as NOT verified.
|
with neither domain nor verify are now reported as NOT verified.
|
||||||
|
|
||||||
- zdeploy optional deploy.gitPull — git pull --ff-only in the
|
- zdeploy optional deploy.gitPull — git pull --ff-only in the
|
||||||
project root before zipping. zdeploy zips the working tree and doesn't
|
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
|
otherwise pull, so a checkout left behind origin after a merged PR would
|
||||||
deploy stale code while still bumping the build number — success that
|
deploy stale code while still bumping the build number — success that
|
||||||
changes nothing. A failed pull aborts the deploy instead.
|
changes nothing. A failed pull aborts the deploy instead.
|
||||||
|
|
||||||
- Per-project start config block — zstart honors optional pre-start
|
- Per-project start config block — zstart honors optional pre-start
|
||||||
steps from zconfig.json: "gitPull": true runs git pull --ff-only in
|
steps from zconfig.json: "gitPull": true runs git pull --ff-only in
|
||||||
the project root before starting (never boot a stale checkout), and
|
the project root before starting (never boot a stale checkout), and
|
||||||
"env": { ... } sets environment variables for the dev-server process.
|
"env": { ... } sets environment variables for the dev-server process.
|
||||||
Example added to zconfig.example.json.
|
Example added to zconfig.example.json.
|
||||||
|
|
||||||
- Switch-style argument tolerance — a leading dash on a project key is
|
- Switch-style argument tolerance — a leading dash on a project key is
|
||||||
ignored everywhere (zdeploy -myapp == zdeploy myapp), for hands that
|
ignored everywhere (zdeploy -myapp == zdeploy myapp), for hands that
|
||||||
grew up on per-project switches.
|
grew up on per-project switches.
|
||||||
@ -212,19 +273,34 @@ Changed
|
|||||||
all does what bare invocation used to (matching zdeploy). The
|
all does what bare invocation used to (matching zdeploy). The
|
||||||
scheduled task created by setup_backup_schedule.ps1 passes all —
|
scheduled task created by setup_backup_schedule.ps1 passes all —
|
||||||
re-run it if your task was registered before this change.
|
re-run it if your task was registered before this change.
|
||||||
|
|
||||||
- zbackup parses more DATABASE_URL styles — double/single-quoted
|
- zbackup parses more DATABASE_URL styles — double/single-quoted
|
||||||
values (Prisma convention), postgres:// and postgresql+driver://
|
values (Prisma convention), postgres:// and postgresql+driver://
|
||||||
schemes, and URLs without an explicit port (defaults to 5432) all work;
|
schemes, and URLs without an explicit port (defaults to 5432) all work;
|
||||||
previously these skipped the Postgres dump with "Could not parse
|
previously these skipped the Postgres dump with "Could not parse
|
||||||
DATABASE_URL".
|
DATABASE_URL".
|
||||||
|
|
||||||
- zkill / port cleanup kills the whole process tree — listeners on a
|
- zkill / port cleanup kills the whole process tree — listeners on a
|
||||||
project's port are now terminated children-first. Auto-reloading servers
|
project's port are now terminated children-first. Auto-reloading servers
|
||||||
(uvicorn/watchfiles, nodemon) spawn workers that inherit the listening
|
(uvicorn/watchfiles, nodemon) spawn workers that inherit the listening
|
||||||
socket; killing only the parent left orphans serving stale code.
|
socket; killing only the parent left orphans serving stale code.
|
||||||
|
|
||||||
- zbackup finds DATABASE_URL in backend\.env too — projects with a
|
- zbackup finds DATABASE_URL in backend\.env too — projects with a
|
||||||
frontend/backend split get their Postgres dump bundled without needing a
|
frontend/backend split get their Postgres dump bundled without needing a
|
||||||
root-level .env.
|
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
|
1.0.0
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
<!--
|
|
||||||
Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||||
Created by Kelly Michels · dev@evomedia.net
|
Created by Kelly Michels · dev@evomedia.net
|
||||||
Licensed under the MIT License. See LICENSE.
|
Licensed under the MIT License. See LICENSE.
|
||||||
-->
|
|
||||||
|
|
||||||
zscripts Token Savers
|
zscripts Token Savers
|
||||||
=====================
|
=====================
|
||||||
|
|||||||
104
scripts/plaintext_twins.py
Normal file
104
scripts/plaintext_twins.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
"""Render this repo's markdown to plain-text twins with the markup removed.
|
||||||
|
|
||||||
|
The .txt files exist for terminals, pagers and anywhere markdown doesn't
|
||||||
|
render. They are generated - never edit one by hand:
|
||||||
|
|
||||||
|
python scripts/plaintext_twins.py # rewrite every .txt twin
|
||||||
|
python scripts/plaintext_twins.py --check # exit 1 if any is out of sync
|
||||||
|
|
||||||
|
tests/PlainTextTwins.Tests.ps1 runs --check, so a markdown edit that forgets
|
||||||
|
to regenerate fails the suite instead of shipping a twin that disagrees with
|
||||||
|
the file it mirrors.
|
||||||
|
|
||||||
|
Was readme_txt.py, which did README only. CHANGELOG.txt was kept by hand and
|
||||||
|
drifted the moment CHANGELOG.md was reorganised - and its docstring claimed a
|
||||||
|
--check the suite never actually ran. Both are fixed here: one renderer, every
|
||||||
|
pair, and a test that invokes it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
# Every markdown file that owes the repo a plain-text twin.
|
||||||
|
PAIRS = (
|
||||||
|
("README.md", "README.txt"),
|
||||||
|
("CHANGELOG.md", "CHANGELOG.txt"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _inline(text: str) -> str:
|
||||||
|
text = re.sub(r"!\[([^\]]*)\]\([^)]*\)", r"\1", text) # images -> alt text
|
||||||
|
text = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r"\1 (\2)", text) # links -> text (url)
|
||||||
|
text = re.sub(r"\*\*([^*]+)\*\*", r"\1", text) # bold
|
||||||
|
text = re.sub(r"(?<!\*)\*([^*\n]+)\*(?!\*)", r"\1", text) # italic
|
||||||
|
text = re.sub(r"`([^`]+)`", r"\1", text) # inline code
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def render(md: str) -> str:
|
||||||
|
out: list[str] = []
|
||||||
|
in_fence = False
|
||||||
|
for line in md.splitlines():
|
||||||
|
if line.lstrip().startswith("```"):
|
||||||
|
# Drop the fence markers; the code itself stays, indented so it
|
||||||
|
# still reads as a block without the backticks.
|
||||||
|
in_fence = not in_fence
|
||||||
|
continue
|
||||||
|
if in_fence:
|
||||||
|
out.append((" " + line) if line else "")
|
||||||
|
continue
|
||||||
|
# An HTML comment is invisible in rendered markdown, but its markers
|
||||||
|
# are not invisible in a text file - they read as stray punctuation.
|
||||||
|
# Keep what the comment says, drop the <!-- --> around it.
|
||||||
|
if line.strip() in ("<!--", "-->"):
|
||||||
|
continue
|
||||||
|
heading = re.match(r"^(#{1,6})\s+(.*)$", line)
|
||||||
|
if heading:
|
||||||
|
text = _inline(heading.group(2))
|
||||||
|
out.append(text)
|
||||||
|
out.append(("=" if len(heading.group(1)) == 1 else "-") * len(text))
|
||||||
|
continue
|
||||||
|
out.append(_inline(line))
|
||||||
|
text = "\n".join(out)
|
||||||
|
text = re.sub(r"\n{3,}", "\n\n", text)
|
||||||
|
return text.strip() + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
check = "--check" in sys.argv
|
||||||
|
stale: list[str] = []
|
||||||
|
for md_name, txt_name in PAIRS:
|
||||||
|
source = ROOT / md_name
|
||||||
|
if not source.exists():
|
||||||
|
print(f"{md_name} is missing - nothing to render")
|
||||||
|
return 1
|
||||||
|
rendered = render(source.read_text(encoding="utf-8"))
|
||||||
|
target = ROOT / txt_name
|
||||||
|
if check:
|
||||||
|
current = target.read_text(encoding="utf-8") if target.exists() else ""
|
||||||
|
if current != rendered:
|
||||||
|
stale.append(txt_name)
|
||||||
|
continue
|
||||||
|
target.write_text(rendered, encoding="utf-8", newline="\n")
|
||||||
|
print(f"Wrote {target} ({len(rendered.splitlines())} lines)")
|
||||||
|
|
||||||
|
if check:
|
||||||
|
if stale:
|
||||||
|
print(f"out of sync: {', '.join(stale)}"
|
||||||
|
f" - run: python scripts/plaintext_twins.py")
|
||||||
|
return 1
|
||||||
|
print(f"in sync: {', '.join(t for _, t in PAIRS)}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@ -1,76 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
"""Render README.md to README.txt with the markdown markup removed.
|
|
||||||
|
|
||||||
README.txt exists for terminals, pagers and anywhere markdown doesn't
|
|
||||||
render. It is generated - never edit it by hand:
|
|
||||||
|
|
||||||
python scripts/readme_txt.py # rewrite README.txt
|
|
||||||
python scripts/readme_txt.py --check # exit 1 if it is out of sync
|
|
||||||
|
|
||||||
The test suite runs --check, so a README.md edit that forgets to
|
|
||||||
regenerate fails CI rather than shipping a stale mirror.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parent.parent
|
|
||||||
|
|
||||||
|
|
||||||
def _inline(text: str) -> str:
|
|
||||||
text = re.sub(r"!\[([^\]]*)\]\([^)]*\)", r"\1", text) # images -> alt text
|
|
||||||
text = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r"\1 (\2)", text) # links -> text (url)
|
|
||||||
text = re.sub(r"\*\*([^*]+)\*\*", r"\1", text) # bold
|
|
||||||
text = re.sub(r"(?<!\*)\*([^*\n]+)\*(?!\*)", r"\1", text) # italic
|
|
||||||
text = re.sub(r"`([^`]+)`", r"\1", text) # inline code
|
|
||||||
return text
|
|
||||||
|
|
||||||
|
|
||||||
def render(md: str) -> str:
|
|
||||||
out: list[str] = []
|
|
||||||
in_fence = False
|
|
||||||
for line in md.splitlines():
|
|
||||||
if line.lstrip().startswith("```"):
|
|
||||||
# Drop the fence markers; the code itself stays, indented so it
|
|
||||||
# still reads as a block without the backticks.
|
|
||||||
in_fence = not in_fence
|
|
||||||
continue
|
|
||||||
if in_fence:
|
|
||||||
out.append((" " + line) if line else "")
|
|
||||||
continue
|
|
||||||
heading = re.match(r"^(#{1,6})\s+(.*)$", line)
|
|
||||||
if heading:
|
|
||||||
text = _inline(heading.group(2))
|
|
||||||
out.append(text)
|
|
||||||
out.append(("=" if len(heading.group(1)) == 1 else "-") * len(text))
|
|
||||||
continue
|
|
||||||
out.append(_inline(line))
|
|
||||||
text = "\n".join(out)
|
|
||||||
text = re.sub(r"\n{3,}", "\n\n", text)
|
|
||||||
return text.strip() + "\n"
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
source = (ROOT / "README.md").read_text(encoding="utf-8")
|
|
||||||
rendered = render(source)
|
|
||||||
target = ROOT / "README.txt"
|
|
||||||
if "--check" in sys.argv:
|
|
||||||
current = target.read_text(encoding="utf-8") if target.exists() else ""
|
|
||||||
if current != rendered:
|
|
||||||
print("README.txt is out of sync - run: python scripts/readme_txt.py")
|
|
||||||
return 1
|
|
||||||
print("README.txt is in sync")
|
|
||||||
return 0
|
|
||||||
target.write_text(rendered, encoding="utf-8", newline="\n")
|
|
||||||
print(f"Wrote {target} ({len(rendered.splitlines())} lines)")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(main())
|
|
||||||
68
tests/PlainTextTwins.Tests.ps1
Normal file
68
tests/PlainTextTwins.Tests.ps1
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# The .txt twins are generated, and this is what makes that true.
|
||||||
|
#
|
||||||
|
# Invoke-Pester .\tests
|
||||||
|
#
|
||||||
|
# scripts/plaintext_twins.py has always shipped a --check mode, and its
|
||||||
|
# docstring claimed "the test suite runs --check". Nothing ran it. So README.txt
|
||||||
|
# could drift from README.md silently, and CHANGELOG.txt - which no generator
|
||||||
|
# covered at all - actually did.
|
||||||
|
#
|
||||||
|
# A twin that disagrees with the file it mirrors is worse than no twin: it is a
|
||||||
|
# second document that looks authoritative and is wrong.
|
||||||
|
|
||||||
|
# -Skip is evaluated during DISCOVERY, before BeforeAll runs, so a python
|
||||||
|
# lookup done in BeforeAll leaves the flag $null and the real check silently
|
||||||
|
# skips - which is how this test first "passed" while verifying nothing.
|
||||||
|
BeforeDiscovery {
|
||||||
|
$script:Python = $null
|
||||||
|
foreach ($candidate in @('python', 'python3', 'py')) {
|
||||||
|
$cmd = Get-Command $candidate -ErrorAction SilentlyContinue
|
||||||
|
if ($cmd) { $script:Python = $cmd.Source; break }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BeforeAll {
|
||||||
|
$script:RepoRoot = Split-Path -Parent $PSScriptRoot
|
||||||
|
$script:Generator = Join-Path $script:RepoRoot "scripts\plaintext_twins.py"
|
||||||
|
|
||||||
|
$script:Python = $null
|
||||||
|
foreach ($candidate in @('python', 'python3', 'py')) {
|
||||||
|
$cmd = Get-Command $candidate -ErrorAction SilentlyContinue
|
||||||
|
if ($cmd) { $script:Python = $cmd.Source; break }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Describe "plain-text twins" {
|
||||||
|
|
||||||
|
It "the generator is where the tests and the docs say it is" {
|
||||||
|
Test-Path -LiteralPath $script:Generator | Should -BeTrue
|
||||||
|
}
|
||||||
|
|
||||||
|
It "every .md that owes a twin has one" {
|
||||||
|
$pairs = Select-String -Path $script:Generator -Pattern '^\s*\("([^"]+\.md)",\s*"([^"]+\.txt)"\),' |
|
||||||
|
ForEach-Object { [pscustomobject]@{ Md = $_.Matches[0].Groups[1].Value; Txt = $_.Matches[0].Groups[2].Value } }
|
||||||
|
|
||||||
|
$pairs.Count | Should -BeGreaterThan 0 -Because "PAIRS in plaintext_twins.py is what this suite checks"
|
||||||
|
foreach ($p in $pairs) {
|
||||||
|
Test-Path -LiteralPath (Join-Path $script:RepoRoot $p.Md) | Should -BeTrue -Because "$($p.Md) is listed in PAIRS"
|
||||||
|
Test-Path -LiteralPath (Join-Path $script:RepoRoot $p.Txt) | Should -BeTrue -Because "$($p.Md) owes a twin at $($p.Txt)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
It "every twin is in sync with its markdown" -Skip:(-not $script:Python) {
|
||||||
|
Push-Location $script:RepoRoot
|
||||||
|
try {
|
||||||
|
$output = & $script:Python $script:Generator --check 2>&1
|
||||||
|
$code = $LASTEXITCODE
|
||||||
|
}
|
||||||
|
finally { Pop-Location }
|
||||||
|
|
||||||
|
$code | Should -Be 0 -Because ($output -join "`n")
|
||||||
|
}
|
||||||
|
|
||||||
|
It "reports the python that was missing rather than passing quietly" -Skip:([bool]$script:Python) {
|
||||||
|
# Not a pass. If this is the test you are reading, --check never ran:
|
||||||
|
# install python, or regenerate the twins by hand before shipping.
|
||||||
|
Set-ItResult -Inconclusive -Because "no python on PATH, so the twins were not verified"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user