Commit Graph

38 Commits

Author SHA1 Message Date
KellyMichels
d64cfe5f14 chore: refresh CHECKSUMS.txt for the lowercase sweep
Every script's header changed, so every hash did. The repo's own
Checksums test caught it -- which is what it is for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 13:32:23 -05:00
86938d0d80
chore: publish the zec2 and zec2online comment updates to the mirror (#87)
Some checks are pending
tests / test (push) Waiting to run
* chore: publish the zec2 and zec2online comment updates

Mirror drift, not new behaviour: the private copies had their comments
reworded and the public ones had not caught up.

Two things the new wording carries that the old did not:

zec2 now records WHY it grew an ssh of its own. The container-side version
read referenced three variables the script never defined, and because that
read sits inside a try/catch the failure was silent - it fell through to the
HTTP call and reported nothing once that endpoint stopped being public. A
missing variable and an unreachable service looked identical from the
outside, which is the kind of thing worth writing down next to the fix.

Both files now describe the container-side read by what it is - an endpoint
that is not public on every project - rather than by a product's own
wording, which is what keeps this mirror publishable.

Published with zpublish_zscripts; CHECKSUMS.txt refreshed by the same run
and committed with them, as that script requires.

Tests: 286 passed, 1 skipped across the suite; checksums, plain-text twins
and sanitization re-run after the changelog edit - 70 passed.

README.txt was left out deliberately. Regenerating the twins rewrote it with
LF where the repo stores CRLF, and the content is byte-identical - a no-op
that would only have added noise to this diff.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: rehash zec2 and zec2online against their checked-out form

CI failed the manifest check on both files. The hashes in CHECKSUMS.txt were
computed from the copies zpublish_zscripts had just written, which land with
LF; .gitattributes pins *.ps1 to eol=crlf, so every checkout - CI's included
- materialises them with CRLF and hashes differently.

Local runs passed because the working tree had already been normalised by a
later git operation. Only CI, checking out clean, saw the mismatch. The new
hashes are byte-for-byte the "But was:" values from the failing run.

Nothing about the scripts changed; this is the manifest catching up with the
form the files actually take on disk after checkout.

Worth noting where the sharp edge is: the manifest is generated from the
working tree, so any tool that writes a pinned file and hashes it in the same
breath records a hash the repo will never reproduce. Re-materialising from
git before hashing is what makes the two agree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 20:47:18 -05:00
1c7d1d639b
chore(release): v1.0.0.0.27 (#86)
Build stamp catch-up for 2 merged PR(s) since v1.0.0.0.26:
  c317b13 docs(readme): give the download-and-verify path a PowerShell form (#85)
  d4cfa98 feat(site): track the public page this project serves (#84)
2026-09-15 19:59:46 -05:00
05b771e64a
chore(release): v1.0.0.0.26 (#83)
Build stamp catch-up for 1 merged PR(s) since v1.0.0.0.25:
  f2e6302 fix(zdeploy): build docker stacks that come from a Dockerfile, instead of restarting the old image (#81)
2026-09-14 14:16:58 -05:00
f2e6302d00
fix(zdeploy): build docker stacks that come from a Dockerfile, instead of restarting the old image (#81)
* fix(zdeploy): build docker stacks that come from a Dockerfile

Mirrors the fix in the private scripts repo; the code is identical in both, only
the config differs.

The docker kind ran `docker compose pull` then `docker compose up -d`. That is
right for a stack of published images and wrong for one built from a Dockerfile
in the tree, where there is nothing to pull. `up -d` builds only when the image
is MISSING, so the first deploy works and every one after it uploads the new
code, starts the old image, and reports success.

A project opts into building with deploy.build, which runs
`docker compose build --pull` so the base image is refreshed at the same time.
Stacks that pull are unaffected.

The example config documents the flag on the docker project, next to the
existing note about startApp, because the failure is silent and nobody goes
looking for a setting they do not know exists.

CHECKSUMS.txt regenerated, since two covered scripts changed.

286 tests pass, 1 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(checksums): hash the scripts as git checks them out, not as a tool wrote them

CI failed on the two files this branch touches while the same suite passed here.
The manifest was right about the wrong bytes.

.gitattributes pins *.ps1 to eol=crlf, and its comment says why: it makes these
files byte-identical on every platform, which is what lets CHECKSUMS.txt hold
one hash per file rather than one per OS. The edit that added Get-DockerImageStep
was applied by a script that wrote LF, so the working copy stopped matching the
pin. zchecksums then faithfully recorded the LF hashes, and every checkout that
honours .gitattributes - including CI - disagreed.

Nothing was wrong with the committed content: git normalises on the way in, so
the objects were always correct. Only the local working copy and the manifest
taken from it were off.

Re-materialised both files through git so they carry the endings the attribute
pins, then regenerated the manifest from those.

286 tests pass, 1 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:32:20 -05:00
0e7b80b4ae
chore(release): v1.0.0.0.25 (#80)
Some checks failed
tests / test (push) Has been cancelled
Build stamp catch-up for 6 merged PR(s) since v1.0.0.0.24:
  732a448 feat: add zmerge and zpull — fleet-wide PR merging and checkout sync (#79)
  573e010 docs(security): security notes, and a twin for every root .md (#78)
  3b0194a perf(tests): run each child-process invocation once (#77)
  f27f9dc fix(deploy): read the string build stamp, and never compare two unreadable labels (#76)
  16c56dc fix(ci): push trigger names master, this repo's default branch, so the workflow runs after a merge (#75)
  32e8d74 chore(ci): add a GitHub Actions workflow that lints with PSScriptAnalyzer and runs the Pester suite on Windows (#74)
2026-09-12 16:05:20 -05:00
732a448be5
feat: add zmerge and zpull — fleet-wide PR merging and checkout sync (#79)
* feat: add zmerge and zpull

Two commands that were private-only until now. They turned out to be useful
beyond the fleet they were written for, so they are manifested for publication
and removed from the sanitization denylist's private-only list.

  zmerge   merge every pull request across the org that is genuinely ready -
           MERGEABLE/CLEAN and 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 at it.

WHY THEY COULD BE PUBLISHED NOW. zmerge carried a hardcoded list of sixteen
repository names, which was both the reason it could not be published and a
bug: the org has thirty active repositories, so it scanned about half and
reported "Nothing open to merge" while a ready pull request sat in one it had
never heard of. It asks GitHub now, and the names went with the list.

Get-FleetRepos throws rather than returning an empty list when gh fails,
because a tool that quietly scans nothing prints the same reassuring line as
one that scanned everything and found nothing.

-e is an alias for -Execute on both, the way -s already works for -Scan.

Also: __pycache__/ is gitignored. scripts/plaintext_twins.py creates it on
every run and it was showing up as untracked work.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: CRLF the new scripts, as .gitattributes pins them

zmerge.ps1, zpull.ps1 and zpull.cmd went in with LF endings. .gitattributes
pins *.ps1 and *.cmd to eol=crlf precisely so CHECKSUMS.txt can hold one hash
per file rather than one per platform - so git handed CI a CRLF checkout while
the manifest carried hashes taken from my LF copies, and the three new files
were the only ones that failed.

Local verification passed and CI did not, which is the tell: the manifest was
generated against bytes that only existed on this machine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 15:52:18 -05:00
f27f9dc5bc
fix(deploy): read the string build stamp, and never compare two unreadable labels (#76)
Get-LabelFromBuildJsonObj knew two stamp shapes and not the third - the string
form the versioning scheme specifies and zbump writes. It fell through to the
legacy branch, where productVersion is null ("") and buildNumber is null (0),
so every string stamp became "v.0".

Deploy verification runs BOTH sides through this function: the local stamp and
the one read back from the running container. So it did not fail loudly - it
collapsed both to "v.0", compared them equal, and printed PASS:

  local 'v1.0.0.0.0' -> 'v.0'   remote 'v9.9.9.9.9' -> 'v.0'   equal? True

A check that cannot fail is worse than no check, because it is believed. It
would report PASS against a container serving a build from weeks ago, which is
the exact case it exists to catch.

- The string form is read first: a stamp that states its version means it,
  even if it also carries stray numeric fields from a half-migration. A
  missing leading v is tolerated so all three shapes stay comparable.
- The legacy branch returns $null when there is nothing to build a label from,
  so a caller sees "no label" instead of a label matching every other
  unreadable stamp.
- zdeploy refuses to verify an unreadable local label, and never treats a null
  remote label as a match - otherwise $null -eq $null restores the same
  vacuous pass one level up.

CHECKSUMS.txt regenerated, since both covered files changed.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 13:47:25 -05:00
fc52501999
chore(release): v1.0.0.0.24 (#73)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 13:44:23 -05:00
4ebb596176
chore(mirror): mirror tagOnDeploy and the zstart gitPull fix, and stop publishing current product names (#72)
The mirror carries the tagOnDeploy feature, its tests, and the zstart
gitPull fix from the private tree. Twelve published references to
current product names and internal issue numbers are reworded
generically, the denylist learns the current spellings (a dot or hyphen
broke the word, an underscore hid the boundary), and planted cases prove
the suite now sees them. CHECKSUMS.txt regenerated.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 13:40:45 -05:00
c968517aef
chore(checksums): manifest catches up to zdeploy.ps1 after the -Scan merge (#70) (#71)
Regenerated in a clean worktree of master, not in the working checkout: that
checkout carries unrelated uncommitted work, and zchecksums hashes files on
disk, so an -Update run there would have recorded hashes of files that are not
committed. Here the only inputs are what master holds.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 18:03:24 -05:00
f1b1fd6264
chore(release): v1.0.0.0.23 (#69)
Build stamp catch-up for 1 merged PR(s) since v1.0.0.0.22:
  135c585 docs(changelog): give every shipped release its own section, and generate the twin (#68)
2026-08-31 18:39:46 -05:00
560a2064a2
chore(release): v1.0.0.0.22 (#67)
Build stamp catch-up for 1 merged PR(s) since v1.0.0.0.21:
  40fa250 refactor(tests): move the sanitization denylist to a data file both sides can read (#66)
2026-08-31 17:56:24 -05:00
af929f73ba
chore(release): v1.0.0.0.21 (#65)
Build stamp catch-up for 1 merged PR(s) since v1.0.0.0.20:
  ac95c47 chore(sync): bring zdeploy/ZHelpers up to the private tree, sanitized (#64)
2026-08-31 15:01:27 -05:00
ac95c47255
chore(sync): bring zdeploy/ZHelpers up to the private tree, sanitized (#64)
The mirror's deploy pair had drifted ~280 lines behind: it lacked the
transactional .env preserve/restore (an interrupted deploy could destroy
server-side env files), the stderr-flattening step wrapper (a successful
deploy reported failure and skipped its own verification), and the
verification rework (channel re-picked every retry, edge only with a Host
to route by, verify.timeoutSeconds, honest split of "stale build" vs "no
channel answered").

The sync is byte-faithful to the private tree except where the mirror's
own Sanitization suite demands otherwise - and it caught the first copy:
three failures for private project names, a private domain, and a
private-only script name that rode along in comments. Each war story keeps
its lesson and loses its cast, per the convention already in the file
("EvoCivilCode: deploy/" was already published as "(deploy/, infra/,
...)"). That suite going red on an unsanitized copy is exactly what it
exists for.

Also in this change:

- tests/VerifyPlan.Tests.ps1 - the channel-selection rules are pure
  functions and Pester pins them (no domain => no edge attempt; the PS 5.1
  one-element-unroll trap). First verification tests in the mirror.
- README: the verify block now documents viaProxy/upstream (they shipped
  in the docker-network read but were never in the README),
  timeoutSeconds, and the channel order with why it re-resolves per retry.
- README.txt: generated plain-text twin, via scripts/readme_txt.py
  (vendored from the fleet's reference implementation; the file is
  generated, never edited by hand).
- CHANGELOG.md/.txt: entries merged into the existing Unreleased sections.

CHECKSUMS.txt refreshed (42 entries). Pester: 240 passed, 0 failed.
Both synced files parse clean.

Observed, untouched: the Unreleased section carries duplicate "### Fixed"
headings from earlier appends; folding them risks reordering entries whose
prose references their neighbours, so it is left for the next release cut
(zbump #110 rolls Unreleased into the version being cut).
2026-08-31 14:44:26 -05:00
471789f530
chore(release): v1.0.0.0.20 (#62)
Build stamp catch-up for 1 merged PR(s) since v1.0.0.0.19:
  e4c563d fix(zversion): help text says one bump per release, not one per PR (#61)
2026-08-30 21:06:11 -05:00
e4c563d1d5
fix(zversion): help text says one bump per release, not one per PR (#61)
zversion's usage block and its bump help line both said 'one per PR, one per
defect fix'. The build counter advances once per release: the number names
something that shipped, so a release carrying five PRs moves it by one, and
PRs that never shipped on their own were never separate builds.

This is help text rather than behaviour, but it is the wording that gets
followed - it is what stamped a single evo.www release as two builds. The
matching comments in ZHelpers.ps1 and zdeploy.ps1 are corrected with it.

CHECKSUMS.txt regenerated for the three edited scripts, since the manifest
tests fail the moment it drifts. CHANGELOG entry added under Unreleased, with
its .txt twin. The older CHANGELOG entry recording what the rule was when
zversion shipped is deliberately left as written - a changelog describes what
happened, not what is currently true.

Pester: 231 passed, 0 failed.
2026-08-30 20:08:26 -05:00
c32fa8537b
chore(release): v1.0.0.0.19 (#60)
Build stamp catch-up for 1 merged PR(s) since v1.0.0.0.18:
  e738b62 feat(version): read a live build from inside the docker network, not the public proxy (#59)
2026-08-30 15:48:25 -05:00
e738b62cdf
feat(version): read a live build from inside the docker network, not the public proxy (#59)
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: a project without those two keys behaves
exactly as before.

zec2 gains $PemKey and $SshTarget, which it had no need of until now -- the
read is inside a try/catch, so without them it would throw, be swallowed,
and fall through silently.

CHECKSUMS.txt regenerated (zchecksums -Update), zconfig.example.json
documents both shapes of the verify block, and CHANGELOG.md carries its
plain-text twin.

Pester: 231 passed, 0 failed -- including the sanitization suite.
2026-08-30 15:44:42 -05:00
58c888aa56
chore(release): v1.0.0.0.18 (#58)
Build stamp catch-up for 4 merged PR(s) since v1.0.0.0.14:
  4230566 feat: two blank lines after every z-script run (#57)
  432f210 fix(zdeploy): the pre-zip line states the label verification will require (#56)
  fcbad44 feat(zdeploy): add the static deploy kind, and a test that keeps this repo sanitized (#55)
  7b32bfa fix(zdeploy): docker kind now ships config subdirectories (#54)
2026-08-25 15:51:58 -05:00
4230566e47
feat: two blank lines after every z-script run (#57)
zdeploy ended with two blank lines and nothing else did, so its output
was the only one that did not butt up against the next prompt. Applied
everywhere.

Implemented in Stop-ZTracking rather than per script, because every
tracked script ends by calling it - including the usage and guard paths
that do `Stop-ZTracking; exit 1`. One place therefore covers every exit
of sixteen scripts.

  * Write-ZTrailer emits the two lines. Stop-ZTracking calls it on both
    paths, including the early return when tracking never started - a
    script that printed output still deserves the separation.
  * -FinalNote prints one last line AFTER the tracking footer and BEFORE
    the blanks. zdeploy's "Last deployed at ..." now goes through it, so
    it stays the last thing on screen instead of being followed by the
    token count.
  * The standalone tools (zchecksums, zversion, zrelease) get a local
    three-line copy at each exit rather than dot-sourcing ZHelpers -
    they are deliberately dependency-free so they run from an extracted
    release zip with nothing beside them.
  * Redundant trailing blanks were removed where a script already
    printed one before exiting, so the count is exactly two, not three.

Also fixes a related gap found while testing: the error exits inside
Get-ZConfig and Get-ZProject bypassed Stop-ZTracking entirely, so a run
that died on a missing zconfig.json printed no trailer AND no token
footer. Those three exits now route through Stop-ZTracking.

zkill/zrestart are one-line wrappers around scripts that already trail,
so they are untouched - adding it would double the blanks.

Verified by running each script and counting the trailing blank lines in
its captured output: 2 on every success path, every usage path, and the
config-error path. Suite 231/231. CHECKSUMS.txt refreshed.
2026-08-25 15:37:45 -05:00
432f2109e1
fix(zdeploy): the pre-zip line states the label verification will require (#56)
Mirrors evo.scripts#79. Invoke-ViteDeploy announced "(server-side build will
bump +1)" and then Wait-VerifyStaticBuild asked for the committed stamp
unchanged, so the operator was told to expect one label and watched a check
pass on another.

The behavior was already correct; the message predates the removal of the
prebuild hook that used to self-bump inside the image. Wait-VerifyStaticBuild
documents it: "Expect the COMMITTED stamp, not +1: builds no longer
self-bump ... so 'is the build I just packed live?' means an exact match."

CHECKSUMS.txt regenerated, since zdeploy.ps1 changed.
2026-08-21 22:38:26 -05:00
fcbad44e0e
feat(zdeploy): add the static deploy kind, and a test that keeps this repo sanitized (#55)
Two things, both prompted by the same incident.

STATIC KIND
Plain static sites - no build, no container of their own; a shared web
container serves them off disk, so shipping the files IS the deploy.
Directories are staged to a sibling and swapped in with mv rather than
copied in place, because a large media file uploaded in place is served
half-written to anyone who requests it mid-copy. The swap is a rename,
so the switch is atomic.

Skips $JunkDirNames + .github + deploy.skipDirs, matching the docker
kind rather than inventing a third convention.

SANITIZATION TEST
This repo is public and must stay standalone, but the toolkit is
developed in a private checkout and copied here. Twice in three days a
wholesale copy landed carrying real project names, internal hostnames,
host disk figures, and references to scripts that exist only in the
private copy. Both times a human reading the diff caught it - the
control that fails exactly when a diff is 280 lines of good work with
three bad words buried in it.

So it is a test now. Seven rules: private project names, private product
domains, private-only script names, local drive paths, the operator home
path, the real key filename, and any IPv4 outside RFC 5737 documentation
space and the private ranges.

Two anti-vacuity guards, because a denylist that silently matches
nothing is worse than no denylist: one asserts the file scan is
non-empty, and one plants a known violation and requires the pattern to
find it.

The IP rule bounds on [\d.] rather than \d deliberately - this toolkit's
own 5-segment version (v1.0.0.0.14) contains "0.0.0.14", which a plain
digit boundary reads as an address.

Verified against the real unsanitized copy that slipped through: 3 of
the 7 rules fire, naming file and line.

Tests: 231/231 (222 + 9 new). CHECKSUMS.txt refreshed.
2026-08-21 21:43:40 -05:00
7b32bfada8
fix(zdeploy): docker kind now ships config subdirectories (#54)
Invoke-DockerDeploy uploaded top-level files ONLY. For any stack that
keeps configuration in a directory, that is silently wrong in the worst
way: the compose file arrives, the containers restart, and the config
they read is whatever was already on the box. The deploy reports
success while shipping nothing that matters.

The sharp case is a provisioning directory read at container start -
alert rules, datasources, mounted config - where the restart makes it
look like the change was applied.

Same fix the edge kind already got: subdirectories ship recursively.
Skipped are $JunkDirNames, plus .github (CI config belongs in the repo,
never on a deploy target - it is not in $JunkDirNames because backups
DO want it), plus anything the project lists in the new
deploy.skipDirs.

deploy.skipDirs exists for SERVER-SIDE STATE that shares the tree: a
data/ holding mailboxes or a time-series database must never be
overwritten by whatever the local checkout has - usually nothing, which
is the dangerous case, since scp -r of an absent directory is not the
no-op you want to rely on.

CHECKSUMS.txt refreshed alongside. Tests: 222/222.
2026-08-21 21:29:53 -05:00
KellyMichels
77d7c2f1f5 chore(release): v1.0.0.0.14
Build stamp catch-up for 3 merged PR(s) since v1.0.0.0.11:
  6c30f40 feat(zdeploy): sync deploy hardening from the private toolkit (#53)
  8959d9f fix(zdeploy): stop passing ssh -n to scp, which rejects it (#52)
  d99f8a1 fix(zdeploy): stop mirroring the build stamp into the local checkout (#51)
2026-08-20 11:45:19 -05:00
6c30f400ad
feat(zdeploy): sync deploy hardening from the private toolkit (#53)
Nine fixes that had accumulated only in the private copy. Each one is a
production failure that already happened:

  * ssh -n on the deploy path. Without it ssh reads its stdin, inherits
    the console handle under PowerShell, and can block forever. The
    existing timeouts cannot catch it - they bound a connection that is
    dying, and this one was never established. Get-Ec2ScpOpts derives
    from the same list minus -n, because scp rejects it with a usage
    error that reads like an unrelated failure.

  * Invoke-DeployGitPull now switches TO the default branch instead of
    pulling whatever is checked out. Pulling the current branch breaks
    as soon as the remote deletes branches on merge, and worse, could
    ship a feature branch to production. Refuses over local changes,
    excluding the files the deploy itself stamps.

  * nextjs handler resolves composeDir. It ran compose against
    remote.path, so a project whose compose lives in a subdirectory
    recycled whatever docker-compose.yml sat at the project root -
    usually the local dev one shipped in the same archive. Two deploys
    in a row exited 0 having shipped nothing, and verification passed
    because the untouched old container still answered.

  * BUILD BEFORE DOWN. The old order took the site offline for the whole
    build and left it offline if the build failed - one deploy served
    502 for 19 hours with no container running. The old image now keeps
    serving until the new one is ready.

  * compose build takes the named service, so a compose file that does
    not define it fails loudly instead of succeeding with nothing to do.

  * deploy.verifyHost overrides domain for verification only, for when a
    domain is retired ahead of its replacement.

  * deploy.stampCmd writes the build version from git before zipping,
    then reverts the tree so the stamp cannot block the next deploy.

  * Optional ztokens pseudo-project and passive usage records. Both
    degrade to a printed skip when no sibling ztokens checkout exists.

Sanitized on the way across, per the public-repo rule: the war stories
that make these comments worth reading are kept, the private product
names, domains, internal script names, outage dates and host figures are
not. Also dropped a "See issue #28" pointer that resolves to an
unrelated PR in this repo, and two references to scripts that exist only
in the private toolkit.

CHECKSUMS.txt refreshed alongside, per the manifest rule.

Tests: 222/222 (the 4 failures before the refresh were the checksum
suite correctly flagging both edited files).
2026-08-20 11:44:42 -05:00
KellyMichels
91bbaca0e7 chore(release): v1.0.0.0.11
Build stamp catch-up for 1 merged PR(s) since v1.0.0.0.10:
  a1dd642 chore(changelog): drop the product name from the versioning entry; add the plain-text twin (#50)
2026-08-14 12:47:12 -05:00
KellyMichels
bc86162170 chore(release): v1.0.0.0.10
Build stamp catch-up for 1 merged PR(s) since v1.0.0.0.9:
  eedb95b fix(zdeploy): sync via explicit fetch + ff-only merge, not git pull (#49)
2026-08-13 19:53:00 -05:00
eedb95bbf1
fix(zdeploy): sync via explicit fetch + ff-only merge, not git pull (#49)
With deploy.gitPull set, Invoke-DeployGitPull ran `git pull --ff-only`.
Pull merges whatever .git/FETCH_HEAD marks "for merge", and a
concurrent fetch in the same repo - an editor's background auto-fetch
racing the deploy - can leave duplicate for-merge lines. Pull then dies
with "Cannot fast-forward to multiple branches" even when both lines
name the same commit. Reproduced live on 2026-08-13: the deploy failed
at step 0, and by inspection time the same pull succeeded, with the
duplicated FETCH_HEAD entries as the only evidence.

Now the function fetches explicitly and fast-forwards against the
remote-tracking ref:

  git fetch origin
  git merge --ff-only origin/$branch

Identical fast-forward-or-refuse semantics, but nothing shared and
mutable in the path. A failed fetch and a failed merge now also throw
distinct messages, so the operator knows which half broke.

CHECKSUMS.txt refreshed alongside, per the manifest rule.

Tested: PS 5.1 parse clean; full Pester suite 222/222 after the
manifest refresh (the 3 pre-refresh failures were the checksum suite
correctly flagging the edited file). The same fix pattern was
exercised against live and scratch repos for the private twin
(evomedia-net/evo.scripts#58): behind -> fast-forwards, diverged ->
refuses and aborts the deploy.

Closes #48
2026-08-13 19:52:19 -05:00
KellyMichels
a2fafcc6d0 chore(release): v1.0.0.0.9
Reconcile to the version build-version.json already committed. No increment.
2026-08-13 18:36:09 -05:00
KellyMichels
5701dff60a chore(release): v1.0.0.0.8
Build stamp catch-up for 8 merged PR(s) since v1.0.0.0.0:
  6c03138 fix(zdeploy): deploy edge first for any project list, not just 'all' (#46)
  976eb6d chore: point repo URLs at evomedia-net/evo.* (#44)
  0e4d9c3 fix(zdeploy): ship edge asset subdirectories, stop deploys hanging on ssh prompts, keep vendored archives (#43)
  c8fcfee fix(zdeploy): verify Next.js deploys on the server instead of probing the public IP (stops the bogus security-group warning) (#41)
  b84d24b fix(zdeploy): report where a domain-less project deployed to (#40)
  4668404 fix(zdeploy): re-land the two blank lines after the timestamp (#38)
  a868973 fix(zdeploy): verify against the committed build stamp, and read 5-segment versions (#37)
  0ddffc1 feat(zdeploy): print 'Last deployed at' timestamp as the last line of a run (#36)
2026-08-12 16:29:36 -05:00
6c03138a2e
fix(zdeploy): deploy edge first for any project list, not just 'all' (#46)
Closes #45.

'zdeploy evo edge' deployed evo first, then edge. The edge-first sort was
nested inside the 'all' branch, so an explicit list walked in whatever order
was typed.

That ordering is a correctness property, not a convenience of 'all': the proxy
has to route before the apps behind it ship, or there is a window where a new
app is live behind stale routing. 'zdeploy evo edge' reads as 'these two, edge
included' and quietly did the risky order.

Hoisted the sort out of the branch so it applies however the list was produced.
Order within each group is preserved, so an intentional sequence still holds -
and when the order does change, it now says so rather than reordering silently.

Verified across six shapes: evo+edge reorders and reports it; edge+evo is left
alone with no note; edge last in a longer list moves to front keeping the rest
in order; single project and no-edge lists are untouched.
2026-08-12 15:45:51 -05:00
kellymichels
0e4d9c3cca
fix(zdeploy): ship edge asset subdirectories, stop deploys hanging on ssh prompts, keep vendored archives (#43)
* fix(zdeploy): edge deploy ships asset subdirectories, not just top-level files

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, so fonts fell back silently and vendored JS
never loaded. Every subdirectory except nginx-logs/ and .git/ now ships
recursively, and the ensure-dir chown is recursive so scp into
docker-created root-owned dirs cannot fail.

Closes #42

* fix(zdeploy): stop deploys hanging on ssh prompts, keep vendored archives, make unzip install idempotent

- Get-Ec2SshOpts: every deploy-path ssh/scp now carries BatchMode=yes
  plus connect/keepalive timeouts. Without BatchMode ssh prompts and
  waits forever, and because the deploy pipes stderr through the
  pipeline the prompt never reaches the screen - the run just stops
  under the last step label with no explanation.
- Archive filter exempts files under vendor/: a vendored *.tgz is a
  build input, and dropping it fails a Dockerfile COPY at image build.
- unzip install checks command -v first instead of an apt round-trip
  on every deploy.

Ported from the private script; three tests cover the vendor/ exemption.
2026-08-06 23:29:33 -05:00
kellymichels
c8fcfee76c
fix(zdeploy): verify Next.js deploys on the server instead of probing the public IP (stops the bogus security-group warning) (#41)
* fix(zdeploy): verify Next.js deploys on the server, not the public IP

The nextjs handler verified by requesting http://<ec2-ip>:<prod-port>/.
A compose stack behind the edge proxy normally publishes to 127.0.0.1
only, so that request can never be answered and every deploy ended with
"is the port open in the security group?" — pointing at a firewall rule
for an app that was already serving fine. No security-group change could
have made that probe succeed, which is what made the warning actively
harmful: it named the one fix guaranteed not to work, and opening the
port would have exposed the app directly, bypassing the proxy and TLS.

The nextjs handler now uses the precedence the python handler already
used: verify.port (curl localhost on the server) -> domain (Host-header
check through the edge proxy) -> an honest "NOT verified" instead of a
misleading warning.

Also follow redirects in the health check. An app whose "/" answers 307
(Next.js -> /login) returns a body of a few bytes that read as "not
ready"; -L fetches the page that actually renders. No effect on projects
that point verify.path at a plain 200 endpoint such as /health.

The example config now documents the verify block on the nextjs project,
and CHECKSUMS.txt is regenerated for the changed script.

* fix(zdeploy): truncate the health-check body in the PASS line

Pointing the nextjs handler at Test-DeployHealth means the body is now
often an HTML page, not a line of JSON, so the PASS line dumped ~10 KB
of markup into the deploy output and buried everything after it.

Match on the full body as before, but print at most 200 characters plus
the byte count. A /health endpoint still prints in full.
2026-08-06 19:01:21 -05:00
kellymichels
b84d24b3a2
fix(zdeploy): report where a domain-less project deployed to (#40)
Closes #39.

A project without a public domain finished with no indication of where it went
- the 'Site:' line was gated on $Proj.domain in all four handlers, so an
internal service deployed successfully and said nothing about the result.

The information was never missing, only unused: a domain-less project that
deploys almost certainly has a verify block (the documented pattern for stacks
not published through the edge proxy), carrying the exact port and health path
the deploy had just checked. zdeploy verified against that endpoint and then
discarded it.

New Write-DeployLocation falls back through what the project actually declares:
domain -> public URL; else verify.port (+ path) -> server-local URL flagged as
not publicly routed; else ports.prod -> same without a path; else prints
nothing, as before. Replaces the four call sites, keeping the nextjs handler's
column alignment via -Pad.

Exercised against every shape: domain, verify-only, ports.prod-only, nothing at
all, and padded. CHECKSUMS regenerated; suite 219/219.
2026-08-06 17:24:26 -05:00
kellymichels
4668404378
fix(zdeploy): re-land the two blank lines after the timestamp (#38)
These were written and pushed, but landed on the PR #36 branch AFTER that PR
had already merged, so they never reached master. Private main has them; public
did not. The two copies of zdeploy.ps1 had quietly diverged on the last line of
output.

CHECKSUMS.txt regenerated. Tail now byte-identical to the private copy.
2026-08-06 11:28:35 -05:00
kellymichels
0ddffc1020
feat(zdeploy): print 'Last deployed at' timestamp as the last line of a run (#36)
* feat(zdeploy): print 'Last deployed at' as the final line of a run

Scroll to the bottom of a deploy and you can see how long ago it happened,
without digging through the log or checking the server.

Placed after Stop-ZTracking so it is genuinely the last line, below the
token-tracking footer. Only reached on success - a failed deploy throws out of
the dispatch loop, so this never claims a deploy that did not happen. Prints
once per run rather than per project, so 'zdeploy all' ends with a single
timestamp.

24-hour clock (MM/dd/yyyy HH:mm:ss): the requested format carried no AM/PM
marker, and 24-hour is unambiguous for judging elapsed time at a glance.

CHECKSUMS.txt regenerated, since changing a covered script invalidates it.

* feat(zdeploy): use 12-hour clock with AM/PM in the timestamp

Kelly's preference: 'Last deployed at 08/03/2026 01:24:09 PM' rather than
24-hour. The AM/PM marker keeps it unambiguous.

CHECKSUMS.txt regenerated.
2026-08-03 13:26:46 -05:00
kellymichels
91b638ac31
feat: checksums, toolkit versioning (v{major}.{rc}.{beta}.{alpha}.{build}), and downloadable release zips (#35)
* feat(zchecksums): SHA-256 manifest so a download can be verified before it's run

CHECKSUMS.txt lists a SHA-256 for every top-level .ps1 and .cmd - the files a
user actually executes. zchecksums verifies them; zchecksums -Update
regenerates after an intentional edit.

The manifest is sha256sum format, so 'sha256sum -c CHECKSUMS.txt' works on
Linux/macOS/WSL as well as the PowerShell path on Windows. Hashes are identical
on every platform because .gitattributes pins .ps1/.cmd to CRLF everywhere -
that pin is now load-bearing, so it is commented as such.

Beyond changed and missing files it also reports a script that is on disk but
NOT in the manifest, so something added outside a commit still gets noticed.
Exits non-zero on any of the three.

Honest about its limits, in the header and the README: the manifest lives in
the same repo as the code, so it is an integrity check rather than a signature.
It catches a truncated clone, a forgotten local edit, or an unlisted file - not
a compromised repo.

CHECKSUMS.txt is pinned to LF: sha256sum treats a trailing CR as part of the
filename and would report every entry as missing on Linux.

tests/Checksums.Tests.ps1 keeps it from rotting - a stale manifest is worse
than none, since it either cries wolf until people ignore it or quietly stops
covering a new script. The tests assert the format, LF endings, sort order,
full coverage of on-disk scripts, current hashes, and that zchecksums itself
exits 1 on a tampered file (proved by appending a byte and restoring it).

* feat(zversion, zrelease): toolkit versioning + downloadable release zips

Implements the versioning rule (SmartPlant's 5-segment scheme, now the global
standard; currently only sp and zscripts are on it at v1.x):

    v{major}.{rc}.{beta}.{alpha}.{build}

zversion: get / bump / bump-stage / set. A stage bump zeroes every lower
segment including build. 'bump' is one per PR and one per defect fix, not per
file. Any write rewrites three things together, because they are only useful
when they agree: build-version.json (source of truth), a '# Version:' line in
all 42 script headers (a lone copied script still says which release it came
from), and CHECKSUMS.txt (stamping changes every file).

zrelease: packages the current version as releases/zscripts-<version>.zip with
a sibling .sha256, for people who want the toolkit without cloning. One hash
verifies the download; the bundled CHECKSUMS.txt verifies the extracted
contents. Refuses to overwrite an existing version's zip (released = immutable;
bump instead), and refuses to package when zchecksums fails. tests/ excluded
from the zip; releases/ never packages itself.

First release included: releases/zscripts-v1.0.0.0.0.zip (42 scripts + 7
support files) and its .sha256.

.gitattributes: releases/*.sha256 pinned LF (sha256sum treats a trailing CR as
part of the filename), releases/*.zip marked binary.

Verified end-to-end as a downloader would experience it, in WSL: sha256sum -c
on the zip passes, unzip, sha256sum -c CHECKSUMS.txt inside gives 42 OK / 0
FAILED, and the extracted zdeploy.ps1 header and build-version.json both read
v1.0.0.0.0. Double-release guard and -Verify mode exercised. Full Pester suite
219/219 (the checksum tests absorb the new files automatically).
2026-07-28 13:47:25 -05:00