Compare commits

..

21 Commits

Author SHA1 Message Date
5406c85837
chore(ci): re-run a test run once when a job in it times out (#96)
Some checks failed
tests / test (push) Has been cancelled
Kelly, 2026-10-03: a run past twice its usual time "should be verified if
it's failed and killed and/or restarted". The timeouts added in part 1
do the killing. This does the restarting, once.

A new workflow watches "tests". When a run of it ends cancelled or timed
out on its first attempt, it reads each stopped job's annotations and
re-runs the whole run only if one "exceeded the maximum execution
time". A run cancelled by hand or by a newer push is left alone, and a
second timeout stays red so a real hang reaches a person. On every other
run the job's `if` is false, so no runner starts.

Part 2 of evomedia-net/evo.testsuites#25.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 16:24:17 -05:00
5bc77f2de1
docs(zdeploy): stop naming the server's web root in a comment (#95)
Some checks failed
tests / test (push) Has been cancelled
* docs(zdeploy): describe the landing layout without naming the server's web root

A comment in the static-site deploy path named the landing container's
real web root. It now says the container serves each host from its own
directory, which is all the comment needs. Comment only; no behaviour
change. Ported by hand from evo.scripts#196, since this file is
hand-maintained in the mirror.

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

* chore(checksums): record zdeploy.ps1's new hash

The comment change in the previous commit changed the file, so
CHECKSUMS.txt is regenerated with zchecksums -Update, as the suite requires.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:34:52 -05:00
57edc629c1
fix(deploy): decode the box's output as UTF-8 so progress bars render as bars, not "Γöü" (#94)
* fix(deploy): decode the box's output as UTF-8, so progress bars are bars and not "Γöü"

Mirror of the same change in the private scripts repo: [Console]::OutputEncoding is switched to UTF-8 (no BOM) around the deploy loop and restored in a finally. PowerShell 5.1 decodes ssh's stdout with the OEM code page, where the UTF-8 bytes of a progress bar read as three characters each.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* chore(checksums): zdeploy.ps1 changed, so its entry in CHECKSUMS.txt follows

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-30 11:25:59 -05:00
73dd27c4f1
chore(release): v1.0.0.0.28 (#93)
Some checks failed
tests / test (push) Has been cancelled
Build stamp catch-up for 4 merged PR(s) since v1.0.0.0.27:
  b26be3c chore: write the site name as evomedia.net, lowercase (#92)
  22a7387 feat(site): link preview for zscripts.evomedia.net, with a card in the page's own colours (#91)
  86938d0 chore: publish the zec2 and zec2online comment updates to the mirror (#87)
  90f4301 fix: pin the .txt twins to LF so regenerating them stops reporting a change (#89)
2026-09-22 14:00:07 -05:00
b26be3cd2a
chore: write the site name as evomedia.net, lowercase (#92)
* chore: write the site name as evomedia.net, lowercase

The name is a domain and is written as one. Script headers, the README,
CHANGELOG and elevator pitch, their .txt twins, and the site page --
matching the same sweep in the private evo.scripts so the mirror does not
drift.

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

* 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>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 13:40:03 -05:00
22a7387548
feat(site): link preview for zscripts.evomedia.net, with a card in the page's own colours (#91)
Some checks failed
tests / test (push) Has been cancelled
The page had a title and a description and nothing else, so pasting the link
anywhere rendered a bare URL.

- the full og:* block with absolute URLs, plus twitter:card and a canonical the
  og:url agrees with
- site/og-card.png, 1200x630, in the page's own dark palette so the preview and
  the page look like the same thing. Composed by make_og_card.py in the private
  tooling repo; it lives in site/ because that directory is what reaches the
  server.
- tests/LinkPreview.Tests.ps1

The last test is the one that earned its place. The first draft of the card
showed `ztests`, which is not a command in this repo - so the test extracts the
z-commands named in og:image:alt and fails unless each one is a real .ps1 or
.cmd at the root. A card is public copy, and public copy must not advertise a
script that does not exist.

CHECKSUMS.txt is untouched: the manifest covers top-level executables only, and
nothing here is one.

Tested: 6 new tests, verified to fail when the card advertises ztests again.
Full suite 292 passed, 1 skipped.

Closes #90

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 17:52:27 -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
90f43010c1
fix: pin the .txt twins to LF so regenerating them stops reporting a change (#89)
Three things disagreed about line endings, and the generator lost:

  stored in git      LF
  checked out        CRLF   (core.autocrlf - nothing pinned *.txt)
  written by the     LF     (plaintext_twins.py, newline="\n")
  generator

So every run wrote LF over a CRLF checkout, git status reported each twin as
modified, and git add normalized it straight back to LF for an empty diff.

A generator that always reports work hides the one time it did some. It also
puts noise in front of the reviewer on every publish - exactly where a real
change should be easy to see - and trains the reader to discard the twins
without looking, which is how genuine drift gets thrown away.

Pinning *.txt to eol=lf makes checkout agree with both the generator and the
stored form. Verified: regenerate, and only .gitattributes is modified.

CHECKSUMS.txt keeps its own explicit line even though *.txt now covers it.
Its reason is different in kind - sha256sum -c treats a trailing CR as part
of the filename and reports every entry as missing, which is a broken
verification rather than a cosmetic diff - and that should not silently
depend on a glob above it staying where it is.

Closes #88

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 20:21:57 -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
c317b139d6
docs(readme): give the download-and-verify path a PowerShell form (#85)
"Downloading without cloning" was bash only, in the README of a PowerShell
toolkit. It is also the first thing a stranger does, so the one section aimed
squarely at newcomers was the one they could not run: `sha256sum` and `unzip`
are not Windows commands at all, and `&&` is a parse error in PowerShell 5.1
rather than a wrong result.

PowerShell goes first here, because these commands are PowerShell - Get-FileHash
against the .sha256, Expand-Archive, then zchecksums.cmd for the contents. The
bash form stays below it, matching how "Verifying what you downloaded" already
leads with zchecksums and offers sha256sum second.

Tested against releases/zscripts-v1.0.0.0.9.zip: the comparison returns True.
It also gets a note, because the output invites a wrong conclusion -
Get-FileHash prints upper case and the .sha256 file holds lower, so the two
strings look different side by side. PowerShell's -eq is case-insensitive on
strings, so the check is right and the eyes are wrong.

Twins regenerated; the Pester twin-sync test passes.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 19:44:30 -05:00
d4cfa980ed
feat(site): track the public page this project serves (#84)
Some checks are pending
tests / test (push) Waiting to run
zscripts.evomedia.net has served a page since July that existed in exactly
one place: the landing container on the box. No repo held it, so a rebuild
would not have brought it back and a change to it had no history.

Two fixes while bringing it in:

- Both GitHub buttons pointed at kellymichels/zscripts-token-savers, the
  pre-org location. It 301s rather than 404s, so it was not broken - but the
  page's primary call to action depended on a redirect that stops working
  the day that name is reused. Both now point at evomedia-net/evo.zscripts.
- robots.txt added, allowing everything. The estate's other landing-served
  pages disallow everything because they are private candidate pages; this
  one is an open-source project's front page and wants to be found.

Sanitization suite passes with the page included - it scans .html, so the
guard that keeps private identifiers out of this mirror now covers the page
as well.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 15:31:25 -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
193d6d86a1
chore(tests): stop the denylist publishing the retired name it guards (#82)
This repo is public, and the denylist that keeps private identifiers out of it
spelled two of them in full: the retired EHS product name, and its old domain.
The file protecting the name was the file publishing it.

Deleting those two rules was not an option. The private tree still carries that
name in ~20 places - sp_fix_kelly_email_prod.ps1 alone has the old domain and a
real prod stack path - so both rules are live, not stale. Dropping them would
trade a visible string for an actual leak path.

So split the literal with a one-character class instead: Smart[P]lant and
smart[p]lantehs. A class of one matches exactly that character, so the regex is
unchanged - verified by matching both spellings and the old domain before and
after, with negative controls - while the contiguous string no longer appears
in a public file.

Commented in place, because the obvious "tidy-up" is to un-split it.

Pester: tests/Sanitization.Tests.ps1 14 passed, 0 failed.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:22:20 -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
573e01021b
docs(security): security notes, and a twin for every root .md (#78)
Defers to the org policy for how to report and covers what is particular to a
repository that is a sanitised mirror: the most valuable report here is not a
crash, it is something REAL that should not be here - a credential, an internal
hostname, an operator path, an identifier naming a private project. Mail those
rather than filing an issue, because a public issue about a leaked secret
publishes it a second time.

It also says what the automated check is and is not. The sanitisation suite is
a DENYLIST: it proves the absence of known patterns, not the absence of
secrets. Green tests are why a human report is still worth sending.

And the ordinary warning for what these actually are - automation that
archives a tree, uploads it, rebuilds containers and restarts services. Read
before running, nothing here is a sandbox, the config is yours to replace.

TWINS ARE NOW DISCOVERED, NOT LISTED. PAIRS was hand-kept and two files had
outgrown it: ELEVATOR_PITCH.md and TOKEN_SAVINGS.md had no twin at all. Adding
a document and remembering to add it to a list are two acts, and the second is
the one that gets skipped.

The Pester test had the same shape in reverse - it scraped PAIRS out of the
generator's source, so it could only prove the list was self-consistent and a
document nobody listed was invisible to it. It now asks the REPOSITORY what
markdown it has. Proven by deleting SECURITY.txt and watching two tests fail.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 00:35:29 -05:00
3b0194af93
perf(tests): run each child-process invocation once (#77)
Some checks failed
tests / test (push) Has been cancelled
Every Invoke-ZScript call starts a fresh Windows PowerShell, which costs about
1.7 seconds - and it is the dominant cost of this file. Thirteen of its
forty-three calls re-run a command an earlier check has already run. The usage
tests are the clearest case: a script is run bare three separate times to
assert that it exits non-zero, that its usage lists the project keys, and that
the usage never mentions the underscore comment key. Those are three questions
about one run.

Memoised on the exact command, so each distinct invocation happens once and
every check that asks for it gets the same captured result. The commands
reached here either refuse their input or inspect an unused fixture port, so
none has a side effect a second run would reveal.

ArgumentParsing.Tests.ps1, over three runs each: 83/108/88s before,
61/68/79s after. All 42 tests still pass.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 15:12:22 -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
16c56dc3af
fix(ci): push trigger names master, this repo's default branch, so the workflow runs after a merge (#75)
Some checks are pending
tests / test (push) Waiting to run
The workflow was written with `branches: [main]` like the other two added
the same day, but evo.zscripts' default branch is master, so the push
trigger could never fire here - the PR trigger ran and passed, the merge
to master ran nothing, and the "tests" badge would have stayed at the
PR's result forever. Caught when the post-merge run was looked for and
did not exist.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 18:12:12 -05:00
32e8d7430f
chore(ci): add a GitHub Actions workflow that lints with PSScriptAnalyzer and runs the Pester suite on Windows (#74)
* chore(ci): run the Pester suite on push and pull request

This repo is one of the twelve on the fleet board and had no CI at all, so
the only thing ever running these tests was one workstation at 04:00 - and
on 2026-09-09 that run did not happen, which is how the gap surfaced.

windows-latest rather than ubuntu: Pester runs on Linux, but these scripts
deploy from a Windows workstation and the suite reads like it. Proving them
on Linux would prove something nobody runs.

Pester pinned to 5.x, since the suite uses the v5 configuration API and the
Windows image carries a v3 that would otherwise be picked first, and
-CI so a red suite fails the job - Invoke-Pester on its own exits 0.

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

* chore(ci): lint with PSScriptAnalyzer before the Pester suite

PSScriptAnalyzer is the PowerShell linter, and there is no typecheck for
PowerShell, so it is the whole of that half. Errors fail the job; warnings
are printed and do not - the repo was written without the analyzer, and a
gate that goes red on day one over style becomes a gate someone disables.
PSAvoidUsingWriteHost is excluded outright: these are command-line tools
whose Write-Host output is the interface.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 17:53:16 -05:00
84 changed files with 1980 additions and 171 deletions

14
.gitattributes vendored
View File

@ -4,8 +4,18 @@
*.ps1 text eol=crlf *.ps1 text eol=crlf
*.cmd text eol=crlf *.cmd text eol=crlf
# The checksum manifest must stay LF: `sha256sum -c` treats a trailing CR as # Every .txt here is either a generated twin or a manifest, and all of them
# part of the filename and reports every entry as missing. # want LF. plaintext_twins.py writes LF and git stores LF, but without this
# pin checkout applied core.autocrlf and handed the working tree CRLF - so
# every regeneration rewrote the file to LF, `git status` reported a change,
# and `git add` normalized it straight back to nothing (#88). Pinning the
# checkout makes all three agree.
*.txt text eol=lf
# Kept explicit even though *.txt already covers it: the checksum manifest
# must stay LF because `sha256sum -c` treats a trailing CR as part of the
# filename and reports every entry as missing. That is a broken verification,
# not a cosmetic diff, and it should not depend on a glob above it.
CHECKSUMS.txt text eol=lf CHECKSUMS.txt text eol=lf
releases/*.sha256 text eol=lf releases/*.sha256 text eol=lf

51
.github/workflows/rerun-timed-out.yml vendored Normal file
View File

@ -0,0 +1,51 @@
# Re-runs a test run once when one of its jobs ran out of time
# (evo.testsuites#25, part 2).
#
# A timeout usually means a stuck runner or a network stall rather than a
# broken test, so the first one gets a second chance. A second timeout stays
# red, so a real hang still reaches a person. A run cancelled by hand, or by a
# newer push, is left alone: only a job whose own record says it "exceeded the
# maximum execution time" counts.
#
# Costs nothing on an ordinary run. The job's `if` is false for every run that
# ended any other way, and a job skipped by its `if` never starts a runner.
name: re-run a timed-out test run
on:
workflow_run:
workflows: ["tests"]
types: [completed]
permissions:
actions: write
checks: read
jobs:
rerun:
if: >-
github.event.workflow_run.run_attempt == 1 &&
(github.event.workflow_run.conclusion == 'cancelled' ||
github.event.workflow_run.conclusion == 'timed_out')
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Re-run it if a job ran out of time
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
RUN: ${{ github.event.workflow_run.id }}
run: |
timed_out=""
for job in $(gh api "repos/$REPO/actions/runs/$RUN/jobs" \
--jq '.jobs[] | select(.conclusion == "cancelled" or .conclusion == "timed_out") | .id'); do
if gh api "repos/$REPO/check-runs/$job/annotations" --jq '.[].message' \
| grep -q "exceeded the maximum execution time"; then
timed_out="$job"
fi
done
if [ -n "$timed_out" ]; then
echo "Job $timed_out ran out of time. Re-running run $RUN once."
gh api -X POST "repos/$REPO/actions/runs/$RUN/rerun"
else
echo "Run $RUN was cancelled, but not by a timeout. Leaving it."
fi

80
.github/workflows/tests.yml vendored Normal file
View File

@ -0,0 +1,80 @@
# The Pester suite, on every push to master and every PR.
#
# This repo is one of the twelve on the fleet board
# (evomedia.net/testsuites.html) and was one of three with no CI at all, so the
# only thing ever running these tests was a workstation at 04:00. That is a
# poor place for the only copy of a check to live.
#
# windows-latest, not ubuntu, even though Pester runs on Linux: these scripts
# deploy from a Windows workstation and the suite reads like it - paths,
# executables, the shell itself. Proving them on Linux would be proving
# something nobody runs. Windows minutes bill at double, which this suite's
# size affords.
name: tests
on:
push:
branches: [master]
pull_request:
# Read-only: this job builds nothing and publishes nothing, so the default
# write-capable token is more than it needs.
permissions:
contents: read
concurrency:
group: tests-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: windows-latest
timeout-minutes: 15
steps:
# Actions pinned to a commit, not a moving tag: a tag can be repointed
# by whoever owns it, and this token, read-only though it is, still sees
# the repository.
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
# Pester pinned to 5.x: the suite is written against the v5 configuration
# API (New-PesterConfiguration), and Windows images still carry a v3 in
# the module path that would be picked ahead of it. PSScriptAnalyzer is
# the PowerShell linter; there is no typecheck for PowerShell, so the
# analyzer is the whole of that half.
- name: Install Pester 5 and PSScriptAnalyzer
shell: powershell
run: |
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
Install-Module Pester -MinimumVersion 5.5.0 -MaximumVersion 5.99.99 `
-Force -SkipPublisherCheck -Scope CurrentUser
Install-Module PSScriptAnalyzer -Force -Scope CurrentUser
Import-Module Pester -MinimumVersion 5.5.0
'Pester ' + (Get-Module Pester).Version + ', PSScriptAnalyzer ' + (Get-Module -ListAvailable PSScriptAnalyzer | Select-Object -First 1).Version
# Errors fail the job; warnings are printed and do not. The repo was
# written without the analyzer, and turning every style warning into a
# red build on day one would make the gate something to disable rather
# than something to keep. PSAvoidUsingWriteHost is excluded outright:
# these are command-line tools whose Write-Host output IS the interface.
- name: Lint (PSScriptAnalyzer)
shell: powershell
run: |
$r = Invoke-ScriptAnalyzer -Path . -Recurse -ExcludeRule PSAvoidUsingWriteHost
$warn = @($r | Where-Object Severity -eq Warning)
$err = @($r | Where-Object Severity -eq Error)
if ($warn) {
Write-Host ("{0} warning(s), not failing the build:" -f $warn.Count)
$warn | Format-Table RuleName, ScriptName, Line -AutoSize | Out-String | Write-Host
}
if ($err) {
$err | Format-Table RuleName, ScriptName, Line, Message -AutoSize -Wrap | Out-String | Write-Host
throw ("PSScriptAnalyzer: {0} error(s)" -f $err.Count)
}
Write-Host "no errors"
# -CI sets the exit code from the result, which is the whole point here:
# Invoke-Pester on its own reports failures and still exits 0, so the
# job would go green with a red suite.
- name: Tests
shell: powershell
run: Invoke-Pester -Path tests -CI

3
.gitignore vendored
View File

@ -19,3 +19,6 @@ md/
# Pester coverage output (regenerated; never committed) # Pester coverage output (regenerated; never committed)
coverage/ coverage/
# Generated by scripts/plaintext_twins.py
__pycache__/

View File

@ -1,15 +1,68 @@
<!-- <!--
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.
--> -->
# Changelog # Changelog
Notable changes to the Evomedia.net Token Savers. Notable changes to the evomedia.net Token Savers.
## Unreleased ## Unreleased
## v1.0.0.0.28 - 2026-09-22
### Changed
- **`zec2` and `zec2online` comments now say what they mean without
naming private detail.** The container-side version read is described
by what it is - an endpoint that is not public on every project - rather
than by a product's own wording, and `zec2` records why it needed its
own ssh: the read referenced three variables the script never defined,
and because it sits inside a try/catch the failure was silent and looked
exactly like a service that could not be reached.
## v1.0.0.0.26 - 2026-09-14
### Fixed
- **`zdeploy` on a docker stack built from source shipped nothing after the
first deploy.** The docker kind ran `docker compose pull` and then
`docker compose up -d`, which is right for a stack of published images and
wrong for one built from a `Dockerfile` in the tree: there is nothing to
pull, and `up -d` builds only when the image is *missing*. So the first
deploy worked and every one after it uploaded the new code, started the old
image, and reported success — worse than an error, because the deploy is
green and the container is healthy. A project now opts into building with
`"deploy": { "build": true }`, which runs `docker compose build --pull` so
the base image is refreshed at the same time. Stacks that pull are
unaffected.
## v1.0.0.0.25 - 2026-09-12
### Added
- **`zmerge`** — merge every pull request across the org that is genuinely
ready (`MERGEABLE` / `CLEAN`, not a draft), re-checking each one immediately
before and after every merge, because merging into a default branch can
conflict a sibling PR in the same repository. Dry run by default;
`-Execute` (or `-e`) merges.
- **`zpull`** — `zmerge`, then `git pull --ff-only` in every checkout the
merges affected. Skips a checkout that is dirty or is not on its default
branch rather than guessing.
### Changed
- **`-e` is an alias for `-Execute`** on both of the above, the way `-s`
already works for `-Scan`.
- **`zmerge` discovers repositories instead of listing them.** It asked a
hand-kept list, which had fallen well behind the org - so a scan covered
about half of it and reported "Nothing open to merge" while a ready pull
request sat in a repository the list had never heard of. It now asks GitHub,
and throws rather than returning an empty list if that fails: a tool that
quietly scans nothing prints the same reassuring line as one that scanned
everything, and the two must not be confusable.
## v1.0.0.0.24 - 2026-09-08 ## v1.0.0.0.24 - 2026-09-08
### Added ### Added

View File

@ -1,15 +1,75 @@
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.
Changelog Changelog
========= =========
Notable changes to the Evomedia.net Token Savers. Notable changes to the evomedia.net Token Savers.
Unreleased Unreleased
---------- ----------
v1.0.0.0.28 - 2026-09-22
------------------------
Changed
-------
- **zec2 and zec2online comments now say what they mean without
naming private detail.** The container-side version read is described
by what it is - an endpoint that is not public on every project - rather
than by a product's own wording, and zec2 records why it needed its
own ssh: the read referenced three variables the script never defined,
and because it sits inside a try/catch the failure was silent and looked
exactly like a service that could not be reached.
v1.0.0.0.26 - 2026-09-14
------------------------
Fixed
-----
- **zdeploy on a docker stack built from source shipped nothing after the
first deploy.** The docker kind ran docker compose pull and then
docker compose up -d, which is right for a stack of published images and
wrong for one built from a Dockerfile in the tree: there is nothing to
pull, and up -d builds only when the image is missing. So the first
deploy worked and every one after it uploaded the new code, started the old
image, and reported success — worse than an error, because the deploy is
green and the container is healthy. A project now opts into building with
"deploy": { "build": true }, which runs docker compose build --pull so
the base image is refreshed at the same time. Stacks that pull are
unaffected.
v1.0.0.0.25 - 2026-09-12
------------------------
Added
-----
- zmerge — merge every pull request across the org that is genuinely
ready (MERGEABLE / CLEAN, not a draft), re-checking each one immediately
before and after every merge, because merging into a default branch can
conflict a sibling PR in the same repository. Dry run by default;
-Execute (or -e) merges.
- zpull — zmerge, then git pull --ff-only in every checkout the
merges affected. Skips a checkout that is dirty or is not on its default
branch rather than guessing.
Changed
-------
- -e is an alias for -Execute on both of the above, the way -s
already works for -Scan.
- zmerge discovers repositories instead of listing them. It asked a
hand-kept list, which had fallen well behind the org - so a scan covered
about half of it and reported "Nothing open to merge" while a ready pull
request sat in a repository the list had never heard of. It now asks GitHub,
and throws rather than returning an empty list if that fails: a tool that
quietly scans nothing prints the same reassuring line as one that scanned
everything, and the two must not be confusable.
v1.0.0.0.24 - 2026-09-08 v1.0.0.0.24 - 2026-09-08
------------------------ ------------------------

View File

@ -1,42 +1,45 @@
9b3e7daf69a8295679b79072ea3e044c28f29b3ebeacdffe72c4878aaae34b08 setup_backup_schedule.ps1 6e95736007b3906950d95a830705ac1118ebfc11836d2c717d6dd49cb3157966 setup_backup_schedule.ps1
196364f1ff68608e4f7427f6b4e9e91811361eaad8d520db85075136bbe6e506 token-count.ps1 d6ab2ddb273a8ca870bd0673cffdd0907f16f4718367b4413a3e34bb1f1769c8 token-count.ps1
4037dd7fb40f91b9f7be380a771d0bdaac51e4a262e1120d38e4f53d4974e942 zbackup.cmd 30ccf43c371ae95cecd5b443d9214ac8ea71ec83be94bc15bcf359beefb8dee1 zbackup.cmd
85fd3b754a069302dc08e33101fae3beb96cd16bc505eedc1f44c1af7bdf7636 zbackup.ps1 b3549b346c90a8ae5a05c4c91b7ac370f72467c4cb0d019edcd0ea80b994393e zbackup.ps1
06cde8cd7332a42f25c11666e759ab8002690c00261df9cd7010a5f29e26edda zbackup_and_sync.ps1 b6e5e15f8e2b128219c7b8b9db3e9b7804eee60fd6aed43f184210048e518f3c zbackup_and_sync.ps1
29bd3b062a8f02c51eb6abfa50e506e304d85ddffe1f870e2ba3ff11fda6077d zbackup_ec2.cmd b407ef5b298cd6fe94af492e9254b4447e34333d22f8c29d3e9d4d2881651cf3 zbackup_ec2.cmd
5833f9c685581879f669a9e19d6c791d876fc91a316c89a8d60fda77dd35de75 zbackup_ec2.ps1 55df5f2e19e81317b1e33b7b030b252fc4d6f49c0c77229d9e4e86affeb3b8ef zbackup_ec2.ps1
f1fb8fb35ec468cf9c084e28f77398b59479daab06fe96909c6c2e7e8ea1a561 zchecksums.cmd 64bc3148b09e422ffe4890339fade2354a31f2c7ba004f2728c29a0a39c32f08 zchecksums.cmd
7f967d89feeeaf4ca1fbea93a3d013a1a7b0141e11ec6f2888b6d4ac0bdb3485 zchecksums.ps1 3e1c573e446238cae494d22d25376f278333dc25fdbc0a5abbdaa9349722b438 zchecksums.ps1
6126973cbc0ccb22785340354afb9a1495f3d9798f60d3e6fc1c3d3a92285d67 zdeploy.cmd 049f8e79fc8c3d8d96ae8ccb1155d191e2dbf2753f4d4d73ceca00aa1ab0481b zdeploy.cmd
bd4868914e022bb1cf4a4e755d69d76a42eddbcfbce5ae441f08969233a4004d zdeploy.ps1 d2ead96436507c8efedc3c72045e623137a1f844940c4ff57c9c7ebe67645511 zdeploy.ps1
1c2c99cbb986fa8993d47404eaba89ae05b854b6396a541783b46ee22ba359e9 zec2.cmd 30918a9260cb6220d6e2140edcf319fc0fa1e75b7fe36d0efe68b0d10cdde241 zec2.cmd
6b427881c670e2c1ba2ce95bee1ee446be770da85cde1fd91c36cbf40c39cff0 zec2.ps1 695ec87ea6518933ee64524d369f1d0d5128bf6e54db31f555a888bdddcb9326 zec2.ps1
0e4e9cbd19fdd151bdc07df8d72aee577e446e2280dbc7afa0039d503364e991 zec2_rotatekeys.cmd ce3209ed8eaab242286e76aeb11b1249239e2804e3de456a1609cc7caa780b43 zec2_rotatekeys.cmd
25068fa51dddd221c0b05628d78c7fa14f1d8c853c6c3f94ec760430122a1589 zec2_rotatekeys.ps1 f58277779b5c54814c29dda55ab567960e305fa9ef515b1206076be97f623093 zec2_rotatekeys.ps1
408d43e37a6febf3543dd74f449b00d6cdb4cfa6d5ae6ab48b6b840bbfd63ecb zec2online.cmd 30ed73711eeddb518e02eb5e1968b7cc1dad51f27ecf42155b515b097487c3f3 zec2online.cmd
f1c3a0f911da86e018f9b3d4d7c9be048210aa56c016548570edfbab79fed8ef zec2online.ps1 1e8814928910ff4f3da59478511feb75280ba2e12111840fb47d2c5504741f0f zec2online.ps1
832ff9fb1f9b318b0868272ba64194f6f7bb4259dea2e6d0417787cec990f2c1 ZHelpers.ps1 4ea81dc9e3cafc514b1e8224e633bf22bd88bfdc6e53545ff88a2a2d33715b4f ZHelpers.ps1
1e1dcdc76250d57b2b393a650fa853452f4cb271b58a05a7dc8c01f436958424 zkill.cmd d97f9b5c68526c3295796b3042ee86194721eacab56b0efa0f3fdf16c881b0ea zkill.cmd
1eb4c23bdc0ed1a82dc495c623b49e5dcd4e342f026b4d896e32c79d592667db zkill.ps1 587de0b176788de917e713915ac468e44bf00cf9c9c7d6ea9b88aaec41ad6ea7 zkill.ps1
1c908b69fb9200610e080ac6bb8f4c15d3d7edf402d2e119c2405158e1986a52 ZKiller.ps1 66556f55688d1f31e890d2b36143d3d0e2f5fcdded562e3d0a9591c02e827cda ZKiller.ps1
558dfdfc7b4d12231e476c14a00c678e2e536140c4b7abbc05364bf214562291 ZKillOnly.ps1 2c9e0fa5dabb1544b6600cb60e5f66e89c787f217db246553b830152ab976ec3 ZKillOnly.ps1
36b01f2cd1d5967dc3ec1313fc4f82ada2ea669d0529a0d313300e21a5b38d04 zrelease.cmd 479d01a4c962eb1dba2dbfee913c3704048f29581a0a06d068c0fb9145c3a51c zmerge.ps1
e48b994605b1ad9f793f95d653f50df41db628a6b2c6976415b7d03e3900a373 zrelease.ps1 dd1cfde33aec53fc0df4a34e45704a59bf48e094fcafcc0ec49e9d13aa442b6f zpull.cmd
02635351847f84d0f644ffa9c0073804e480d360fcabcd6a8b793cffd6f0026f zrepair.cmd 9e746ad18b92ee7344061847b38c870f280421cf55d31e36a72a1514772e53a2 zpull.ps1
ffb544517ca8613ea2babc61dad19352a06840b1cb8c692e3f4b890f1e214297 zrepair.ps1 4a57e270fc75ae5419bd27cf01b7dd6d8965be8dd58e0c80af985cb7bf27bdc5 zrelease.cmd
d2d80e9f063284cb4c4a0605e5e4aa4636f1d2bdb8bce2bd038961ae95515501 zrestart.cmd a6906e118f13442340cc4e0b14d7523d63e85d2bdb0eff8fd3a7c25567af8962 zrelease.ps1
2107fdd0e25ed6694eb992e706590da0f6fb0461e9cef300df5ca8ae568d27b3 zrestart.ps1 58e5b604cc260af7e644412094ff6e3bf799f80f9e52b9a83a044299f7704107 zrepair.cmd
5b5a54a82c126552624af4a35b3f0da222b64e614b639877950dbd4d12cfb64f zrestartd.cmd 186f7b0fb72808466ece328ea1a4e3a97bb2d4bae9f204ff7055e8b7f009614b zrepair.ps1
00eb0665d23d700170267b23be494c63f0ffaea420813e153aabfe49e5594d0f zsetup.cmd d97c9cf55b94c53dba49471d13f5c9870f2f50627ac5732ac4b60222da936244 zrestart.cmd
ed73481327cb3fbf2a2eff85fbe1dc723c4f26a2f6b9e54c16719be6ce96fd8f zsetup.ps1 32a8484d356c5f740d7a12f18bf0dd51a0c7da66010f6a6f553d976484315eef zrestart.ps1
46695f5009dee0dd425fccc3993c6c368882257bc03e91111f69ee8a6250e342 zsetup_mail.ps1 2ebf37f72323cac3963a4a95d1ae414fecf4da1e1e618147663c6cf94d1ece24 zrestartd.cmd
98ac29e9d767d5173de828fba46bb0d82bc57999f2da83eec50079c934d44119 zstart.cmd 0a15ad0d341a5efdcd4c14e22e91d4b45f2bf87d0c5608fcf985200f6797bab4 zsetup.cmd
70381eacf132ac3af60655e72fd6e40db2c3be2c5978ef451207b4597191f712 zstart.ps1 0d3042c5e44c3edf396005d0177f744c0c29536c25a50743e93a92156aa2a96c zsetup.ps1
d7ef72318b6764e2948272deae539a65904c6b5e06ef5c2aaf0e435da3d025d2 zstart_docker.cmd 498ad7f8a7a56b332fede299e7172211ba360e76d9244c6c93f7cceae8a58619 zsetup_mail.ps1
293f2d9a77c3657f7a40f4a03ddbc723adf5871b3b6a1c614911cf0222984069 zstart_docker.ps1 46b3782d9b05649bdb1bcbd2a007bf6abc889abee420ac2f6e75530fa45a4694 zstart.cmd
f37fcf3b49f7af8a39507293c4f96fdbc3cc460318acae9fdae13b4ac89688e0 zstartd.cmd 4b50cadf761f285f4fe655b143e04ec6b3565646ee8fb1ed165c234a201802b7 zstart.ps1
50537f209f3152f5a9afef6c28ad1b3bac37bca70bc5ee5c7b54236ed7acb01b zstop.ps1 2d74dcca3d4f51a28c84fbef5cc134126b61dfcc8ffb35e2d72f9596bf685809 zstart_docker.cmd
d51df3f48a18c7e2225e1a24b5db8c984eee196d58038e015ac915e1e2384d33 zsync.cmd 5016654d9ad68e11b85594be5e05318e19c5c6154174f734c43251970bc2086f zstart_docker.ps1
fee5782dc503ba3bbd3a987698277ba467bbdce5ea0527f8f42fb396e3ef3db7 zsync.ps1 1e1c5990e1dba41a165e797790f73cd33fcee5035eec65dd0d2610ea8b56a023 zstartd.cmd
2056ffca049697a186015eec3da2b6634153482e294659fa550c6e9d693395a3 zversion.cmd a894876ad9fa1bfac6cacfec2a837914debb773e05b975d9b3cdf56589dd8ed2 zstop.ps1
6656725cee68f9082a0932e15c24a08eddc24247e387a9f8ae5dd24413836188 zversion.ps1 2e165d35d9915099b98a14329c9eb2774b2bf2979c937bc3843d95b5b16400e7 zsync.cmd
5d008cc252b978a9f873101139054988dff24562655bdfcd22e781aa5c14029a zsync.ps1
d55020eeb926e7190d06cc3de3e4a0d0656e8c73e8a89032c06ffe29c0b661eb zversion.cmd
25de4c34219edb98b583be639b798b27c353e978a81f0480d49ba9d965f70f0b zversion.ps1

View File

@ -1,5 +1,5 @@
<!-- <!--
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.
--> -->

38
ELEVATOR_PITCH.txt Normal file
View File

@ -0,0 +1,38 @@
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.
Elevator Pitch
==============
The one-liner
-------------
AI coding agents waste thousands of tokens a day on infrastructure orchestration. Token Savers gives you one-word commands to run those parts yourself — so your agent spends tokens on code, not on SSH.
The 30-second version
---------------------
Every time your AI coding agent runs your infrastructure for you — a deploy, a restart, a health check — the full output lands in its context window: Docker layers, SSH banners, health-check chatter. We measured it per run — at a typical active-day cadence that's ~26,500 tokens of pure script output through the agent, and a single full Docker rebuild adds ~35,000 more. The dollars are small; the context is not — every line of infrastructure noise crowds out the code your agent is supposed to be reasoning about.
Token Savers collapses the infrastructure side into short, one-word commands you run yourself: zdeploy myapp, zrepair myapp, zstart myapp. Describe each project once in zconfig.json — where it lives, what kind it is, where it deploys — and every command just knows. You run the deploy; your agent edits the code. You run the health check; your agent reads the result and fixes whatever's wrong.
Measured per-run; ~26,500 tokens of script output per active development day at a typical cadence — kept out of your agent's context entirely when you run the commands yourself. See TOKEN_SAVINGS.md (TOKEN_SAVINGS.md) for the per-script measurements and method.
Why it's different
------------------
- Built around the AI-agent workflow. The commands are short on purpose — fewer keystrokes for you, fewer tokens when an agent invokes them. But the real saving is the operations you don't hand to the agent at all.
- The project name IS the command. zstart blog, zdeploy api, zbackup store — no flags to memorize, no switches to wire up.
- One config file, zero secrets in git. Server IP, SSH key, paths, and project definitions live in one gitignored JSON. Clone it anywhere, drop in your config, go.
- It verifies the deploy actually landed. Not "did the server return 200" (a stale cache does that too) — it checks that the build number went live, so you know the code you just shipped is the code that's running.
Who it's for
------------
Solo developers and small teams running several containerized web apps (Python, Vite, Next.js, plus edge proxies and stock Docker images) on a single VPS or EC2 box, from a Windows dev machine, over SSH — and using AI coding agents to write the code.
The tagline
-----------
Fewer keystrokes. Fewer tokens. One config to rule your fleet.

View File

@ -1,5 +1,5 @@
<!-- <!--
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.
--> -->
@ -136,6 +136,8 @@ The `.cmd` wrappers are the everyday interface. Every command takes one or more
| `zchecksums [-Update]` | Verify every script against `CHECKSUMS.txt` (SHA-256) | | `zchecksums [-Update]` | Verify every script against `CHECKSUMS.txt` (SHA-256) |
| `zversion [bump \| bump-stage <s> \| set <v>]` | Show or advance the toolkit version (stamps every header) | | `zversion [bump \| bump-stage <s> \| set <v>]` | Show or advance the toolkit version (stamps every header) |
| `zrelease [-Verify]` | Package the current version as `releases/zscripts-<version>.zip` + `.sha256` | | `zrelease [-Verify]` | Package the current version as `releases/zscripts-<version>.zip` + `.sha256` |
| `zmerge [-Execute\|-e]` | Merge every pull request across the org that is genuinely ready |
| `zpull [-Execute\|-e]` | `zmerge`, then bring every affected local checkout current |
### Local development ### Local development
@ -409,6 +411,24 @@ Verification walks its channels in trust order — docker-network `viaProxy`, th
Each release is packaged as a zip in [`releases/`](releases/) — grab the latest `zscripts-v*.zip`, check it, unzip, done: Each release is packaged as a zip in [`releases/`](releases/) — grab the latest `zscripts-v*.zip`, check it, unzip, done:
**Windows · PowerShell** — these commands are a PowerShell toolkit, so this is
most people's path. `sha256sum` and `unzip` are not Windows commands:
```powershell
$zip = "zscripts-v1.0.0.0.0.zip"
(Get-FileHash $zip -Algorithm SHA256).Hash -eq (Get-Content "$zip.sha256").Split()[0] # True = good
Expand-Archive $zip -DestinationPath zscripts
cd zscripts
.\zchecksums.cmd # verify the contents
```
`Get-FileHash` prints the hash in **upper** case and the `.sha256` file holds it
in lower — they look different side by side and are not. `-eq` on strings is
case-insensitive in PowerShell, so the comparison above is right; trust the
`True`, not your eyes.
**macOS · Linux · Git Bash · WSL**
```bash ```bash
sha256sum -c zscripts-v1.0.0.0.0.zip.sha256 # verify the download sha256sum -c zscripts-v1.0.0.0.0.zip.sha256 # verify the download
unzip zscripts-v1.0.0.0.0.zip -d zscripts # extract unzip zscripts-v1.0.0.0.0.zip -d zscripts # extract

View File

@ -1,4 +1,4 @@
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.
@ -133,6 +133,8 @@ The .cmd wrappers are the everyday interface. Every command takes one or more pr
| zchecksums [-Update] | Verify every script against CHECKSUMS.txt (SHA-256) | | zchecksums [-Update] | Verify every script against CHECKSUMS.txt (SHA-256) |
| zversion [bump \| bump-stage <s> \| set <v>] | Show or advance the toolkit version (stamps every header) | | zversion [bump \| bump-stage <s> \| set <v>] | Show or advance the toolkit version (stamps every header) |
| zrelease [-Verify] | Package the current version as releases/zscripts-<version>.zip + .sha256 | | zrelease [-Verify] | Package the current version as releases/zscripts-<version>.zip + .sha256 |
| zmerge [-Execute\|-e] | Merge every pull request across the org that is genuinely ready |
| zpull [-Execute\|-e] | zmerge, then bring every affected local checkout current |
Local development Local development
----------------- -----------------
@ -382,6 +384,22 @@ Downloading without cloning
Each release is packaged as a zip in releases/ (releases/) — grab the latest zscripts-v*.zip, check it, unzip, done: Each release is packaged as a zip in releases/ (releases/) — grab the latest zscripts-v*.zip, check it, unzip, done:
Windows · PowerShell — these commands are a PowerShell toolkit, so this is
most people's path. sha256sum and unzip are not Windows commands:
$zip = "zscripts-v1.0.0.0.0.zip"
(Get-FileHash $zip -Algorithm SHA256).Hash -eq (Get-Content "$zip.sha256").Split()[0] # True = good
Expand-Archive $zip -DestinationPath zscripts
cd zscripts
.\zchecksums.cmd # verify the contents
Get-FileHash prints the hash in upper case and the .sha256 file holds it
in lower — they look different side by side and are not. -eq on strings is
case-insensitive in PowerShell, so the comparison above is right; trust the
True, not your eyes.
macOS · Linux · Git Bash · WSL
sha256sum -c zscripts-v1.0.0.0.0.zip.sha256 # verify the download sha256sum -c zscripts-v1.0.0.0.0.zip.sha256 # verify the download
unzip zscripts-v1.0.0.0.0.zip -d zscripts # extract unzip zscripts-v1.0.0.0.0.zip -d zscripts # extract
cd zscripts && sha256sum -c CHECKSUMS.txt # verify the contents cd zscripts && sha256sum -c CHECKSUMS.txt # verify the contents

68
SECURITY.md Normal file
View File

@ -0,0 +1,68 @@
# Security
How to report a vulnerability, what to expect, and what is in scope:
**[the evomedia-net security policy](https://github.com/evomedia-net/.github/blob/main/SECURITY.md)**.
Short version — email [dev@evomedia.net](mailto:dev@evomedia.net), not a public
issue.
What follows is particular to this repository, which is unusual in one way
worth stating plainly.
## This is a mirror, and the interesting bug is a leak
These scripts are published from a private tree. The copy here is sanitised:
placeholder hosts, example configuration, dummy data. So the most valuable
thing anyone can report about this repository is not a crash — it is
**something real that should not be here**:
- a credential, key, token, or private-key block
- an internal hostname, a product domain, or an operator's path
- an identifier that names a private project or a private-only script
If you find one, treat it as a live secret and mail
[dev@evomedia.net](mailto:dev@evomedia.net) rather than opening an issue. A
public issue about a leaked secret publishes it a second time and pins it to
the top of the page.
**The automated check is a denylist.** `tests/Sanitization.Tests.ps1` and
`tests/sanitization-patterns.psd1` hold the patterns this repository must never
contain, and CI enforces them. A denylist proves the absence of *known*
patterns, not the absence of secrets — it is a regression net for a specific
recurring mistake, not a substitute for reading what is published. That is why
a report here is worth sending even though the tests are green.
## They are automation scripts, so read them before running them
Everything here drives real infrastructure: archives a working tree, uploads
it, rebuilds containers, restarts services. That is the purpose, and it means
the ordinary rules for running someone else's shell scripts apply with more
force than usual.
- **Read a script before the first run**, and run it against something you can
afford to break.
- **Nothing here is a sandbox.** There is no dry-run guarantee unless a script
documents one; the flag that exists on one command may not exist on the next.
- **The configuration is yours.** The example config carries placeholders, and
every host, key path and target in it has to be replaced with your own before
anything is pointed at real infrastructure.
- Addresses in examples use the ranges reserved for documentation, and
loopback. They are placeholders, not somewhere to send anything.
Scripts that destroy or overwrite state are the ones to read twice. A report
that one of them does something destructive **without saying so** is a good
report; a report that a script named after a destructive act performs it is
not.
## Release integrity
Releases carry checksums. They are an **integrity check, not a signature** —
they catch a truncated download, a corrupted mirror and an accidental edit,
and they do not catch a forger, because whoever can change an archive can
change the manifest that travels with it.
## Not a finding here
- **Placeholder credentials and example configuration.** Fake values are the
sanitisation working, not a leak.
- **The private tree.** Only what is published here is in scope; the internal
original is not public and cannot be reviewed.

73
SECURITY.txt Normal file
View File

@ -0,0 +1,73 @@
Security
========
How to report a vulnerability, what to expect, and what is in scope:
the evomedia-net security policy (https://github.com/evomedia-net/.github/blob/main/SECURITY.md).
Short version — email dev@evomedia.net (mailto:dev@evomedia.net), not a public
issue.
What follows is particular to this repository, which is unusual in one way
worth stating plainly.
This is a mirror, and the interesting bug is a leak
---------------------------------------------------
These scripts are published from a private tree. The copy here is sanitised:
placeholder hosts, example configuration, dummy data. So the most valuable
thing anyone can report about this repository is not a crash — it is
something real that should not be here:
- a credential, key, token, or private-key block
- an internal hostname, a product domain, or an operator's path
- an identifier that names a private project or a private-only script
If you find one, treat it as a live secret and mail
dev@evomedia.net (mailto:dev@evomedia.net) rather than opening an issue. A
public issue about a leaked secret publishes it a second time and pins it to
the top of the page.
The automated check is a denylist. tests/Sanitization.Tests.ps1 and
tests/sanitization-patterns.psd1 hold the patterns this repository must never
contain, and CI enforces them. A denylist proves the absence of known
patterns, not the absence of secrets — it is a regression net for a specific
recurring mistake, not a substitute for reading what is published. That is why
a report here is worth sending even though the tests are green.
They are automation scripts, so read them before running them
-------------------------------------------------------------
Everything here drives real infrastructure: archives a working tree, uploads
it, rebuilds containers, restarts services. That is the purpose, and it means
the ordinary rules for running someone else's shell scripts apply with more
force than usual.
- Read a script before the first run, and run it against something you can
afford to break.
- Nothing here is a sandbox. There is no dry-run guarantee unless a script
documents one; the flag that exists on one command may not exist on the next.
- The configuration is yours. The example config carries placeholders, and
every host, key path and target in it has to be replaced with your own before
anything is pointed at real infrastructure.
- Addresses in examples use the ranges reserved for documentation, and
loopback. They are placeholders, not somewhere to send anything.
Scripts that destroy or overwrite state are the ones to read twice. A report
that one of them does something destructive without saying so is a good
report; a report that a script named after a destructive act performs it is
not.
Release integrity
-----------------
Releases carry checksums. They are an integrity check, not a signature —
they catch a truncated download, a corrupted mirror and an accidental edit,
and they do not catch a forger, because whoever can change an archive can
change the manifest that travels with it.
Not a finding here
------------------
- Placeholder credentials and example configuration. Fake values are the
sanitisation working, not a leak.
- The private tree. Only what is published here is in scope; the internal
original is not public and cannot be reviewed.

View File

@ -1,5 +1,5 @@
<!-- <!--
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.
--> -->

296
TOKEN_SAVINGS.txt Normal file
View File

@ -0,0 +1,296 @@
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.
Token Savings: Why You Should Run These Scripts Yourself
========================================================
Running these scripts manually keeps their output out of your AI coding agent's
context window. Every line the agent doesn't have to read is a token you don't
pay for — and a token the agent can spend on the actual problem instead of on
deployment sequencing, SSH output, and Docker health checks.
This document reports two baselines side by side:
- You run it → Claude runs the script. What Claude ingests if it invokes the
z-script as a single command. These figures are measured (see method below).
- You run it → Claude orchestrates raw. What Claude would ingest if the scripts
didn't exist and it drove scp / ssh / docker compose step by step itself.
These figures are estimates — the same command output plus the agent's
reasoning and retry logic across every discrete step.
The savings from running a script yourself is the first column: if you run it,
Claude ingests zero. The extra value of having the scripts at all is the gap
between the two columns.
Dollar equivalents use a blended input/output rate: Sonnet 5 ≈ $9/1M | Opus 4.8 ≈ $15/1M | Fable 5 ≈ $30/1M
> Measurement note: "Measured" figures come from token-count.ps1, which runs
> each script under Start-Transcript and counts output characters ÷ 3.5
> chars/token. Captured in Claude Code (Sonnet 4.6) against the sp project.
> Strictly speaking that's measured output volume with estimated tokenization:
> ÷3.5 is a prose heuristic, and code-heavy output (paths, JSON, container IDs)
> fragments into more tokens per character under a real BPE tokenizer — so the
> token figures here are likely conservative. "Estimated (raw)" figures are not
> measured — they approximate manual orchestration and are marked est.
> throughout. Other models/interfaces tokenize differently.
---
Measured per-run output (script-run baseline)
---------------------------------------------
The bold figures below are real captures from token-count.ps1 sp; rows flagged est. are not:
| Script | Measured tokens/run | Notes |
|--------|--------------------:|-------|
| zec2online | 267 | reachability + version check |
| zec2 | 331 | EC2 TCP/HTTP + build match |
| zbackup_ec2 | 334 | pull server backup |
| zrepair | 364 | clean audit; more if it restarts containers |
| zkill | 377 | free the dev port |
| zbackup | 436 | local project snapshot |
| zrestart | 724 | kill + restart (detached) |
| zstart | 762 | start dev server (detached) |
| zsync | 769 | mirror backups offsite |
| zdeploy (cached) | ~810 | 53s deploy, layers cached |
| zdeploy (full rebuild) | ~34,600 est. | packages changed; streams full docker build |
| zstart_docker | not measured | est. ~500–1,500 |
Cache state is what drives zdeploy. A cached deploy is ~810 tokens; the
large number only appears on a full rebuild (dependencies changed), which streams
the entire docker build. During rapid deploy → test → fix iteration almost every run
is cached, so ~810 is the realistic per-run cost — with occasional spikes when you
change packages.
---
Local Development Control
-------------------------
zstart — Start dev servers
--------------------------
Measured: ~762 tokens/run | est. raw orchestration: ~1,500–3,000 | typical 2–3 runs/day
Run it yourself and Claude sees none of the version-bump, MOTD, and startup output.
If Claude started the server raw, it would also wait on health checks and confirm
the port is listening — reasoning the script does deterministically.
zstart viteapp # start Vite dev server on its configured port
zstart pyapp -Port 3000 # override the port
zstart nextapp -Detached # start in background, prompt returns
---
zkill — Stop dev servers
------------------------
Measured: ~377 tokens/run | est. raw orchestration: ~1,000–2,000 | typical 2–3 runs/day
Raw, Claude would enumerate processes, kill them, and re-check the port is free.
The script collapses that to one command.
zkill viteapp
zkill pyapp nextapp
---
zrestart — Restart in one command
---------------------------------
Measured: ~724 tokens/run | est. raw orchestration: ~2,500–4,500 | typical 10–15 runs/day
The most-used command during rapid iteration. Raw, it's stop → wait → start with
error handling at each hop — several tool calls and their reasoning. As one script
it's a single call, and the -Detached switch now propagates correctly through the
kill→restart chain so the server backgrounds cleanly.
zrestart viteapp
zrestart pyapp -Detached
---
Build & Deployment
------------------
zdeploy — Deploy to EC2
-----------------------
Measured: ~810 tokens/run cached (spikes to ~34,600 on a full rebuild) | est. raw orchestration: ~5,000–12,000 cached, ~35,000+ full rebuild | typical 10–15 runs/day
The biggest lever — and the one where cache state matters most. The script streams
the docker/SSH output whether Claude runs it or not, so a cached deploy really is only
~810 tokens even through Claude. The raw-orchestration cost is higher not because of
extra output but because Claude would reason between ~15 discrete steps (zip, preflight
cleanup, scp, unzip, build, up, version bump, restart, verify) and handle retries
itself. Running it yourself zeroes out all of that.
Measured cached: three runs at 808 / 858 / 808 tokens (53–54s each). The full-rebuild
figure (~34,600) is an estimate for package-change deploys — treat it as the upper
bound.
zdeploy pyapp -Note "Fix nav alignment"
zdeploy edge # reload edge nginx config
zdeploy all -Note "weekly release"
---
zstart_docker — Start local Docker stack
----------------------------------------
Not measured (est. ~500–1,500 tokens/run) | typical 1 run/day
One-time setup per session; doesn't need agent involvement.
---
Backup & Sync
-------------
zbackup — Backup projects locally
---------------------------------
Measured: ~436 tokens/run | est. raw orchestration: ~1,200–2,500 | typical 1–2 runs/day
Raw, Claude enumerates files, decides exclusions, compresses, and stamps timestamps.
You decide when to snapshot.
zbackup # everything + scripts folder
zbackup pyapp -Tag "pre-refactor"
---
zsync — Sync backups offsite
----------------------------
Measured: ~769 tokens/run | est. raw orchestration: ~1,500–3,000 | typical 1 run/day
Raw, Claude tracks file diffs, runs robocopy, and verifies the copy. You manage
cadence independently.
zsync
zsync viteapp # build + mirror dist to $env:ZSYNC_DEST
---
zbackup_ec2 — Pull backups from the server
------------------------------------------
Measured: ~334 tokens/run | est. raw orchestration: ~1,000–2,000 | typical 1 run/day
Separates database/app backup from code changes. Claude focuses on code; you manage
infrastructure snapshots.
zbackup_ec2
---
Diagnostics & Troubleshooting
-----------------------------
zec2 — Check EC2 reachability
-----------------------------
Measured: ~331 tokens/run (zec2online: ~267) | est. raw orchestration: ~1,000–2,000 | typical 5–8 runs/day
When a deploy fails you run this first to confirm EC2 is reachable and the right
build is live — before asking Claude to debug. Raw, that's blind network diagnostics
over SSH. Runs frequently alongside zdeploy.
zec2 viteapp
zec2 # check all projects
zec2online sp # lightweight HTTP-only variant
---
zrepair — Audit & repair container routing
------------------------------------------
Measured: ~364 tokens/run (clean audit) | est. raw orchestration: ~2,000–4,000 | typical 1–2 runs/day
When a page 502s, this isolates routing vs. DNS vs. app logic across several
containers — rather than handing Claude an SSH session to figure out blind. The
364-token figure is a healthy run with nothing to repair; a run that actually
restarts containers emits more. Raw, Claude would SSH per container and reason
across each check.
zrepair viteapp
---
Daily Token Savings Summary
---------------------------
Per-run × runs/day. The per-run figures are measured; the daily totals multiply
them by assumed typical run counts (midpoints) — zdeploy and zrestart at
10–15/day dominate the sum, so scale the total to your own cadence. The est. raw
column approximates what Claude would burn orchestrating the same work with no
scripts.
| Script | Measured/run | Runs/day | Measured/day | Est. raw/day |
|--------|-------------:|:--------:|-------------:|-------------:|
| zstart | 762 | 2–3 | ~1,900 | ~3,800–9,000 |
| zkill | 377 | 2–3 | ~940 | ~2,500–6,000 |
| zrestart | 724 | 10–15 | ~9,050 | ~31,000–68,000 |
| zdeploy (cached) | ~810 | 10–15 | ~10,100 | ~62,000–180,000 |
| zec2 (+online) | ~330 | 5–8 | ~2,200 | ~6,500–16,000 |
| zbackup | 436 | 1–2 | ~650 | ~1,800–5,000 |
| zsync | 769 | 1 | ~770 | ~1,500–3,000 |
| zbackup_ec2 | 334 | 1 | ~330 | ~1,000–2,000 |
| zrepair | 364 | 1–2 | ~550 | ~3,000–6,000 |
| Total (active dev day) | | | ~26,500 | ~115,000–295,000 est. |
The ~26,500/day figure is measured per-run at an assumed typical cadence —
reproducible on the per-run side, workflow-specific on the multiplier. It reflects an
active tool-development day of mostly cached deploys. The
~115k–295k est. upper figure is what it would cost to have Claude drive the raw
ssh/docker sequences instead — dominated by per-step reasoning on zdeploy and
zrestart, not by output volume. Treat that column as an **upper bound, not a
prediction**: a capable agent asked to deploy might well write its own wrapper
script and ingest very little — the counterfactual depends entirely on how the
agent chooses to work. A day with several full-rebuild deploys pushes the measured
figure higher too, since each rebuild streams ~34,600 tokens.
Daily dollar savings during active tool development:
Script output the agent ingests is billed at input rates, so the measured column
uses input pricing. The est.-raw column keeps the blended rate, because raw
orchestration also generates agent output (reasoning and tool calls between steps).
| Model | Measured/day @ input rate | Est. raw/day @ blended rate |
|-------|--------------------------:|----------------------------:|
| Sonnet 5 | ~$0.08 ($3/1M) | ~$1.04–$2.66 ($9/1M) |
| Opus 4.8 | ~$0.13 ($5/1M) | ~$1.73–$4.43 ($15/1M) |
| Fable 5 | ~$0.27 ($10/1M) | ~$3.45–$8.85 ($30/1M) |
One-time ingest slightly understates the true cost: tokens that enter the context are
re-sent on every later turn of the session (at cheaper cache-read rates when prompt
caching applies), so the cumulative figure is somewhat higher than a single ingest.
Over a ~22-day working month, the measured savings run ~$2–$6/mo (Sonnet →
Fable); the raw-orchestration estimate runs ~$23–$195/mo. The honest dollar
figure is small — the real currency is context: every infrastructure token kept
out of the window is context your agent keeps for the actual problem, and that's
worth more than the dollars suggest.
---
Claude Model Token Costs (July 2026)
------------------------------------
| Model | Input | Output | Typical use |
|-------|-------|--------|-------------|
| Haiku 4.5 | $1/1M | $5/1M | Quick edits, small changes |
| Sonnet 5 | $3/1M | $15/1M | Daily coding, medium complexity |
| Opus 4.8 | $5/1M | $25/1M | Complex reasoning, multi-file refactors |
| Fable 5 | $10/1M | $50/1M | Advanced reasoning, agentic workflows |
---
When to Run Scripts Yourself vs. Ask the Agent
----------------------------------------------
Run yourself when:
- ✅ You know exactly what action is needed
- ✅ The script is deterministic (same input = same output)
- ✅ You want to parallelize — run zstart while asking Claude for code
- ✅ You're troubleshooting and need fast feedback loops
Ask the agent when:
- ❌ You need conditional logic ("if this test fails, try X")
- ❌ You're chaining operations that depend on each other's output
- ❌ You want the agent to interpret script output and decide next steps
Bottom line: These scripts are optimized for you to run directly. Use them. Save
tokens. Let Claude focus on coding.

View File

@ -1,7 +1,7 @@
# 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.
# Version: v1.0.0.0.24 # Version: v1.0.0.0.28
# ZHelpers.ps1 — shared library dot-sourced by every z script. Not run directly. # ZHelpers.ps1 — shared library dot-sourced by every z script. Not run directly.
@ -105,6 +105,33 @@ function Get-ZEdgeProject {
} }
# Remote compose directory for a project: remote.composeDir if set, else remote.path. # Remote compose directory for a project: remote.composeDir if set, else remote.path.
# How a docker stack gets its images: built here, or pulled from a registry.
#
# `docker compose pull` is right for a stack of published images and wrong for
# one built from a Dockerfile in the tree - there is nothing to pull. The trap
# is what happens next: `docker compose up -d` builds only when the image is
# MISSING. So the FIRST deploy of a build-from-source stack works, and every
# one after it uploads the new code, starts the old image, and reports success.
# That is worse than an error, because nothing looks wrong: the deploy is
# green, the container is up, and the change simply is not in it.
#
# deploy.build opts a project into building instead. --pull refreshes the base
# image at the same time, so a rebuild also picks up its security updates
# rather than pinning whatever happened to be on the box the first time.
function Get-DockerImageStep {
param($Proj, [Parameter(Mandatory)][string]$RemotePath)
if ($Proj -and $Proj.deploy -and $Proj.deploy.build) {
return @{
Label = 'docker compose build'
Command = "cd $RemotePath && sudo docker compose build --pull"
}
}
return @{
Label = 'docker compose pull'
Command = "cd $RemotePath && sudo docker compose pull"
}
}
function Get-RemoteComposeDir { function Get-RemoteComposeDir {
param([Parameter(Mandatory)][string]$Key) param([Parameter(Mandatory)][string]$Key)
$proj = Get-ZProject -Key $Key $proj = Get-ZProject -Key $Key
@ -563,14 +590,50 @@ function Get-LabelFromVersionJson {
} }
function Get-LabelFromBuildJsonObj { function Get-LabelFromBuildJsonObj {
<#
.SYNOPSIS
The build label out of a parsed build-version.json, or $null.
.DESCRIPTION
Three stamp shapes, and this has to read all of them because deploy
verification runs BOTH sides through it - the local stamp and the one read
back from the running container. A shape it cannot read does not fail
loudly; it collapses both sides to the same wrong string and the comparison
passes unconditionally, which is worse than having no check at all.
{ "major":1, "rc":0, "beta":0, "alpha":0, "build":98 } object form
{ "version": "v1.0.0.0.98" } string form
{ "productVersion": "1.2", "buildNumber": 7 } legacy form
The string form is what the versioning scheme specifies and what zbump
writes, so it is checked FIRST - a stamp carrying both an explicit version
and stray numeric fields means the version it states.
Earned the hard way: the string form used to fall through to the legacy
branch, where productVersion is null ("") and buildNumber is null (0), so
EVERY string-form stamp became "v.0". A site verified `expect v.0` against
a live `v.0` and reported PASS while serving whatever it liked.
#>
param($obj) param($obj)
if (-not $obj) { return $null } if (-not $obj) { return $null }
# Five-segment scheme: v{major}.{rc}.{beta}.{alpha}.{build}
# String form: the version is stated, so state it back. Trimmed, and given
# the leading v the other branches add, so all three shapes are comparable.
if (-not [string]::IsNullOrWhiteSpace([string]$obj.version)) {
$v = ([string]$obj.version).Trim()
return $(if ($v -match '^[vV]') { 'v' + $v.Substring(1) } else { "v$v" })
}
# Object form: v{major}.{rc}.{beta}.{alpha}.{build}
if ($null -ne $obj.build -or $null -ne $obj.alpha) { if ($null -ne $obj.build -or $null -ne $obj.alpha) {
$alpha = if ($null -ne $obj.alpha) { [int]$obj.alpha } else { 1 } $alpha = if ($null -ne $obj.alpha) { [int]$obj.alpha } else { 1 }
return "v$([int]$obj.major).$([int]$obj.rc).$([int]$obj.beta).$alpha.$([int]$obj.build)" return "v$([int]$obj.major).$([int]$obj.rc).$([int]$obj.beta).$alpha.$([int]$obj.build)"
} }
# Legacy two-part stamp (projects not yet migrated): v{productVersion}.{buildNumber} # Legacy two-part stamp (projects not yet migrated): v{productVersion}.{buildNumber}
# Only reached when there is something to build it from; otherwise $null, so
# a caller sees "no label" instead of a label that matches everything.
if ([string]::IsNullOrWhiteSpace([string]$obj.productVersion) -and $null -eq $obj.buildNumber) { return $null }
return "v$([string]$obj.productVersion).$([int]$obj.buildNumber)" return "v$([string]$obj.productVersion).$([int]$obj.buildNumber)"
} }

View File

@ -1,7 +1,7 @@
# 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.
# Version: v1.0.0.0.24 # Version: v1.0.0.0.28
# ZKillOnly.ps1 — stop local dev server listeners for any project in zconfig.json. # ZKillOnly.ps1 — stop local dev server listeners for any project in zconfig.json.
# #

View File

@ -1,7 +1,7 @@
# 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.
# Version: v1.0.0.0.24 # Version: v1.0.0.0.28
# ZKiller.ps1 — kill then restart dev servers for any project in zconfig.json. # ZKiller.ps1 — kill then restart dev servers for any project in zconfig.json.
# #

View File

@ -1,3 +1,3 @@
{ {
"version": "v1.0.0.0.24" "version": "v1.0.0.0.28"
} }

Binary file not shown.

View File

@ -0,0 +1 @@
6f6f1bf1b46e014e0518ef4ffa2b64e455e894681d5b337ffdd947b0efd0d538 zscripts-v1.0.0.0.25.zip

Binary file not shown.

View File

@ -0,0 +1 @@
dfaa85f53e4dd16789ac0f1f8c9d7e506b56c0c068f4c5f8e2c4d1cce7db8d95 zscripts-v1.0.0.0.26.zip

Binary file not shown.

View File

@ -0,0 +1 @@
f5b940344ffd832032b0c62e65e77f94bc2de294e690c9fd68362deb99d21e82 zscripts-v1.0.0.0.27.zip

Binary file not shown.

View File

@ -0,0 +1 @@
5eca5198ba500bb0e1ceb1e5000cfb405d5fb5024fa500daa8f64f9c012c1291 zscripts-v1.0.0.0.28.zip

View File

@ -1,4 +1,4 @@
# 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.
@ -28,11 +28,20 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent ROOT = Path(__file__).resolve().parent.parent
# Every markdown file that owes the repo a plain-text twin. def pairs() -> tuple[tuple[str, str], ...]:
PAIRS = ( """Every root-level markdown file, with the twin it owes.
("README.md", "README.txt"),
("CHANGELOG.md", "CHANGELOG.txt"), Discovered rather than listed. The list was hand-kept, and two files had
) quietly outgrown it - ELEVATOR_PITCH.md and TOKEN_SAVINGS.md had no twin
at all, because adding a document and remembering to add it here are two
separate acts and the second one is the one that gets skipped. Discovery
makes them one act.
"""
return tuple((f.name, f.with_suffix(".txt").name) for f in sorted(ROOT.glob("*.md")))
#: Kept as a name because the Pester suite reads it to know what to check.
PAIRS = pairs()
def _inline(text: str) -> str: def _inline(text: str) -> str:

View File

@ -1,7 +1,7 @@
# 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.
# Version: v1.0.0.0.24 # Version: v1.0.0.0.28
# setup_backup_schedule.ps1 — create a scheduled task for daily backups + OneDrive sync # setup_backup_schedule.ps1 — create a scheduled task for daily backups + OneDrive sync
# #

108
site/index.html Normal file
View File

@ -0,0 +1,108 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>zscripts — one-word dev commands</title>
<meta name="description" content="One-word commands for AI-assisted development. Routine start, deploy, and backup chores become scripts — saving tokens and keeping the AI's context window focused on real work.">
<link rel="canonical" href="https://zscripts.evomedia.net/">
<!-- Open Graph — LinkedIn, Slack and the rest build link previews from
these. The URLs must be absolute: a relative og:image is dropped by the
strict crawlers and the card renders as bare text. -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://zscripts.evomedia.net/">
<meta property="og:site_name" content="evomedia.net">
<meta property="og:title" content="zscripts — one-word dev commands">
<meta property="og:description" content="One-word commands for AI-assisted development. Routine start, deploy and backup chores become scripts, keeping the AI's context window on real work.">
<meta property="og:image" content="https://zscripts.evomedia.net/og-card.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="zscripts — one-word dev commands, over a terminal showing zdeploy, zbackup and zrestart.">
<!-- Twitter/X card, reusing the same image. -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="zscripts — one-word dev commands">
<meta name="twitter:description" content="One-word commands for AI-assisted development. Routine start, deploy and backup chores become scripts, keeping the AI's context window on real work.">
<meta name="twitter:image" content="https://zscripts.evomedia.net/og-card.png">
<style>
:root{
--bg:#0d1117; --panel:#161b22; --border:#2a313c;
--fg:#e6edf3; --muted:#9aa7b4; --accent:#4ea1ff; --accent2:#3fb950;
--mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace;
--sans:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;
}
*{box-sizing:border-box}
html,body{margin:0;padding:0}
body{background:var(--bg);color:var(--fg);font-family:var(--sans);line-height:1.55;
-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
.wrap{max-width:760px;margin:0 auto;padding:64px 24px 80px}
.eyebrow{font-family:var(--mono);font-size:.8rem;letter-spacing:.08em;text-transform:uppercase;color:var(--accent)}
h1{font-size:2.6rem;line-height:1.1;margin:.4rem 0 .2rem;font-weight:700}
h1 .z{color:var(--accent)}
.tag{font-size:1.2rem;color:var(--muted);margin:0 0 2rem}
.lead{font-size:1.05rem;color:var(--fg);margin:0 0 2rem}
.card{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:20px 22px;margin:0 0 18px}
.card h2{font-size:.95rem;margin:0 0 12px;color:var(--muted);font-weight:600;letter-spacing:.02em;text-transform:uppercase}
pre{margin:0;font-family:var(--mono);font-size:.92rem;overflow-x:auto;color:var(--fg)}
pre .c{color:var(--muted)}
pre .g{color:var(--accent2)}
.cmds{display:grid;grid-template-columns:auto 1fr;gap:6px 18px;font-size:.95rem}
.cmds code{font-family:var(--mono);color:var(--accent);white-space:nowrap}
.cmds span{color:var(--muted)}
.actions{display:flex;gap:12px;flex-wrap:wrap;margin-top:6px}
a.btn{display:inline-block;text-decoration:none;font-weight:600;font-size:.95rem;
padding:11px 20px;border-radius:9px;border:1px solid var(--border)}
a.primary{background:var(--accent);color:#04121f;border-color:var(--accent)}
a.ghost{color:var(--fg)}
a.primary:hover{filter:brightness(1.08)}
a.ghost:hover{border-color:var(--accent);color:var(--accent)}
footer{margin-top:44px;padding-top:20px;border-top:1px solid var(--border);color:var(--muted);font-size:.85rem}
footer a{color:var(--muted)}
</style>
</head>
<body>
<main class="wrap">
<p class="eyebrow">evomedia.net · developer tools</p>
<h1><span class="z">z</span>scripts</h1>
<p class="tag">Short, one-word commands for multi-project development.</p>
<p class="lead">
A small suite of PowerShell commands built for AI-assisted
development. Routine start / deploy / backup chores become single
words a coding agent can run without reasoning through them — which
saves tokens, but the real win is keeping the AI's context window
focused on the actual work instead of housekeeping.
</p>
<div class="card">
<h2>The idea</h2>
<pre><span class="c"># every chore the agent doesn't narrate is context kept free</span>
<span class="g">zstart</span> sp <span class="c"># launch a project's dev server</span>
<span class="g">zdeploy</span> sp <span class="c"># package + ship it</span>
<span class="g">zbackup</span> all <span class="c"># snapshot the databases</span></pre>
</div>
<div class="card">
<h2>What's in the box</h2>
<div class="cmds">
<code>zstart</code><span>run a project's dev server locally</span>
<code>zdeploy</code><span>build and deploy to the server</span>
<code>zbackup</code><span>back up project databases</span>
<code>zrestart</code><span>restart a running dev server</span>
<code>zkill</code><span>stop a dev server cleanly</span>
</div>
</div>
<div class="actions">
<a class="btn primary" href="https://github.com/evomedia-net/evo.zscripts">View on GitHub</a>
<a class="btn ghost" href="https://github.com/evomedia-net/evo.zscripts#readme">Read the docs</a>
</div>
<footer>
An <a href="https://evomedia.net">evomedia.net</a> project ·
open source, sanitized for public use.
</footer>
</main>
</body>
</html>

BIN
site/og-card.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

7
site/robots.txt Normal file
View File

@ -0,0 +1,7 @@
# zscripts.evomedia.net — the public page for this toolkit.
#
# Deliberately the opposite of the candidate pages on this estate
# (cardiff, opensesame, kelly, unify), which disallow everything. This one
# is an open-source project page: being found is the point.
User-agent: *
Allow: /

View File

@ -1,4 +1,4 @@
# 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.
@ -73,14 +73,30 @@ BeforeAll {
$fixture | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath (Join-Path $script:Install "zconfig.json") -Encoding UTF8 $fixture | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath (Join-Path $script:Install "zconfig.json") -Encoding UTF8
# Run a script in a child process; capture merged output and exit code. # Run a script in a child process; capture merged output and exit code.
#
# Memoised on the exact command. Several checks deliberately assert
# different things about the SAME invocation - that running bare exits
# non-zero, that its usage lists the project keys, and that the usage
# never mentions the underscore comment key are three checks of one run.
# Starting a Windows PowerShell costs about 1.7 s, so re-running the same
# command to ask it a second question is the single most expensive thing
# this file does. The scripts reached here either refuse their input or
# inspect an unused fixture port, so none of them has a side effect a
# second run would reveal.
$script:zRuns = @{}
function Invoke-ZScript { function Invoke-ZScript {
param([string]$Script, [string[]]$ScriptArgs = @()) param([string]$Script, [string[]]$ScriptArgs = @())
$key = @($Script) + $ScriptArgs -join "`n"
if ($script:zRuns.ContainsKey($key)) { return $script:zRuns[$key] }
$path = Join-Path $script:Install $Script $path = Join-Path $script:Install $Script
$out = & powershell -NoProfile -ExecutionPolicy Bypass -File $path @ScriptArgs 2>&1 | ForEach-Object { "$_" } $out = & powershell -NoProfile -ExecutionPolicy Bypass -File $path @ScriptArgs 2>&1 | ForEach-Object { "$_" }
return [pscustomobject]@{ $result = [pscustomobject]@{
ExitCode = $LASTEXITCODE ExitCode = $LASTEXITCODE
Output = ($out -join "`n") Output = ($out -join "`n")
} }
$script:zRuns[$key] = $result
return $result
} }
} }

View File

@ -1,4 +1,4 @@
# 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.

View File

@ -0,0 +1,82 @@
# How a docker stack gets its images, and the deploy that shipped nothing.
#
# Invoke-Pester .\tests
#
# `docker compose pull` is right for a stack of published images - Prometheus,
# Grafana, docker-mailserver - and wrong for one built from a Dockerfile in the
# tree, where there is nothing to pull.
#
# The trap is what happens after. `docker compose up -d` builds only when the
# image is MISSING, so the first deploy of a build-from-source stack works and
# every one after it uploads the new code, starts the OLD image, and reports
# success. Green deploy, healthy container, and the change is not in it. That
# is the failure this helper exists to prevent, and it is worse than an error
# because nothing about it looks wrong.
#
# ZHelpers.ps1 is dot-sourced rather than zdeploy.ps1: zdeploy executes its
# main flow on load, helpers only define functions.
BeforeAll {
. (Join-Path (Split-Path -Parent $PSScriptRoot) "ZHelpers.ps1")
function New-Proj { param($Build)
if ($null -eq $Build) { return [pscustomobject]@{ deploy = [pscustomobject]@{ gitPull = $true } } }
return [pscustomobject]@{ deploy = [pscustomobject]@{ build = $Build } }
}
}
Describe 'Get-DockerImageStep - build here, or pull from a registry' {
It 'pulls by default, so every existing docker stack is unaffected' {
$step = Get-DockerImageStep -Proj (New-Proj $null) -RemotePath '/home/u/stack/x'
$step.Command | Should -BeLike '*docker compose pull*'
$step.Command | Should -Not -BeLike '*build*'
}
It 'pulls for a project with no deploy block at all' {
$step = Get-DockerImageStep -Proj ([pscustomobject]@{}) -RemotePath '/home/u/stack/x'
$step.Command | Should -BeLike '*docker compose pull*'
}
It 'builds when the project asks to be built' {
$step = Get-DockerImageStep -Proj (New-Proj $true) -RemotePath '/home/u/stack/x'
$step.Command | Should -BeLike '*docker compose build*'
$step.Command | Should -Not -BeLike '*compose pull*'
}
It 'still pulls when build is explicitly false' {
$step = Get-DockerImageStep -Proj (New-Proj $false) -RemotePath '/home/u/stack/x'
$step.Command | Should -BeLike '*docker compose pull*'
}
It 'refreshes the base image on a build, so a rebuild is not pinned to the first one' {
$step = Get-DockerImageStep -Proj (New-Proj $true) -RemotePath '/home/u/stack/x'
$step.Command | Should -BeLike '*--pull*'
}
It 'runs in the project directory: <Build>' -ForEach @(
@{ Build = $true }
@{ Build = $false }
) {
$step = Get-DockerImageStep -Proj (New-Proj $Build) -RemotePath '/home/u/stack/ablecamera'
$step.Command | Should -BeLike 'cd /home/u/stack/ablecamera &&*'
}
It 'labels the step with what it actually does: <Build>' -ForEach @(
@{ Build = $true; Expected = 'docker compose build' }
@{ Build = $false; Expected = 'docker compose pull' }
) {
(Get-DockerImageStep -Proj (New-Proj $Build) -RemotePath '/x').Label | Should -Be $Expected
}
}
Describe 'the docker deploy uses it' {
It 'no longer hardcodes compose pull' {
$text = Get-Content -Raw (Join-Path (Split-Path -Parent $PSScriptRoot) "zdeploy.ps1")
$body = $text.Substring($text.IndexOf('function Invoke-DockerDeploy'))
$body = $body.Substring(0, $body.IndexOf('function Invoke-ZTokensPublish'))
$body | Should -Match 'Get-DockerImageStep'
$body | Should -Not -Match '"docker compose pull"'
}
}

View File

@ -1,4 +1,4 @@
# 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.

View File

@ -0,0 +1,90 @@
# 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.
# LinkPreview.Tests.ps1 - the public page renders a card, not a bare URL.
#
# Invoke-Pester .\tests
#
# Pasting zscripts.evomedia.net into LinkedIn, Slack or a message builds a card
# from the page's og:* tags. The page had a title and a description and nothing
# else, so it pasted as a bare URL.
#
# None of that is visible from here. The page is correct, the site is up, and
# the only symptom is a card somewhere else - which is why the rule is asserted
# rather than remembered.
#
# The last test is the one that earned its place: the first draft of the card
# showed `ztests`, which is not a command in this repo. A card is public copy,
# and public copy must not advertise a script that does not exist.
BeforeAll {
$script:RepoRoot = Split-Path -Parent $PSScriptRoot
$script:Page = Join-Path $script:RepoRoot "site\index.html"
$script:Card = Join-Path $script:RepoRoot "site\og-card.png"
$script:Html = [IO.File]::ReadAllText($script:Page)
function Get-Meta {
param([string]$Key)
$pattern = '<meta (?:property|name)="' + [regex]::Escape($Key) + '" content="([^"]*)">'
$m = [regex]::Match($script:Html, $pattern)
if ($m.Success) { return $m.Groups[1].Value }
return $null
}
}
Describe "zscripts.evomedia.net link preview" {
It "carries the tags a card is built from" {
foreach ($key in @('og:type', 'og:url', 'og:title', 'og:description', 'og:image')) {
Get-Meta $key | Should -Not -BeNullOrEmpty -Because "$key is what the card is made of"
}
# Without it X renders the small square variant instead of a card.
Get-Meta 'twitter:card' | Should -Be 'summary_large_image'
}
It "gives absolute URLs, which the strict crawlers require" {
foreach ($key in @('og:url', 'og:image', 'twitter:image')) {
Get-Meta $key | Should -Match '^https://'
}
}
It "points og:url at the same place as the canonical" {
$canonical = [regex]::Match($script:Html, '<link rel="canonical" href="([^"]*)">')
$canonical.Success | Should -BeTrue
Get-Meta 'og:url' | Should -Be $canonical.Groups[1].Value
}
It "publishes the card beside the page" {
# site/ is copied to the server wholesale; a card outside it is a 404
# and the preview falls back to text.
Test-Path -LiteralPath $script:Card | Should -BeTrue
}
It "serves a card that is the size the tags claim" {
# PNG header: width and height are big-endian at offsets 16 and 20.
$bytes = [IO.File]::ReadAllBytes($script:Card)[0..23]
$width = [int]$bytes[16] * 16777216 + [int]$bytes[17] * 65536 + [int]$bytes[18] * 256 + [int]$bytes[19]
$height = [int]$bytes[20] * 16777216 + [int]$bytes[21] * 65536 + [int]$bytes[22] * 256 + [int]$bytes[23]
$width | Should -Be 1200
$height | Should -Be 630
Get-Meta 'og:image:width' | Should -Be '1200'
Get-Meta 'og:image:height' | Should -Be '630'
}
It "does not advertise a command this repo does not ship" {
$alt = Get-Meta 'og:image:alt'
$alt | Should -Not -BeNullOrEmpty
$named = [regex]::Matches($alt, '\bz[a-z_]+\b') | ForEach-Object { $_.Value } | Sort-Object -Unique
$named.Count | Should -BeGreaterThan 0 -Because 'the alt text names the commands on the card'
foreach ($cmd in $named) {
if ($cmd -eq 'zscripts') { continue } # the toolkit, not a command
$exists = @('.ps1', '.cmd') | Where-Object {
Test-Path -LiteralPath (Join-Path $script:RepoRoot "$cmd$_")
}
$exists | Should -Not -BeNullOrEmpty -Because "$cmd is on the card but is not in this repo"
}
}
}

View File

@ -1,4 +1,4 @@
# 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.

View File

@ -38,14 +38,19 @@ Describe "plain-text twins" {
Test-Path -LiteralPath $script:Generator | Should -BeTrue Test-Path -LiteralPath $script:Generator | Should -BeTrue
} }
It "every .md that owes a twin has one" { # Asks the REPOSITORY what markdown it has, not the generator what it was
$pairs = Select-String -Path $script:Generator -Pattern '^\s*\("([^"]+\.md)",\s*"([^"]+\.txt)"\),' | # told about. Scraping the generator's own list could only ever prove the
ForEach-Object { [pscustomobject]@{ Md = $_.Matches[0].Groups[1].Value; Txt = $_.Matches[0].Groups[2].Value } } # list was self-consistent - a document nobody added to it was invisible to
# the check, which is how ELEVATOR_PITCH.md and TOKEN_SAVINGS.md sat here
# with no twin while this test passed.
It "every .md at the repository root has a twin" {
$mds = Get-ChildItem -LiteralPath $script:RepoRoot -Filter *.md -File
$pairs.Count | Should -BeGreaterThan 0 -Because "PAIRS in plaintext_twins.py is what this suite checks" $mds.Count | Should -BeGreaterThan 0 -Because "the repo documents itself in markdown"
foreach ($p in $pairs) { foreach ($md in $mds) {
Test-Path -LiteralPath (Join-Path $script:RepoRoot $p.Md) | Should -BeTrue -Because "$($p.Md) is listed in PAIRS" $txt = [IO.Path]::ChangeExtension($md.FullName, ".txt")
Test-Path -LiteralPath (Join-Path $script:RepoRoot $p.Txt) | Should -BeTrue -Because "$($p.Md) owes a twin at $($p.Txt)" Test-Path -LiteralPath $txt |
Should -BeTrue -Because "$($md.Name) owes a twin at $(Split-Path -Leaf $txt)"
} }
} }

View File

@ -1,4 +1,4 @@
# 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.

View File

@ -1,4 +1,4 @@
# 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.

View File

@ -23,15 +23,21 @@
#> #>
@{ @{
Denied = @( Denied = @(
@{ Name = 'private project name'; Pattern = '\b(EvoCivilCode|EvoPlatform|DocketMail|SmartPlant\w*|ProvenSheet|evoehs|evoproven|evoaicc|evolocate|evoplatform)\b' } # The retired EHS name is split with a one-character class in both rules
# below (Smart[P]lant, smart[p]lantehs). The regex is identical - a class of
# one matches exactly that character - but the literal no longer appears in
# this file, which is public. The private tree still carries that name in
# ~20 places, so these rules are still load-bearing: do not delete them,
# and do not un-split them.
@{ Name = 'private project name'; Pattern = '\b(EvoCivilCode|EvoPlatform|DocketMail|Smart[P]lant\w*|ProvenSheet|evoehs|evoproven|evoaicc|evolocate|evoplatform)\b' }
# The current spellings, which the line above never saw: a dot or a # The current spellings, which the line above never saw: a dot or a
# hyphen breaks the word and an underscore hides the boundary, so # hyphen breaks the word and an underscore hides the boundary, so
# evo.ehs, evo-ai and evoehs_app all passed (evo.scripts#138 in the # evo.ehs, evo-ai and evoehs_app all passed (evo.scripts#138 in the
# private tree). Internal issue references travel with them. # private tree). Internal issue references travel with them.
@{ Name = 'current product name'; Pattern = '(?i)\bevo[.-](ehs|ai|edge|locate|proven|platform|civilcode|scripts)\b|\bevoehs' } @{ Name = 'current product name'; Pattern = '(?i)\bevo[.-](ehs|ai|edge|locate|proven|platform|civilcode|scripts)\b|\bevoehs' }
@{ Name = 'internal issue reference'; Pattern = 'evo\.scripts#\d+' } @{ Name = 'internal issue reference'; Pattern = 'evo\.scripts#\d+' }
@{ Name = 'private product domain'; Pattern = '\b(smartplantehs\.com|provensheet\.com|evoehs\.com|civilcode\.evomedia\.net|dashboard\.evomedia\.net|webmail\.evomedia\.net|mail-admin\.evomedia\.net|docketmail\.evomedia\.net|cardiff\.evomedia\.net|platform\.evomedia\.net|ai\.evomedia\.net|git\.evomedia\.net|analytics\.evomedia\.net)\b' } @{ Name = 'private product domain'; Pattern = '\b(smart[p]lantehs\.com|provensheet\.com|evoehs\.com|civilcode\.evomedia\.net|dashboard\.evomedia\.net|webmail\.evomedia\.net|mail-admin\.evomedia\.net|docketmail\.evomedia\.net|cardiff\.evomedia\.net|platform\.evomedia\.net|ai\.evomedia\.net|git\.evomedia\.net|analytics\.evomedia\.net)\b' }
@{ Name = 'private-only script'; Pattern = '\b(register_civilcode|register_docketmail|sp_seed_demo_prod|zpublish_stats|zcoverage|zmerge|zpull|zresume|swag_set_owner|provision_demo|apply_platform_config_fixes)\b' } @{ Name = 'private-only script'; Pattern = '\b(register_civilcode|register_docketmail|sp_seed_demo_prod|zpublish_stats|zcoverage|zresume|swag_set_owner|provision_demo|apply_platform_config_fixes)\b' }
@{ Name = 'local drive path'; Pattern = '[A-Za-z]:\\\\?evomedia\.net' } @{ Name = 'local drive path'; Pattern = '[A-Za-z]:\\\\?evomedia\.net' }
@{ Name = 'operator home path'; Pattern = '/home/ubuntu/' } @{ Name = 'operator home path'; Pattern = '/home/ubuntu/' }
@{ Name = 'real pem key name'; Pattern = 'evomedia-prod\.pem' } @{ Name = 'real pem key name'; Pattern = 'evomedia-prod\.pem' }

View File

@ -1,7 +1,7 @@
# 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.
# Version: v1.0.0.0.24 # Version: v1.0.0.0.28
# token-count.ps1 — measure script output volume to estimate AI agent token costs. # token-count.ps1 — measure script output volume to estimate AI agent token costs.
# #

View File

@ -1,7 +1,7 @@
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
REM Created by Kelly Michels · dev@evomedia.net REM Created by Kelly Michels · dev@evomedia.net
REM Licensed under the MIT License. See LICENSE. REM Licensed under the MIT License. See LICENSE.
REM Version: v1.0.0.0.24 REM Version: v1.0.0.0.28
@echo off @echo off
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zbackup.ps1" %* powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zbackup.ps1" %*

View File

@ -1,7 +1,7 @@
# 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.
# Version: v1.0.0.0.24 # Version: v1.0.0.0.28
# zbackup.ps1 — local backups: zip project sources (plus a Postgres dump when the # zbackup.ps1 — local backups: zip project sources (plus a Postgres dump when the
# project's .env has a DATABASE_URL) into the backups folder. # project's .env has a DATABASE_URL) into the backups folder.

View File

@ -1,7 +1,7 @@
# 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.
# Version: v1.0.0.0.24 # Version: v1.0.0.0.28
# zbackup_and_sync.ps1 — run backups, then sync the backups folder offsite. # zbackup_and_sync.ps1 — run backups, then sync the backups folder offsite.
# #

View File

@ -1,7 +1,7 @@
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
REM Created by Kelly Michels · dev@evomedia.net REM Created by Kelly Michels · dev@evomedia.net
REM Licensed under the MIT License. See LICENSE. REM Licensed under the MIT License. See LICENSE.
REM Version: v1.0.0.0.24 REM Version: v1.0.0.0.28
@echo off @echo off
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zbackup_ec2.ps1" %* powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zbackup_ec2.ps1" %*

View File

@ -1,7 +1,7 @@
# 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.
# Version: v1.0.0.0.24 # Version: v1.0.0.0.28
# zbackup_ec2.ps1 — pull backups down from the server: a Postgres dump for projects # zbackup_ec2.ps1 — pull backups down from the server: a Postgres dump for projects
# with a "db" config block, plus a zip of server-side data dirs (uploads/archive/dist). # with a "db" config block, plus a zip of server-side data dirs (uploads/archive/dist).

View File

@ -1,7 +1,7 @@
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
REM Created by Kelly Michels · dev@evomedia.net REM Created by Kelly Michels · dev@evomedia.net
REM Licensed under the MIT License. See LICENSE. REM Licensed under the MIT License. See LICENSE.
REM Version: v1.0.0.0.24 REM Version: v1.0.0.0.28
@echo off @echo off
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zchecksums.ps1" %* powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zchecksums.ps1" %*

View File

@ -1,7 +1,7 @@
# 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.
# Version: v1.0.0.0.24 # Version: v1.0.0.0.28
# zchecksums.ps1 - verify (or regenerate) SHA-256 checksums for the scripts. # zchecksums.ps1 - verify (or regenerate) SHA-256 checksums for the scripts.
# #

View File

@ -122,6 +122,7 @@
"analytics": { "analytics": {
"label": "Analytics (any docker compose app)", "label": "Analytics (any docker compose app)",
"kind": "docker", "kind": "docker",
"_build_note": "Images pulled from a registry need nothing here. If the image is BUILT from a Dockerfile in this tree, add \"deploy\": { \"build\": true } \u2014 without it a redeploy uploads the new code and restarts the OLD image, and reports success.",
"localRoot": "C:\\YourRoot\\analytics", "localRoot": "C:\\YourRoot\\analytics",
"domain": "analytics.yourdomain.com", "domain": "analytics.yourdomain.com",
"remote": { "remote": {

View File

@ -1,7 +1,7 @@
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
REM Created by Kelly Michels · dev@evomedia.net REM Created by Kelly Michels · dev@evomedia.net
REM Licensed under the MIT License. See LICENSE. REM Licensed under the MIT License. See LICENSE.
REM Version: v1.0.0.0.24 REM Version: v1.0.0.0.28
@echo off @echo off
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zdeploy.ps1" %* powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zdeploy.ps1" %*

View File

@ -1,7 +1,7 @@
# 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.
# Version: v1.0.0.0.24 # Version: v1.0.0.0.28
# zdeploy.ps1 — deploy any project defined in zconfig.json to the server. # zdeploy.ps1 — deploy any project defined in zconfig.json to the server.
# Each project runs its own docker compose stack; the handler is picked by the # Each project runs its own docker compose stack; the handler is picked by the
@ -546,6 +546,16 @@ function Wait-VerifyStaticBuild {
# advances deliberately — one version bump per release — so "is the # advances deliberately — one version bump per release — so "is the
# build I just packed live?" means an exact match. # build I just packed live?" means an exact match.
$expectedLabel = Get-LabelFromBuildJsonObj $PreZipBuildState $expectedLabel = Get-LabelFromBuildJsonObj $PreZipBuildState
# A stamp we cannot read is not a version to check against. Comparing an
# unreadable local label to an unreadable remote one is how a verification
# once passed while the container served anything it liked, so refuse to
# run rather than run a comparison that cannot fail.
if ([string]::IsNullOrWhiteSpace($expectedLabel)) {
Write-Host "`n--- [$Key version] NOT VERIFIED - build-version.json is present but unreadable ---" -ForegroundColor Yellow
Write-Host " Got: $(($PreZipBuildState | ConvertTo-Json -Compress -Depth 4))" -ForegroundColor DarkGray
Write-Host " Expected one of: {`"version`":`"v1.0.0.0.0`"} | {major,rc,beta,alpha,build} | {productVersion,buildNumber}" -ForegroundColor DarkGray
return
}
Write-Host "`n--- [$Key] Live build verification (expect $expectedLabel) ---" -ForegroundColor Cyan Write-Host "`n--- [$Key] Live build verification (expect $expectedLabel) ---" -ForegroundColor Cyan
$containerName = $Proj.remote.containerName $containerName = $Proj.remote.containerName
$deadline = (Get-Date).AddSeconds(45) $deadline = (Get-Date).AddSeconds(45)
@ -565,7 +575,7 @@ function Wait-VerifyStaticBuild {
} }
if ($r) { if ($r) {
$remoteLabel = Get-LabelFromBuildJsonObj $r $remoteLabel = Get-LabelFromBuildJsonObj $r
if ($remoteLabel -eq $expectedLabel) { if ($remoteLabel -and $remoteLabel -eq $expectedLabel) {
Write-Host " PASS - live build $remoteLabel matches expected." -ForegroundColor Green Write-Host " PASS - live build $remoteLabel matches expected." -ForegroundColor Green
return return
} }
@ -1166,8 +1176,8 @@ function Invoke-StaticDeploy {
param([string]$Key, $Proj) param([string]$Key, $Proj)
# Plain static sites - no build, no container of their own. The landing # Plain static sites - no build, no container of their own. The landing
# container serves them straight off disk out of /srv/$host, so shipping # container serves them straight off disk, one directory per host, so
# the files IS the deploy: there is nothing to restart afterwards. # shipping the files IS the deploy: there is nothing to restart afterwards.
$root = Join-Path $Proj.localRoot $Proj.siteDir $root = Join-Path $Proj.localRoot $Proj.siteDir
$remotePath = $Proj.remote.path $remotePath = $Proj.remote.path
@ -1244,7 +1254,10 @@ function Invoke-DockerDeploy {
if ($LASTEXITCODE -ne 0) { throw "SCP failed for $($d.Name) (exit $LASTEXITCODE)" } if ($LASTEXITCODE -ne 0) { throw "SCP failed for $($d.Name) (exit $LASTEXITCODE)" }
} }
Invoke-Ec2Step "docker compose pull" "cd $remotePath && sudo docker compose pull" # Built here or pulled from a registry - see Get-DockerImageStep for why
# a build-from-source stack cannot use `pull` and silently ships nothing.
$imageStep = Get-DockerImageStep -Proj $Proj -RemotePath $remotePath
Invoke-Ec2Step $imageStep.Label $imageStep.Command
Invoke-Ec2Step "docker compose up -d" "cd $remotePath && sudo docker compose up -d" Invoke-Ec2Step "docker compose up -d" "cd $remotePath && sudo docker compose up -d"
Invoke-Ec2PostDeployCleanup -Label $Key Invoke-Ec2PostDeployCleanup -Label $Key
@ -1291,7 +1304,17 @@ function Invoke-ZTokensPublish {
# ── Dispatch ───────────────────────────────────────────────────────────────── # ── Dispatch ─────────────────────────────────────────────────────────────────
foreach ($key in $Projects) { # pip, uv and docker draw progress bars with box-drawing characters: "━" is
# the bytes E2 94 81. PowerShell 5.1 decodes a native command's stdout - ssh's,
# here - with [Console]::OutputEncoding, which on Windows is the OEM code page
# (437 on this machine), where those three bytes read "Γöü". Forty per bar.
# Decode the box's output as the UTF-8 it is for the duration of the deploy,
# and put the console back in the finally so a deploy that throws does not
# leave the session changed.
$prevConsoleEncoding = [Console]::OutputEncoding
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
try {
foreach ($key in $Projects) {
# 'ztokens' matches the tool it runs (ztokens.cmd / ztokens.ps1). The old # 'ztokens' matches the tool it runs (ztokens.cmd / ztokens.ps1). The old
# singular 'ztoken' still works so existing habits and any script that # singular 'ztoken' still works so existing habits and any script that
# already calls it keep running. # already calls it keep running.
@ -1307,6 +1330,9 @@ foreach ($key in $Projects) {
"static" { Invoke-StaticDeploy -Key $key -Proj $proj } "static" { Invoke-StaticDeploy -Key $key -Proj $proj }
default { throw "No deploy handler for kind '$($proj.kind)' (project '$key'). Add an Invoke-<Kind>Deploy function in zdeploy.ps1." } default { throw "No deploy handler for kind '$($proj.kind)' (project '$key'). Add an Invoke-<Kind>Deploy function in zdeploy.ps1." }
} }
}
} finally {
[Console]::OutputEncoding = $prevConsoleEncoding
} }
# The timestamp goes through Stop-ZTracking as the FinalNote so it lands after # The timestamp goes through Stop-ZTracking as the FinalNote so it lands after
# the tracking footer and before the trailing blank lines - the last thing on # the tracking footer and before the trailing blank lines - the last thing on

View File

@ -1,7 +1,7 @@
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
REM Created by Kelly Michels · dev@evomedia.net REM Created by Kelly Michels · dev@evomedia.net
REM Licensed under the MIT License. See LICENSE. REM Licensed under the MIT License. See LICENSE.
REM Version: v1.0.0.0.24 REM Version: v1.0.0.0.28
@echo off @echo off
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zec2.ps1" %* powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zec2.ps1" %*

View File

@ -1,7 +1,7 @@
# 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.
# Version: v1.0.0.0.24 # Version: v1.0.0.0.28
# zec2.ps1 — quick reachability check (TCP + HTTP + live build version) for deployed projects. # zec2.ps1 — quick reachability check (TCP + HTTP + live build version) for deployed projects.
# #
@ -22,9 +22,11 @@ Start-ZTracking
$cfg = Get-ZConfig $cfg = Get-ZConfig
if (-not $HostName) { $HostName = $cfg.ec2.ip } if (-not $HostName) { $HostName = $cfg.ec2.ip }
# Needed by the container-side version read below; same names zec2online.ps1 # Needed by the container-side version read below. zec2 had no ssh of its own
# uses. Without them that read throws inside its try/catch and falls through # before that, so the read referenced three variables this script never
# silently, which looks identical to a service that cannot be reached. # defined -- and because it 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. Same names zec2online.ps1 uses.
$PemKey = $cfg.ec2.pemKey $PemKey = $cfg.ec2.pemKey
$SshTarget = Get-Ec2Target $SshTarget = Get-Ec2Target
@ -103,9 +105,9 @@ function Show-Zec2LiveVersion {
$r = Invoke-RestMethod -Uri "http://${HostName}/build-version.json" -Headers $headers -TimeoutSec 10 -ErrorAction Stop $r = Invoke-RestMethod -Uri "http://${HostName}/build-version.json" -Headers $headers -TimeoutSec 10 -ErrorAction Stop
if ($r) { Write-Host " Live build: $(Get-LabelFromBuildJsonObj $r)" -ForegroundColor Gray } if ($r) { Write-Host " Live build: $(Get-LabelFromBuildJsonObj $r)" -ForegroundColor Gray }
} else { } else {
# Container-side first where the project configures it: a build # Container-side first where the project configures it: the
# stamp is not public on every site, and asking the proxy answers # endpoint is not public on every project, and asking the edge
# from whichever vhost matches the Host header. # answers from whichever vhost matches the Host header.
$execCmd = Get-ServerSideVersionCommand -Proj $Proj $execCmd = Get-ServerSideVersionCommand -Proj $Proj
$label = $null $label = $null
if ($execCmd -and (Test-Path $PemKey)) { if ($execCmd -and (Test-Path $PemKey)) {

View File

@ -1,7 +1,7 @@
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
REM Created by Kelly Michels · dev@evomedia.net REM Created by Kelly Michels · dev@evomedia.net
REM Licensed under the MIT License. See LICENSE. REM Licensed under the MIT License. See LICENSE.
REM Version: v1.0.0.0.24 REM Version: v1.0.0.0.28
@echo off @echo off
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zec2_rotatekeys.ps1" %* powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zec2_rotatekeys.ps1" %*

View File

@ -1,7 +1,7 @@
# 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.
# Version: v1.0.0.0.24 # Version: v1.0.0.0.28
# zec2_rotatekeys.ps1 - rotate / reset secret keys in a project's SERVER-SIDE # zec2_rotatekeys.ps1 - rotate / reset secret keys in a project's SERVER-SIDE
# .env, in place, without the values ever passing through this machine's shell # .env, in place, without the values ever passing through this machine's shell

View File

@ -1,7 +1,7 @@
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
REM Created by Kelly Michels · dev@evomedia.net REM Created by Kelly Michels · dev@evomedia.net
REM Licensed under the MIT License. See LICENSE. REM Licensed under the MIT License. See LICENSE.
REM Version: v1.0.0.0.24 REM Version: v1.0.0.0.28
@echo off @echo off
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zec2online.ps1" %* powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zec2online.ps1" %*

View File

@ -1,7 +1,7 @@
# 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.
# Version: v1.0.0.0.24 # Version: v1.0.0.0.28
# zec2online.ps1 — deep health check: verify apps are live AND running the expected # zec2online.ps1 — deep health check: verify apps are live AND running the expected
# build; auto-start downed stacks via docker compose and stream diagnostics. # build; auto-start downed stacks via docker compose and stream diagnostics.
@ -86,9 +86,10 @@ function Get-RemoteVersionLabel {
param($Proj) param($Proj)
$headers = @{} $headers = @{}
if ($Proj.domain) { $headers['Host'] = $Proj.domain } if ($Proj.domain) { $headers['Host'] = $Proj.domain }
# Container-side first where the project configures it. A build stamp is # Container-side first where the project configures it. The endpoint is
# not public on every site, and the proxy answers from whichever vhost # not public on every project, and the edge answers from whichever vhost
# matches the Host header - which is how a check reads another service. # matches the Host header -- which is how a check reads another
# product's version.
$execCmd = Get-ServerSideVersionCommand -Proj $Proj $execCmd = Get-ServerSideVersionCommand -Proj $Proj
if ($execCmd -and (Test-Path $PemKey)) { if ($execCmd -and (Test-Path $PemKey)) {
try { try {

View File

@ -1,7 +1,7 @@
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
REM Created by Kelly Michels · dev@evomedia.net REM Created by Kelly Michels · dev@evomedia.net
REM Licensed under the MIT License. See LICENSE. REM Licensed under the MIT License. See LICENSE.
REM Version: v1.0.0.0.24 REM Version: v1.0.0.0.28
@echo off @echo off
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0ZKillOnly.ps1" %* powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0ZKillOnly.ps1" %*

View File

@ -1,7 +1,7 @@
# 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.
# Version: v1.0.0.0.24 # Version: v1.0.0.0.28
# zkill.ps1 — alias for ZKillOnly.ps1 (kept so both names work). All args pass through. # zkill.ps1 — alias for ZKillOnly.ps1 (kept so both names work). All args pass through.
& (Join-Path $PSScriptRoot "ZKillOnly.ps1") @args & (Join-Path $PSScriptRoot "ZKillOnly.ps1") @args

251
zmerge.ps1 Normal file
View File

@ -0,0 +1,251 @@
# 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.
# Version: v1.0.0.0.28
# zmerge.ps1 - merge the fleet's ready pull requests in one pass.
#
# Usage:
# zmerge dry run: list every open PR and its verdict
# zmerge -Execute merge everything that is genuinely ready
# zmerge -e the same; -e is an alias, as -s is for -Scan
# zmerge -Exclude 431 skip PRs by number (repeatable)
# zmerge -Repo <name> limit to one repo
# zmerge -Only 68,67 merge just these
# zmerge -Execute -Yes skip the confirmation prompt
#
# WHY THIS EXISTS
# ---------------
# Eleven ready PRs across three repos is eleven trips through the GitHub UI, and
# the failure mode is not the clicking - it is that `gh pr create` and the merge
# button will both happily accept a PR that cannot actually merge. Mergeability
# is computed asynchronously, so a PR reports UNKNOWN for a few seconds after any
# push and CONFLICTING only later. Merging by hand, the tenth PR is the one that
# gets rubber-stamped.
#
# So this refuses to merge anything it has not just re-checked, and it re-checks
# after every merge, because merging one PR can conflict another in the same
# repo.
#
# WHAT IT WILL NOT DO
# -------------------
# * merge a PR that is not MERGEABLE/CLEAN at the moment it is reached
# * merge a draft, or one with a failing required check
# * bump versions - each repo stamps differently (zbump for zscripts,
# bump_build_version.mjs for www), and a wrong stamp is worse than none.
# The follow-up commands are printed instead.
# * deploy anything. Deploys are run by hand, deliberately.
#
# ON UNKNOWN
# ----------
# GitHub returns mergeable=UNKNOWN while it computes, which is indistinguishable
# from trouble if you only look once. Each PR is polled up to $PollTries times
# before being treated as not ready, so a slow answer does not read as a failure
# and a real CONFLICTING never reads as "probably fine".
param(
[Alias('e')][switch]$Execute,
[switch]$Yes,
[int[]]$Exclude = @(),
[int[]]$Only = @(),
[string]$Repo,
[int]$PollTries = 6,
[int]$PollDelaySeconds = 4
)
$ErrorActionPreference = "Stop"
# Two blank lines at the end of a run, matching every other z-script, so output
# is separated from the next prompt. Local copy rather than ZHelpers: this
# script does not dot-source it.
function Write-ZTrailer { Write-Host ""; Write-Host "" }
# Every repository in the org, asked of GitHub rather than remembered here.
#
# Local to this script for the same reason Write-ZTrailer is: zmerge needs gh
# and nothing else, and dot-sourcing 1,200 lines of deploy helpers for one
# function would trade that away.
#
# THROWS rather than returning an empty list when gh fails. A merge tool that
# quietly scans nothing prints exactly the same reassuring line as one that
# scanned everything and found nothing, and those two must never be
# confusable - which is precisely how the list this replaced hid its own rot.
function Get-FleetRepos {
param([Parameter(Mandatory)][string]$Org)
$raw = & gh repo list $Org --limit 200 --json name,isArchived 2>&1
if ($LASTEXITCODE -ne 0) {
throw "gh repo list $Org failed ($LASTEXITCODE): $($raw -join ' ')"
}
try { $all = $raw | ConvertFrom-Json } catch {
throw "gh repo list $Org did not return JSON: $($raw -join ' ')"
}
if (-not $all) { throw "gh repo list $Org returned no repositories" }
$names = @($all | Where-Object { -not $_.isArchived } |
ForEach-Object { $_.name } | Sort-Object)
if ($names.Count -eq 0) { throw "every repository in $Org is archived?" }
return $names
}
$ORG = "evomedia-net"
# Discovered, never listed. The list this replaced had fallen fourteen
# repositories behind: a scan covered sixteen of thirty and said "Nothing open
# to merge" while a ready PR sat in one of the fourteen it could not see.
$REPOS = Get-FleetRepos -Org $ORG
# How each repo advances its build stamp after a merge. Printed as follow-up,
# never run: see the header.
$BUMP = @{
"evo.zscripts" = "zbump"
"evo.www" = "node scripts/bump_build_version.mjs bump (on main, then push)"
}
function Invoke-Gh {
param([string[]]$GhArgs, [switch]$AllowFail)
$prev = $ErrorActionPreference
$ErrorActionPreference = "Continue"
try {
$out = & gh @GhArgs 2>&1 | ForEach-Object { "$_" }
$code = $LASTEXITCODE
} finally { $ErrorActionPreference = $prev }
if ($code -ne 0 -and -not $AllowFail) {
throw "gh $($GhArgs -join ' ') failed ($code): $($out -join "`n")"
}
return [pscustomobject]@{ Output = ($out -join "`n"); Code = $code }
}
function Get-OpenPrs {
param([string]$RepoName)
$r = Invoke-Gh @("pr", "list", "-R", "$ORG/$RepoName", "--state", "open",
"--limit", "100", "--json", "number,title,isDraft,headRefName") -AllowFail
if ($r.Code -ne 0 -or -not $r.Output) { return @() }
return @($r.Output | ConvertFrom-Json)
}
# Re-checked immediately before every merge, and again after each one, because
# merging into the default branch can conflict a sibling PR in the same repo.
function Get-Readiness {
param([string]$RepoName, [int]$Number)
for ($i = 1; $i -le $PollTries; $i++) {
$r = Invoke-Gh @("pr", "view", "$Number", "-R", "$ORG/$RepoName",
"--json", "mergeable,mergeStateStatus,state,isDraft") -AllowFail
if ($r.Code -ne 0) { return [pscustomobject]@{ Ready = $false; Why = "cannot read PR" } }
$j = $r.Output | ConvertFrom-Json
if ($j.state -ne "OPEN") { return [pscustomobject]@{ Ready = $false; Why = "state is $($j.state)" } }
if ($j.isDraft) { return [pscustomobject]@{ Ready = $false; Why = "draft" } }
if ($j.mergeable -eq "MERGEABLE" -and $j.mergeStateStatus -eq "CLEAN") {
return [pscustomobject]@{ Ready = $true; Why = "MERGEABLE/CLEAN" }
}
if ($j.mergeable -eq "CONFLICTING") {
return [pscustomobject]@{ Ready = $false; Why = "CONFLICTING - rebase it" }
}
# UNKNOWN, or a non-CLEAN state such as BLOCKED/BEHIND: give GitHub a
# moment, since it computes mergeability asynchronously.
if ($j.mergeable -ne "UNKNOWN" -and $j.mergeStateStatus -ne "UNKNOWN") {
return [pscustomobject]@{ Ready = $false; Why = "$($j.mergeable)/$($j.mergeStateStatus)" }
}
Start-Sleep -Seconds $PollDelaySeconds
}
return [pscustomobject]@{ Ready = $false; Why = "still UNKNOWN after $PollTries tries" }
}
$targets = if ($Repo) { @($Repo) } else { $REPOS }
Write-Host ""
Write-Host "Scanning $($targets.Count) repo(s) for open pull requests..." -ForegroundColor Cyan
$queue = @()
foreach ($r in $targets) {
foreach ($pr in (Get-OpenPrs -RepoName $r)) {
if ($Exclude -contains $pr.number) { continue }
if ($Only.Count -gt 0 -and $Only -notcontains $pr.number) { continue }
$queue += [pscustomobject]@{ Repo = $r; Number = $pr.number; Title = $pr.title; Draft = $pr.isDraft }
}
}
if ($queue.Count -eq 0) { Write-Host "Nothing open to merge." -ForegroundColor Yellow; Write-ZTrailer; exit 0 }
Write-Host ""
foreach ($p in $queue) {
$v = Get-Readiness -RepoName $p.Repo -Number $p.Number
$p | Add-Member -NotePropertyName Ready -NotePropertyValue $v.Ready -Force
$p | Add-Member -NotePropertyName Why -NotePropertyValue $v.Why -Force
$mark = if ($v.Ready) { "OK " } else { "SKIP" }
$col = if ($v.Ready) { "Green" } else { "Yellow" }
Write-Host (" {0} {1,-14} #{2,-4} {3}" -f $mark, $p.Repo, $p.Number, $p.Title) -ForegroundColor $col
if (-not $v.Ready) { Write-Host (" -> {0}" -f $v.Why) -ForegroundColor DarkYellow }
}
$ready = @($queue | Where-Object { $_.Ready })
Write-Host ""
Write-Host "$($ready.Count) of $($queue.Count) ready to merge." -ForegroundColor Cyan
if (-not $Execute) {
Write-Host ""
Write-Host "Dry run. Re-run with -Execute (or -e) to merge." -ForegroundColor Yellow
Write-ZTrailer
exit 0
}
if ($ready.Count -eq 0) { Write-ZTrailer; exit 1 }
if (-not $Yes) {
Write-Host ""
$answer = Read-Host "Squash-merge these $($ready.Count) PRs and delete their branches? (y/N)"
if ($answer -notmatch '^(y|yes)$') { Write-Host "Aborted." -ForegroundColor Yellow; Write-ZTrailer; exit 1 }
}
$merged = @(); $failed = @()
foreach ($p in $ready) {
# Re-check: an earlier merge in this same repo may have conflicted this one.
$v = Get-Readiness -RepoName $p.Repo -Number $p.Number
if (-not $v.Ready) {
Write-Host (" SKIP {0} #{1} - {2}" -f $p.Repo, $p.Number, $v.Why) -ForegroundColor Yellow
$failed += [pscustomobject]@{ Repo = $p.Repo; Number = $p.Number; Why = $v.Why }
continue
}
$r = Invoke-Gh @("pr", "merge", "$($p.Number)", "-R", "$ORG/$($p.Repo)",
"--squash", "--delete-branch") -AllowFail
if ($r.Code -eq 0) {
Write-Host (" MERGED {0} #{1}" -f $p.Repo, $p.Number) -ForegroundColor Green
$merged += $p
} else {
Write-Host (" FAILED {0} #{1}" -f $p.Repo, $p.Number) -ForegroundColor Red
Write-Host (" {0}" -f $r.Output) -ForegroundColor DarkRed
$failed += [pscustomobject]@{ Repo = $p.Repo; Number = $p.Number; Why = $r.Output }
}
}
Write-Host ""
Write-Host "merged $($merged.Count), failed/skipped $($failed.Count)" -ForegroundColor Cyan
# Verify rather than trust the exit codes - a merge can report success and leave
# the PR in an unexpected state.
if ($merged.Count -gt 0) {
Write-Host ""
Write-Host "Verifying:" -ForegroundColor Cyan
foreach ($p in $merged) {
$r = Invoke-Gh @("pr", "view", "$($p.Number)", "-R", "$ORG/$($p.Repo)",
"--json", "state,mergedAt") -AllowFail
$j = if ($r.Code -eq 0) { $r.Output | ConvertFrom-Json } else { $null }
$state = if ($j) { $j.state } else { "unreadable" }
$col = if ($state -eq "MERGED") { "Green" } else { "Red" }
Write-Host (" {0,-14} #{1,-4} {2}" -f $p.Repo, $p.Number, $state) -ForegroundColor $col
}
# Follow-up, printed not run: ONE build bump per release - not one per
# merged PR - on the default branch, and then a deploy. Both deliberately
# by hand. This used to print one bump per PR, which is how a single
# release came to be stamped as two builds.
Write-Host ""
Write-Host "Follow-up (not run):" -ForegroundColor Cyan
foreach ($grp in ($merged | Group-Object Repo)) {
$how = if ($BUMP.ContainsKey($grp.Name)) { $BUMP[$grp.Name] } else { "bump this repo's build stamp" }
Write-Host (" {0,-14} {1} merged -> 1 build bump for the release: {2}" -f $grp.Name, $grp.Count, $how)
}
Write-Host " then deploy each project you want live (zdeploy, by hand)"
}
if ($failed.Count -gt 0) { Write-ZTrailer; exit 1 }
Write-ZTrailer

6
zpull.cmd Normal file
View File

@ -0,0 +1,6 @@
@echo off
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
REM Created by Kelly Michels · dev@evomedia.net
REM Licensed under the MIT License. See LICENSE.
REM Version: v1.0.0.0.28
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zpull.ps1" %*

359
zpull.ps1 Normal file
View File

@ -0,0 +1,359 @@
# 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.
# Version: v1.0.0.0.28
# zpull.ps1 - merge the fleet's ready PRs, then bring the local checkouts current.
#
# Usage:
# zpull dry run: what would merge, what would pull
# zpull -Execute merge ready PRs, then pull every affected checkout
# zpull -e the same; -e is an alias, as -s is for -Scan
# zpull -Repo <name> limit to one repo
# zpull -Only 65 merge just these PR numbers
# zpull -PullOnly skip merging; only bring checkouts up to date
# zpull -Execute -Yes skip zmerge's confirmation prompt
#
# WHY THIS EXISTS
# ---------------
# zmerge stops at the merge, deliberately - deploys are run by hand. But the
# tooling repos are not deployed anywhere at all: they run
# from the local checkout. For those, "deployed" just means "pulled". Merging a
# zdeploy.ps1 fix and then forgetting the pull leaves you running the old file
# while GitHub says the bug is fixed - which is its own kind of lie.
#
# So: merge (via zmerge, which owns all the mergeability safety), then pull.
#
# WHAT IT WILL NOT DO
# -------------------
# * pull over uncommitted work. It reports and skips. Twice this month a
# checkout sat on a feature branch or held unstaged edits, and anything that
# "helpfully" resolved that would have destroyed real work.
# * pull anything but a fast-forward. A diverged local main is a decision,
# not something a sync script should guess at.
# * deploy to a server. Still by hand. This only touches local checkouts.
#
# HOW CHECKOUTS ARE FOUND
# -----------------------
# By reading each candidate directory's `origin` remote and matching the repo
# name, not from a hardcoded table - a table drifts the moment a directory is
# renamed, and this fleet renames directories.
[CmdletBinding(PositionalBinding = $false)]
param(
[Alias('e')][switch]$Execute,
[switch]$Yes,
[switch]$PullOnly,
# Sweep only the repos with no zdeploy target - the ones where a pull is
# the whole job. Tooling, archives, libraries.
[switch]$ReposOnly,
[string]$Repo,
[int[]]$Only = @(),
[int[]]$Exclude = @(),
# PowerShell binds --help to -Help on its own (it tolerates the extra
# dash), so this one switch answers --help, -help and -h. The bare words
# land in $Rest below and are handled there.
[Alias('h')][switch]$Help,
# Catches anything unmatched. Without it, PositionalBinding=$false makes an
# unknown argument a raw PowerShell binding error - a wall of red that does
# not say what the valid arguments are. Owning the message means a typo
# gets the usage block instead.
[Parameter(ValueFromRemainingArguments = $true)][string[]]$Rest = @()
)
$ErrorActionPreference = "Stop"
# Two blank lines at the end of a run, matching every other z-script, so output
# is separated from the next prompt. Local copy rather than ZHelpers: this
# script does not dot-source it.
function Write-ZTrailer { Write-Host ""; Write-Host "" }
$FLEET_ROOT = Split-Path -Parent $PSScriptRoot
$ORG = "evomedia-net"
function Show-ZPullUsage {
Write-Host ""
Write-Host "zpull - merge the fleet's ready PRs, then bring local checkouts current." -ForegroundColor Cyan
Write-Host ""
Write-Host "Usage: zpull [-Execute|-e] [-Yes] [-PullOnly] [-ReposOnly] [-Repo <name>] [-Only <n,n>] [-Exclude <n,n>]" -ForegroundColor Yellow
Write-Host ""
Write-Host " (no args) dry run - what would merge, what would pull. Changes nothing." -ForegroundColor Gray
Write-Host " -Execute, -e actually merge ready PRs, then pull every affected checkout" -ForegroundColor Gray
Write-Host " -PullOnly skip merging entirely; only bring checkouts up to date" -ForegroundColor Gray
Write-Host " -Repo <name> limit to one repo, by its GitHub name" -ForegroundColor Gray
Write-Host " -Only <n,n> merge just these PR numbers" -ForegroundColor Gray
Write-Host " -Exclude <n,n> merge everything ready except these PR numbers" -ForegroundColor Gray
Write-Host " -ReposOnly only repos with no deploy target (pull = done)" -ForegroundColor Gray
Write-Host " -Yes skip zmerge's confirmation prompt (needs -Execute)" -ForegroundColor Gray
Write-Host " --help, -h this text" -ForegroundColor Gray
Write-Host ""
Write-Host "What each result line means:" -ForegroundColor Yellow
Write-Host " ok already current - nothing to do" -ForegroundColor Gray
Write-Host " PULLED fast-forwarded to the new tip" -ForegroundColor Gray
Write-Host " SKIP deliberately left alone: uncommitted work, not on the default" -ForegroundColor Gray
Write-Host " branch, or diverged. Never resolved automatically." -ForegroundColor Gray
Write-Host " FAIL the repo could not be read or fetched. The sweep continues;" -ForegroundColor Gray
Write-Host " that one repo is simply not current." -ForegroundColor Gray
Write-Host ""
Write-Host "Each line is marked with what the repo still owes:" -ForegroundColor Yellow
Write-Host " [repo] nothing runs from a server - the pull is the whole job" -ForegroundColor Gray
Write-Host " [zdeploy <key>] a pull leaves the server on the old build" -ForegroundColor Gray
Write-Host ""
Write-Host "It will not pull over uncommitted work, will not do anything but a" -ForegroundColor DarkGray
Write-Host "fast-forward, and will not deploy. Deploys stay manual." -ForegroundColor DarkGray
Write-Host ""
Write-Host "Checkouts are found by reading each directory's origin remote under" -ForegroundColor DarkGray
Write-Host "$FLEET_ROOT, not from a hardcoded list." -ForegroundColor DarkGray
}
# Bare-word help too, matching the rest of the toolkit (zdeploy myapp, zkill all).
$helpWords = @('help', '?', '/?', '--help', '-help')
if ($Help -or @($Rest | Where-Object { $helpWords -contains $_.ToLowerInvariant() }).Count -gt 0) {
Show-ZPullUsage
Write-ZTrailer
exit 0
}
if ($Rest.Count -gt 0) {
Write-Host ""
Write-Host "ERROR: unrecognised argument(s): $($Rest -join ', ')" -ForegroundColor Red
Show-ZPullUsage
Write-ZTrailer
exit 1
}
function Get-DeployTargetsByPath {
# Checkout path -> the zdeploy keys that ship from it.
#
# Read from zconfig rather than listed here: a second table would drift the
# first time a target is added, and drift in THIS table is the failure it
# exists to prevent - a repo quietly reported as "done at the pull" while a
# server runs the old build.
#
# Matched by containment, not equality, because a target's localRoot is
# often a subdirectory of its checkout (a service may ship from
# <checkout>\<subdir>), and one checkout can carry several targets
# (vidplayer ships cardiff, opensesame and kelly).
$map = @{}
# Read here rather than via ZHelpers' Get-ZConfig: this script is
# standalone by design, and that helper exits the process when the config
# is missing - which would turn "no zconfig" into a dead sweep instead of
# a sweep that simply knows of no deploy targets.
$configPath = if ($env:ZCONFIG) { $env:ZCONFIG } else { Join-Path $PSScriptRoot "zconfig.json" }
if (-not (Test-Path -LiteralPath $configPath)) { return $map }
try {
$cfg = Get-Content -LiteralPath $configPath -Raw | ConvertFrom-Json
} catch {
Write-Host " (zconfig.json unreadable - every repo will report as [repo])" -ForegroundColor Yellow
return $map
}
if (-not $cfg.projects) { return $map }
foreach ($key in $cfg.projects.PSObject.Properties.Name) {
if ($key -like '_*') { continue } # underscore keys are comments
$root = $cfg.projects.$key.localRoot
if (-not $root) { continue }
try { $map[$key] = [System.IO.Path]::GetFullPath($root).TrimEnd('\') } catch { }
}
return $map
}
function Get-DeployKeysFor {
param([string]$Path, [hashtable]$Targets)
$full = [System.IO.Path]::GetFullPath($Path).TrimEnd('\')
$hits = @()
foreach ($key in $Targets.Keys) {
$t = $Targets[$key]
if ($t -eq $full -or $t.StartsWith($full + '\', [StringComparison]::OrdinalIgnoreCase)) {
$hits += $key
}
}
return @($hits | Sort-Object)
}
function Get-LocalCheckouts {
# repo name -> local path, discovered from origin remotes.
$map = @{}
$candidates = @(Get-ChildItem -LiteralPath $FLEET_ROOT -Directory -ErrorAction SilentlyContinue)
# One level deeper too: some projects keep theirs nested.
foreach ($d in @($candidates)) {
$candidates += @(Get-ChildItem -LiteralPath $d.FullName -Directory -ErrorAction SilentlyContinue)
}
foreach ($d in $candidates) {
if (-not (Test-Path (Join-Path $d.FullName ".git"))) { continue }
# A pruned worktree leaves a .git FILE pointing at an admin dir that no
# longer exists, so Test-Path above passes and git then fails. Same
# redirect trap as everywhere else, so keep this on Continue and judge
# by exit code.
$prev = $ErrorActionPreference
$ErrorActionPreference = "Continue"
$url = (git -C $d.FullName remote get-url origin 2>$null)
$ok = ($LASTEXITCODE -eq 0)
$ErrorActionPreference = $prev
if (-not $ok -or -not $url) { continue }
if ($url -match "[:/]$ORG/([^/]+?)(\.git)?$") {
$name = $Matches[1]
if (-not $map.ContainsKey($name)) { $map[$name] = $d.FullName }
}
}
return $map
}
function Get-DefaultBranch {
# origin/HEAD is a LOCAL cache of the remote's default branch. It is written
# at clone time, and repos created some other way (git init + remote add,
# which is how the *-stack and hostops checkouts here were made) simply do
# not have it. `git symbolic-ref` then fails with
# fatal: ref refs/remotes/origin/HEAD is not a symbolic ref
# and - because this script runs under ErrorActionPreference='Stop' - PS 5.1
# turns that redirected stderr into a TERMINATING NativeCommandError. The
# 2>$null does not prevent it; it is the redirect itself that wraps each
# stderr line in an ErrorRecord. So drop to Continue for the native calls.
param([string]$Path)
$prev = $ErrorActionPreference
$ErrorActionPreference = "Continue"
try {
$d = (git -C $Path symbolic-ref --short refs/remotes/origin/HEAD 2>$null) -replace '^origin/', ''
if (-not $d) {
# Repair the cache from the remote, then re-ask. Costs one network
# round-trip on first run per repo and is permanent afterwards.
git -C $Path remote set-head origin --auto 2>$null | Out-Null
$d = (git -C $Path symbolic-ref --short refs/remotes/origin/HEAD 2>$null) -replace '^origin/', ''
}
if (-not $d) {
# Offline, or no such remote. Believe the remote-tracking refs that
# exist rather than assuming "main" - zscripts is on master, and
# guessing wrong makes this script skip the repo with a misleading
# "on 'master', not 'main'".
foreach ($c in @('main', 'master')) {
git -C $Path rev-parse --verify --quiet "refs/remotes/origin/$c" 2>$null | Out-Null
if ($LASTEXITCODE -eq 0) { $d = $c; break }
}
}
if (-not $d) { $d = (git -C $Path rev-parse --abbrev-ref HEAD 2>$null) }
if (-not $d) { $d = "main" }
return $d
}
finally { $ErrorActionPreference = $prev }
}
function Sync-Checkout {
param([string]$Name, [string]$Path, [bool]$DoIt, [string[]]$DeployKeys = @())
# Everything below judges git by $LASTEXITCODE, so drop to Continue for the
# whole function (scoped, auto-reverts on exit).
#
# This is not tidiness. Under the script's ErrorActionPreference='Stop', a
# stderr REDIRECT on a native command makes PS 5.1 wrap each stderr line in
# a terminating ErrorRecord - so `git fetch origin 2>$null` against one
# repo with an unreachable remote killed the ENTIRE sweep mid-list, leaving
# every repo after it unvisited and unreported. A fleet sweep must survive
# one bad repo; that repo gets a FAIL row and the run continues.
$prev = $ErrorActionPreference
$ErrorActionPreference = "Continue"
try {
Sync-CheckoutCore -Name $Name -Path $Path -DoIt $DoIt -DeployKeys $DeployKeys
}
catch {
Write-Host (" {0,-18} FAIL {1}" -f $Name, $_.Exception.Message) -ForegroundColor Red
}
finally { $ErrorActionPreference = $prev }
}
function Sync-CheckoutCore {
param([string]$Name, [string]$Path, [bool]$DoIt, [string[]]$DeployKeys = @())
# Appended to every line: the point is that you never have to remember
# which kind of repo you are looking at.
$mark = if ($DeployKeys.Count -gt 0) { " [zdeploy $($DeployKeys -join ', ')]" } else { " [repo]" }
$branch = (git -C $Path rev-parse --abbrev-ref HEAD 2>$null)
if ($LASTEXITCODE -ne 0 -or -not $branch) {
Write-Host (" {0,-18} FAIL not a usable git checkout: {1}{2}" -f $Name, $Path, $mark) -ForegroundColor Red
return
}
$dirty = @(git -C $Path status --porcelain --untracked-files=no 2>$null)
$default = Get-DefaultBranch -Path $Path
if ($dirty) {
Write-Host (" {0,-18} SKIP uncommitted changes ({1} file(s)) - commit or stash first{2}" -f $Name, $dirty.Count, $mark) -ForegroundColor Yellow
return
}
if ($branch -ne $default) {
Write-Host (" {0,-18} SKIP on '{1}', not '{2}'{3}" -f $Name, $branch, $default, $mark) -ForegroundColor Yellow
return
}
git -C $Path fetch origin --quiet 2>$null
if ($LASTEXITCODE -ne 0) {
# Unreachable remote, renamed repo, dead credential. Say so and move on
# - continuing would compare against stale remote-tracking refs and
# report "already current" about a repo we could not actually reach.
Write-Host (" {0,-18} FAIL cannot fetch origin - check the remote{1}" -f $Name, $mark) -ForegroundColor Red
return
}
$behind = (git -C $Path rev-list --count "HEAD..origin/$default" 2>$null)
$ahead = (git -C $Path rev-list --count "origin/$default..HEAD" 2>$null)
if ([int]$ahead -gt 0) {
Write-Host (" {0,-18} SKIP local '{1}' is {2} commit(s) ahead - diverged, resolve by hand{3}" -f $Name, $default, $ahead, $mark) -ForegroundColor Yellow
return
}
if ([int]$behind -eq 0) {
Write-Host (" {0,-18} ok already current{1}" -f $Name, $mark) -ForegroundColor DarkGray
return
}
if (-not $DoIt) {
Write-Host (" {0,-18} would pull {1} commit(s){2}" -f $Name, $behind, $mark) -ForegroundColor Cyan
return
}
git -C $Path merge --ff-only "origin/$default" --quiet 2>$null
if ($LASTEXITCODE -eq 0) {
Write-Host (" {0,-18} PULLED {1} commit(s) -> {2}{3}" -f $Name, $behind, (git -C $Path rev-parse --short HEAD), $mark) -ForegroundColor Green
# The whole reason the marker exists. A tooling repo is finished here;
# a deployable one now has a checkout ahead of its own server, which is
# the state that gets forgotten.
foreach ($k in $DeployKeys) {
Write-Host (" {0,-18} still on the old build - run: zdeploy {1}" -f "", $k) -ForegroundColor Yellow
}
} else {
Write-Host (" {0,-18} FAILED to fast-forward{1}" -f $Name, $mark) -ForegroundColor Red
}
}
# ── 1. Merge ─────────────────────────────────────────────────────
if (-not $PullOnly) {
Write-Host "`n=== Merging ready PRs (via zmerge) ===" -ForegroundColor Cyan
# Hashtable splatting, not an array. Array splatting passes elements
# positionally, so @("-Repo","<name>") fed "-Repo" into zmerge's
# [int[]]$Exclude and died on the type conversion.
$zm = @{}
if ($Execute) { $zm.Execute = $true }
if ($Yes) { $zm.Yes = $true }
if ($Repo) { $zm.Repo = $Repo }
if ($Only) { $zm.Only = $Only }
if ($Exclude) { $zm.Exclude = $Exclude }
& (Join-Path $PSScriptRoot "zmerge.ps1") @zm
}
# ── 2. Pull ──────────────────────────────────────────────────────
Write-Host "`n=== Bringing local checkouts current ===" -ForegroundColor Cyan
if (-not $Execute) {
Write-Host " (dry run - nothing will be pulled; add -Execute or -e)" -ForegroundColor DarkGray
}
$checkouts = Get-LocalCheckouts
if ($Repo) {
if ($checkouts.ContainsKey($Repo)) { $checkouts = @{ $Repo = $checkouts[$Repo] } }
else { Write-Host " no local checkout found for '$Repo'" -ForegroundColor Yellow; $checkouts = @{} }
}
$targets = Get-DeployTargetsByPath
if ($ReposOnly) {
Write-Host " (-ReposOnly: repos with a zdeploy target are not listed)" -ForegroundColor DarkGray
}
$shown = 0
foreach ($name in ($checkouts.Keys | Sort-Object)) {
$keys = Get-DeployKeysFor -Path $checkouts[$name] -Targets $targets
if ($ReposOnly -and $keys.Count -gt 0) { continue }
$shown++
Sync-Checkout -Name $name -Path $checkouts[$name] -DoIt:$Execute -DeployKeys $keys
}
if ($shown -eq 0) { Write-Host " nothing matched" -ForegroundColor DarkGray }
Write-ZTrailer

View File

@ -1,7 +1,7 @@
REM Evomedia.net — https://github.com/evomedia-net/evo.zscripts REM Evomedia.net — https://github.com/evomedia-net/evo.zscripts
REM Created by Kelly Michels · dev@evomedia.net REM Created by Kelly Michels · dev@evomedia.net
REM Licensed under the MIT License. See LICENSE. REM Licensed under the MIT License. See LICENSE.
REM Version: v1.0.0.0.24 REM Version: v1.0.0.0.28
@echo off @echo off
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zrelease.ps1" %* powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zrelease.ps1" %*

View File

@ -1,7 +1,7 @@
# Evomedia.net — https://github.com/evomedia-net/evo.zscripts # evomedia.net — 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.
# Version: v1.0.0.0.24 # Version: v1.0.0.0.28
# zrelease.ps1 - package the current version as a downloadable zip. # zrelease.ps1 - package the current version as a downloadable zip.
# #

View File

@ -1,7 +1,7 @@
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
REM Created by Kelly Michels · dev@evomedia.net REM Created by Kelly Michels · dev@evomedia.net
REM Licensed under the MIT License. See LICENSE. REM Licensed under the MIT License. See LICENSE.
REM Version: v1.0.0.0.24 REM Version: v1.0.0.0.28
@echo off @echo off
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zrepair.ps1" %* powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zrepair.ps1" %*

View File

@ -1,7 +1,7 @@
# 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.
# Version: v1.0.0.0.24 # Version: v1.0.0.0.28
# zrepair.ps1 — audit and repair container/proxy routing on the server, then smoke test. # zrepair.ps1 — audit and repair container/proxy routing on the server, then smoke test.
# #

View File

@ -1,7 +1,7 @@
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
REM Created by Kelly Michels · dev@evomedia.net REM Created by Kelly Michels · dev@evomedia.net
REM Licensed under the MIT License. See LICENSE. REM Licensed under the MIT License. See LICENSE.
REM Version: v1.0.0.0.24 REM Version: v1.0.0.0.28
@echo off @echo off
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0ZKiller.ps1" %* powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0ZKiller.ps1" %*

View File

@ -1,7 +1,7 @@
# 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.
# Version: v1.0.0.0.24 # Version: v1.0.0.0.28
# zrestart.ps1 — alias for ZKiller.ps1 (kept so both names work). All args pass through. # zrestart.ps1 — alias for ZKiller.ps1 (kept so both names work). All args pass through.
& (Join-Path $PSScriptRoot "ZKiller.ps1") @args & (Join-Path $PSScriptRoot "ZKiller.ps1") @args

View File

@ -1,7 +1,7 @@
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
REM Created by Kelly Michels · dev@evomedia.net REM Created by Kelly Michels · dev@evomedia.net
REM Licensed under the MIT License. See LICENSE. REM Licensed under the MIT License. See LICENSE.
REM Version: v1.0.0.0.24 REM Version: v1.0.0.0.28
@echo off @echo off
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0ZKiller.ps1" -Detached %* powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0ZKiller.ps1" -Detached %*

View File

@ -1,7 +1,7 @@
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
REM Created by Kelly Michels · dev@evomedia.net REM Created by Kelly Michels · dev@evomedia.net
REM Licensed under the MIT License. See LICENSE. REM Licensed under the MIT License. See LICENSE.
REM Version: v1.0.0.0.24 REM Version: v1.0.0.0.28
@echo off @echo off
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zsetup.ps1" %* powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zsetup.ps1" %*

View File

@ -1,7 +1,7 @@
# 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.
# Version: v1.0.0.0.24 # Version: v1.0.0.0.28
# zsetup.ps1 — prepare a project for local dev: create its Python venv and install # zsetup.ps1 — prepare a project for local dev: create its Python venv and install
# dependencies (python kind), or run `npm install` (vite/nextjs). Idempotent - # dependencies (python kind), or run `npm install` (vite/nextjs). Idempotent -

View File

@ -1,7 +1,7 @@
# 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.
# Version: v1.0.0.0.24 # Version: v1.0.0.0.28
# zsetup_mail.ps1 — create admin@ and noreply@ mailboxes in a docker-mailserver # zsetup_mail.ps1 — create admin@ and noreply@ mailboxes in a docker-mailserver
# container on the server, and print the DNS records + SMTP/IMAP settings to use. # container on the server, and print the DNS records + SMTP/IMAP settings to use.

View File

@ -1,7 +1,7 @@
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
REM Created by Kelly Michels · dev@evomedia.net REM Created by Kelly Michels · dev@evomedia.net
REM Licensed under the MIT License. See LICENSE. REM Licensed under the MIT License. See LICENSE.
REM Version: v1.0.0.0.24 REM Version: v1.0.0.0.28
@echo off @echo off
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0ZStart.ps1" %* powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0ZStart.ps1" %*

View File

@ -1,7 +1,7 @@
# 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.
# Version: v1.0.0.0.24 # Version: v1.0.0.0.28
# zstart.ps1 — start local dev servers for any project defined in zconfig.json. # zstart.ps1 — start local dev servers for any project defined in zconfig.json.
# #

View File

@ -1,7 +1,7 @@
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
REM Created by Kelly Michels · dev@evomedia.net REM Created by Kelly Michels · dev@evomedia.net
REM Licensed under the MIT License. See LICENSE. REM Licensed under the MIT License. See LICENSE.
REM Version: v1.0.0.0.24 REM Version: v1.0.0.0.28
@echo off @echo off
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zstart_docker.ps1" %* powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zstart_docker.ps1" %*

View File

@ -1,7 +1,7 @@
# 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.
# Version: v1.0.0.0.24 # Version: v1.0.0.0.28
# zstart_docker.ps1 — bring up a local docker compose stack from <scriptsRoot>\docker\. # zstart_docker.ps1 — bring up a local docker compose stack from <scriptsRoot>\docker\.
# #

View File

@ -1,7 +1,7 @@
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
REM Created by Kelly Michels · dev@evomedia.net REM Created by Kelly Michels · dev@evomedia.net
REM Licensed under the MIT License. See LICENSE. REM Licensed under the MIT License. See LICENSE.
REM Version: v1.0.0.0.24 REM Version: v1.0.0.0.28
@echo off @echo off
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0ZStart.ps1" -Detached %* powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0ZStart.ps1" -Detached %*

View File

@ -1,7 +1,7 @@
# 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.
# Version: v1.0.0.0.24 # Version: v1.0.0.0.28
# zstop.ps1 — stop docker compose stacks on the server without removing data or files. # zstop.ps1 — stop docker compose stacks on the server without removing data or files.
# #

View File

@ -1,7 +1,7 @@
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
REM Created by Kelly Michels · dev@evomedia.net REM Created by Kelly Michels · dev@evomedia.net
REM Licensed under the MIT License. See LICENSE. REM Licensed under the MIT License. See LICENSE.
REM Version: v1.0.0.0.24 REM Version: v1.0.0.0.28
@echo off @echo off
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zsync.ps1" %* powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zsync.ps1" %*

View File

@ -1,7 +1,7 @@
# 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.
# Version: v1.0.0.0.24 # Version: v1.0.0.0.28
# zsync.ps1 — copy new backup files offsite; or build + mirror a vite project's dist. # zsync.ps1 — copy new backup files offsite; or build + mirror a vite project's dist.
# #

View File

@ -1,7 +1,7 @@
REM Evomedia.net — https://github.com/evomedia-net/evo.zscripts REM Evomedia.net — https://github.com/evomedia-net/evo.zscripts
REM Created by Kelly Michels · dev@evomedia.net REM Created by Kelly Michels · dev@evomedia.net
REM Licensed under the MIT License. See LICENSE. REM Licensed under the MIT License. See LICENSE.
REM Version: v1.0.0.0.24 REM Version: v1.0.0.0.28
@echo off @echo off
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zversion.ps1" %* powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zversion.ps1" %*

View File

@ -1,7 +1,7 @@
# Evomedia.net — https://github.com/evomedia-net/evo.zscripts # evomedia.net — 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.
# Version: v1.0.0.0.24 # Version: v1.0.0.0.28
# zversion.ps1 - manage the toolkit version. # zversion.ps1 - manage the toolkit version.
# #