mirror of
https://github.com/kellymichels/zscripts-token-savers
synced 2026-10-08 07:28:18 +00:00
Compare commits
No commits in common. "master" and "v1.0.0.0.19" have entirely different histories.
master
...
v1.0.0.0.1
14
.gitattributes
vendored
14
.gitattributes
vendored
@ -4,18 +4,8 @@
|
||||
*.ps1 text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
|
||||
# Every .txt here is either a generated twin or a manifest, and all of them
|
||||
# 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.
|
||||
# The checksum manifest must stay LF: `sha256sum -c` treats a trailing CR as
|
||||
# part of the filename and reports every entry as missing.
|
||||
CHECKSUMS.txt text eol=lf
|
||||
releases/*.sha256 text eol=lf
|
||||
|
||||
|
||||
51
.github/workflows/rerun-timed-out.yml
vendored
51
.github/workflows/rerun-timed-out.yml
vendored
@ -1,51 +0,0 @@
|
||||
# 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
80
.github/workflows/tests.yml
vendored
@ -1,80 +0,0 @@
|
||||
# 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
3
.gitignore
vendored
@ -19,6 +19,3 @@ md/
|
||||
|
||||
# Pester coverage output (regenerated; never committed)
|
||||
coverage/
|
||||
|
||||
# Generated by scripts/plaintext_twins.py
|
||||
__pycache__/
|
||||
|
||||
223
CHANGELOG.md
223
CHANGELOG.md
@ -1,194 +1,15 @@
|
||||
<!--
|
||||
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
|
||||
Licensed under the MIT License. See LICENSE.
|
||||
-->
|
||||
|
||||
# Changelog
|
||||
|
||||
Notable changes to the evomedia.net Token Savers.
|
||||
Notable changes to the Evomedia.net Token Savers.
|
||||
|
||||
## 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
|
||||
|
||||
### Added
|
||||
- **`zdeploy` can lay the release tag it already knows the number for.**
|
||||
A versioning scheme that asks every release to carry an annotated tag needs
|
||||
something to enforce it, and for a project whose build number lives outside
|
||||
git - in a database, say - nothing did: one project reached thirty-eight
|
||||
builds with four tags, and the missing ones were unrecoverable because the
|
||||
number had never existed anywhere else. With `deploy.tagOnDeploy`, the
|
||||
deployed commit is tagged with its build number and pushed, but only after
|
||||
the live build has been *verified* - a tag is a claim about what is running.
|
||||
Opt-in, because a project that already tags releases through a pull request
|
||||
must not also collect a tag per deploy. It can never fail a deploy: an
|
||||
existing tag is left alone, a failed push keeps the tag local and prints the
|
||||
command to finish it, and a missing repo just says so.
|
||||
|
||||
### Fixed
|
||||
- **The mirror stopped publishing current product names.** Its own denylist
|
||||
never saw the current spellings (a dot or hyphen breaks the word, an
|
||||
underscore hides the boundary), so twelve references went out while the
|
||||
suite ran green. The patterns learn the spellings, planted cases prove it,
|
||||
and the references read generically now.
|
||||
- **`zstart` no longer aborts on a pull that succeeded.** git reports
|
||||
ordinary fetch progress (`From https://...`) on stderr, and under Windows
|
||||
PowerShell 5.1 the script's `2>&1` turned that into a terminating error -
|
||||
so a pull that had *worked* stopped the dev server from starting, before
|
||||
the script's own "Auto-pull skipped" branch could run. The pull now lives
|
||||
in `Invoke-StartGitPull`, which never throws, never switches branch, and
|
||||
never touches a dirty tree: it fast-forwards when it can, reports when it
|
||||
can't, and `zstart` carries on either way - the opposite failure mode
|
||||
from `Invoke-DeployGitPull`, on purpose. Fourteen tests drive real git
|
||||
under `Stop` on 5.1, the host the defect lives on (#130).
|
||||
|
||||
## v1.0.0.0.23 - 2026-08-31
|
||||
|
||||
### Changed
|
||||
- **Every release since 1.0.0 has its own section again** - 21 entries had
|
||||
piled up under `Unreleased` while 22 builds shipped, so this file said
|
||||
nothing had been released since 1.0.0 and a reader at any tag found no
|
||||
section for the version they were holding. Which release carried which
|
||||
entry was derived from git, not guessed: for every line, the commit that
|
||||
introduced it, then the earliest tag containing that commit. No entry text
|
||||
changed - only headings were added and whole entries moved under the
|
||||
release that carried them.
|
||||
- **`scripts/readme_txt.py` is now `scripts/plaintext_twins.py` and covers
|
||||
every markdown file that owes a twin**, `CHANGELOG.txt` included. It was
|
||||
kept by hand, so it drifted the moment this file was reorganised. The
|
||||
renderer also drops `<!-- -->` markers, which are invisible in markdown
|
||||
and read as stray punctuation in a text file.
|
||||
|
||||
### Fixed
|
||||
- **The `--check` that keeps the twins honest is actually run now** -
|
||||
`readme_txt.py` shipped one and its docstring claimed "the test suite runs
|
||||
--check", but nothing invoked it, so a twin could disagree with its
|
||||
markdown indefinitely. `tests/PlainTextTwins.Tests.ps1` runs it, and
|
||||
reports *inconclusive* rather than passing when python is unavailable.
|
||||
|
||||
## v1.0.0.0.22 - 2026-08-31
|
||||
|
||||
### Changed
|
||||
- **The sanitization denylist moved to `tests/sanitization-patterns.psd1`**,
|
||||
so the test suite here and the publisher in the private toolkit read one
|
||||
list instead of keeping two. They had two, and they disagreed: the
|
||||
publisher's scan looked only for secrets, while these rules are about
|
||||
identity — internal project names, product domains, private-only script
|
||||
names, operator paths. It therefore reported "clean" on files this suite
|
||||
rejects. No rule changed; only where they live.
|
||||
|
||||
## v1.0.0.0.21 - 2026-08-31
|
||||
|
||||
### Added
|
||||
- **`tests/VerifyPlan.Tests.ps1`** — the verification channel-selection rules
|
||||
are pure functions in `ZHelpers.ps1` (`Get-VerifyAttempts`,
|
||||
`Get-VerifyTimeout`) and Pester pins them, including "a project with no
|
||||
domain must never produce an edge attempt" and the PowerShell 5.1
|
||||
one-element-unroll trap.
|
||||
|
||||
- **`scripts/readme_txt.py` and `README.txt`** — a generated plain-text twin
|
||||
of the README for terminals and pagers. `README.txt` is generated, never
|
||||
edited by hand.
|
||||
|
||||
### Changed
|
||||
- **`zdeploy` verification picks its channel on every retry, and never asks
|
||||
the bare IP** — the check used to choose its channel once, before the wait
|
||||
loop, by probing; the probes raced the app restart the check exists to wait
|
||||
through, so the whole window went to the edge fallback. For a project with
|
||||
no `domain` that fallback had no `Host` header, and the proxy can then only
|
||||
answer from its **default vhost — a different product**: that is how one
|
||||
deploy's check compared another app's build number against its own. Now
|
||||
channels are re-resolved each retry in trust order (docker-network
|
||||
`viaProxy` → `localhost:<port>` → edge with the project's `Host`), the edge
|
||||
is skipped entirely when there is no host to route by, and a project with
|
||||
no trustworthy channel is reported as unverifiable instead of guessed at.
|
||||
The final warning also says which failure happened: a version that never
|
||||
matched (stale/failed build) reads differently from channels that never
|
||||
answered (probably still booting). `verify.timeoutSeconds` joins the
|
||||
config so a project that is slow to boot — e.g. one that runs database
|
||||
migrations in its entrypoint — can widen its own window instead of
|
||||
warning on every routine success.
|
||||
|
||||
- **An interrupted deploy can no longer destroy server-side `.env` files** —
|
||||
the preserve/restore of operator files is transactional: the restore comes
|
||||
from a tarball taken before the tree is replaced, so a deploy that dies
|
||||
mid-flight leaves the previous files in place instead of an empty
|
||||
directory.
|
||||
|
||||
- **A successful deploy no longer reports failure** — `docker compose
|
||||
restart` writes routine progress to stderr, which PowerShell 5.1 turns
|
||||
into a terminating error under `$ErrorActionPreference = 'Stop'`; four ssh
|
||||
calls bypassed the wrapper that flattens this. All remote steps now run
|
||||
through it and are judged by exit code alone.
|
||||
|
||||
## v1.0.0.0.20 - 2026-08-30
|
||||
|
||||
### Changed
|
||||
- **`zversion bump` is once per *release*, not once per PR** — the usage text
|
||||
and the `bump` help line both said "one per PR, one per defect fix". The
|
||||
build number names something that shipped, so a release carrying five PRs
|
||||
moves it by one; PRs that never shipped on their own were never separate
|
||||
builds. Help text only here, but it is the wording people follow: it stamped
|
||||
a single evo.www release as two builds. The historical entry below, which
|
||||
records what the rule was when `zversion` shipped, is deliberately left as
|
||||
written.
|
||||
|
||||
## v1.0.0.0.19 - 2026-08-30
|
||||
|
||||
### Added
|
||||
- **Read a live build from inside the docker network, not through the public
|
||||
proxy** — `zdeploy`, `zec2` and `zec2online` now prefer
|
||||
@ -208,7 +29,6 @@ Notable changes to the evomedia.net Token Savers.
|
||||
knows a version. Purely additive: projects without those two keys behave
|
||||
exactly as before.
|
||||
|
||||
## v1.0.0.0.14 - 2026-08-20
|
||||
|
||||
### Fixed
|
||||
- **`scp` no longer receives an ssh-only flag** — the stdin-hang fix added
|
||||
@ -223,8 +43,6 @@ Notable changes to the evomedia.net Token Savers.
|
||||
without checking; it now points at `scp`'s own output, where the real
|
||||
diagnosis already was.
|
||||
|
||||
## v1.0.0.0.8 - 2026-08-12
|
||||
|
||||
### Fixed
|
||||
- **Deploys can no longer hang forever on an ssh prompt** — every
|
||||
deploy-path `ssh`/`scp` now carries `BatchMode=yes` plus connect and
|
||||
@ -236,19 +54,16 @@ Notable changes to the evomedia.net Token Savers.
|
||||
there is no prompt on this path worth answering. `ServerAlive*` bounds a
|
||||
session that dies mid-command (dropped VPN, sleeping laptop, rebooting
|
||||
host) to about a minute instead of hanging.
|
||||
|
||||
- **Vendored archives survive the archive filter** — files under a
|
||||
`vendor/` directory are exempt from the "no archives in the zip" rule.
|
||||
A project that vendors a dependency as `vendor/*.tgz` needs it in the
|
||||
deploy zip; dropping it makes a Dockerfile's `COPY vendor ./vendor`
|
||||
fail at image build, a confusing way to learn the filter ate a build
|
||||
input.
|
||||
|
||||
- **`unzip` install is idempotent** — the remote step ran
|
||||
`apt-get update && apt-get install -y unzip` on every deploy; it now
|
||||
checks `command -v unzip` first and skips the apt round-trip when the
|
||||
binary is already there.
|
||||
|
||||
- **`zdeploy` edge kind now ships asset subdirectories** (#42) — the edge
|
||||
deploy uploaded top-level files only, so a project self-hosting assets
|
||||
in folders (`fonts/`, `vendor/`) lost them on every deploy: docker
|
||||
@ -257,8 +72,15 @@ Notable changes to the evomedia.net Token Savers.
|
||||
loading. Every subdirectory except `nginx-logs/` and `.git/` now ships
|
||||
recursively, and the `ensure edge dir` chown is recursive so scp into
|
||||
docker-created root-owned dirs cannot fail.
|
||||
|
||||
## v1.0.0.0.0 - 2026-07-28
|
||||
- **`zdeploy` no longer deletes operator-managed files on deploy** (#2) —
|
||||
the project-directory replacement preserved only `./.env`, silently
|
||||
destroying every other server-side file (`.env.db`, staged signing
|
||||
keys, certs) on every deploy. All `.env*` files at the project root are
|
||||
now preserved by default, plus anything listed in the new
|
||||
`deploy.preserve` array (files or directories); the vite kind, which
|
||||
previously preserved nothing, gets the same protection. Found the hard
|
||||
way: a first production deploy of an auth service wiped its staged DB
|
||||
credentials and RSA signing keys.
|
||||
|
||||
### Added
|
||||
- **Versioned releases: `zversion`, `zrelease`, `releases/`** — the toolkit now
|
||||
@ -272,7 +94,6 @@ Notable changes to the evomedia.net Token Savers.
|
||||
hash verifies the download, the bundled `CHECKSUMS.txt` verifies the
|
||||
extracted contents, so nobody needs to clone the repo to get a verifiable
|
||||
copy. Released zips are immutable — `zrelease` refuses to overwrite one.
|
||||
|
||||
- **`zchecksums` + `CHECKSUMS.txt`** — a SHA-256 manifest covering every `.ps1`
|
||||
and `.cmd`, so a download can be verified before anything is run. `zchecksums`
|
||||
checks them; `zchecksums -Update` regenerates after an intentional edit. The
|
||||
@ -283,18 +104,15 @@ Notable changes to the evomedia.net Token Savers.
|
||||
It's an integrity check, not a signature — the manifest sits in the same repo
|
||||
as the code, so it catches corruption and accidental drift, not a compromised
|
||||
repo. A Pester test fails if the manifest ever goes stale.
|
||||
|
||||
- **Test suite (Pester)** — the toolkit now has automated coverage of its own
|
||||
pure logic: `Get-ArchiveExcludes` (including the deploy-vs-backup rule that
|
||||
keeps `.env`/`uploads` out of deploys but *in* backups), config and project
|
||||
lookups, `remote.composeDir` fallback, EC2 target composition, and build-label
|
||||
formatting. Run with `Invoke-Pester .\tests` (Pester 5+). Verified by mutation
|
||||
testing — reintroducing each historical bug turns the suite red.
|
||||
|
||||
- **`ZCONFIG` environment variable** — overrides the path to `zconfig.json`, so
|
||||
a run can target an alternate config. Also gives the test suite a seam for
|
||||
injecting a fixture.
|
||||
|
||||
- **`zec2_rotatekeys` — safely rotate/reset server-side secrets** — a new
|
||||
tool for when a secret leaks or a deploy overwrites a production `.env`
|
||||
with dev values. `-Rotate KEY` regenerates a key **on the server**
|
||||
@ -307,12 +125,10 @@ Notable changes to the evomedia.net Token Savers.
|
||||
the container (`up -d --force-recreate`, so the new values actually load —
|
||||
a plain restart keeps the old environment). `-WhatIf` previews the plan
|
||||
without touching anything.
|
||||
|
||||
- **`zkill all`** — `zkill` now accepts `all`, stopping the dev server of
|
||||
every project that has a `ports.dev` (edge/docker stacks with no local dev
|
||||
server are skipped). Brings it in line with `zdeploy all` / `zbackup all`;
|
||||
the one-shot "stop everything I've got running locally".
|
||||
|
||||
- **`zdeploy` server-side health verification (`verify` block)** — projects
|
||||
not published through the edge proxy can declare
|
||||
`"verify": { "port": ..., "path": "/health", "expect": "..." }` and the
|
||||
@ -320,19 +136,16 @@ Notable changes to the evomedia.net Token Savers.
|
||||
over SSH) instead of hitting the public IP. Fixes a false PASS where the
|
||||
proxy's default vhost answered for apps that never started; projects
|
||||
with neither `domain` nor `verify` are now reported as NOT verified.
|
||||
|
||||
- **`zdeploy` optional `deploy.gitPull`** — `git pull --ff-only` in the
|
||||
project root before zipping. `zdeploy` zips the working tree and doesn't
|
||||
otherwise pull, so a checkout left behind `origin` after a merged PR would
|
||||
deploy stale code while still bumping the build number — success that
|
||||
changes nothing. A failed pull aborts the deploy instead.
|
||||
|
||||
- **Per-project `start` config block** — `zstart` honors optional pre-start
|
||||
steps from `zconfig.json`: `"gitPull": true` runs `git pull --ff-only` in
|
||||
the project root before starting (never boot a stale checkout), and
|
||||
`"env": { ... }` sets environment variables for the dev-server process.
|
||||
Example added to `zconfig.example.json`.
|
||||
|
||||
- **Switch-style argument tolerance** — a leading dash on a project key is
|
||||
ignored everywhere (`zdeploy -myapp` == `zdeploy myapp`), for hands that
|
||||
grew up on per-project switches.
|
||||
@ -343,33 +156,19 @@ Notable changes to the evomedia.net Token Savers.
|
||||
`all` does what bare invocation used to (matching `zdeploy`). The
|
||||
scheduled task created by `setup_backup_schedule.ps1` passes `all` —
|
||||
re-run it if your task was registered before this change.
|
||||
|
||||
- **`zbackup` parses more `DATABASE_URL` styles** — double/single-quoted
|
||||
values (Prisma convention), `postgres://` and `postgresql+driver://`
|
||||
schemes, and URLs without an explicit port (defaults to 5432) all work;
|
||||
previously these skipped the Postgres dump with "Could not parse
|
||||
DATABASE_URL".
|
||||
|
||||
- **`zkill` / port cleanup kills the whole process tree** — listeners on a
|
||||
project's port are now terminated children-first. Auto-reloading servers
|
||||
(uvicorn/watchfiles, nodemon) spawn workers that inherit the listening
|
||||
socket; killing only the parent left orphans serving stale code.
|
||||
|
||||
- **`zbackup` finds `DATABASE_URL` in `backend\.env` too** — projects with a
|
||||
frontend/backend split get their Postgres dump bundled without needing a
|
||||
root-level `.env`.
|
||||
|
||||
### Fixed
|
||||
- **`zdeploy` no longer deletes operator-managed files on deploy** (#2) —
|
||||
the project-directory replacement preserved only `./.env`, silently
|
||||
destroying every other server-side file (`.env.db`, staged signing
|
||||
keys, certs) on every deploy. All `.env*` files at the project root are
|
||||
now preserved by default, plus anything listed in the new
|
||||
`deploy.preserve` array (files or directories); the vite kind, which
|
||||
previously preserved nothing, gets the same protection. Found the hard
|
||||
way: a first production deploy of an auth service wiped its staged DB
|
||||
credentials and RSA signing keys.
|
||||
|
||||
## 1.0.0
|
||||
|
||||
Initial public release: `zstart` / `zkill` / `zrestart` (local dev servers),
|
||||
|
||||
278
CHANGELOG.txt
278
CHANGELOG.txt
@ -1,240 +1,33 @@
|
||||
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
|
||||
Licensed under the MIT License. See LICENSE.
|
||||
|
||||
Changelog
|
||||
=========
|
||||
|
||||
Notable changes to the evomedia.net Token Savers.
|
||||
Notable changes to the Evomedia.net Token Savers.
|
||||
|
||||
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
|
||||
------------------------
|
||||
|
||||
Added
|
||||
-----
|
||||
- zdeploy can lay the release tag it already knows the number for.
|
||||
A versioning scheme that asks every release to carry an annotated tag needs
|
||||
something to enforce it, and for a project whose build number lives outside
|
||||
git - in a database, say - nothing did: one project reached thirty-eight
|
||||
builds with four tags, and the missing ones were unrecoverable because the
|
||||
number had never existed anywhere else. With deploy.tagOnDeploy, the
|
||||
deployed commit is tagged with its build number and pushed, but only after
|
||||
the live build has been verified - a tag is a claim about what is running.
|
||||
Opt-in, because a project that already tags releases through a pull request
|
||||
must not also collect a tag per deploy. It can never fail a deploy: an
|
||||
existing tag is left alone, a failed push keeps the tag local and prints the
|
||||
command to finish it, and a missing repo just says so.
|
||||
|
||||
Fixed
|
||||
-----
|
||||
- The mirror stopped publishing current product names. Its own denylist
|
||||
never saw the current spellings (a dot or hyphen breaks the word, an
|
||||
underscore hides the boundary), so twelve references went out while the
|
||||
suite ran green. The patterns learn the spellings, planted cases prove it,
|
||||
and the references read generically now.
|
||||
- zstart no longer aborts on a pull that succeeded. git reports
|
||||
ordinary fetch progress (From https://...) on stderr, and under Windows
|
||||
PowerShell 5.1 the script's 2>&1 turned that into a terminating error -
|
||||
so a pull that had worked stopped the dev server from starting, before
|
||||
the script's own "Auto-pull skipped" branch could run. The pull now lives
|
||||
in Invoke-StartGitPull, which never throws, never switches branch, and
|
||||
never touches a dirty tree: it fast-forwards when it can, reports when it
|
||||
can't, and zstart carries on either way - the opposite failure mode
|
||||
from Invoke-DeployGitPull, on purpose. Fourteen tests drive real git
|
||||
under Stop on 5.1, the host the defect lives on (#130).
|
||||
|
||||
v1.0.0.0.23 - 2026-08-31
|
||||
------------------------
|
||||
|
||||
Changed
|
||||
-------
|
||||
- Every release since 1.0.0 has its own section again - 21 entries had
|
||||
piled up under Unreleased while 22 builds shipped, so this file said
|
||||
nothing had been released since 1.0.0 and a reader at any tag found no
|
||||
section for the version they were holding. Which release carried which
|
||||
entry was derived from git, not guessed: for every line, the commit that
|
||||
introduced it, then the earliest tag containing that commit. No entry text
|
||||
changed - only headings were added and whole entries moved under the
|
||||
release that carried them.
|
||||
- **scripts/readme_txt.py is now scripts/plaintext_twins.py and covers
|
||||
every markdown file that owes a twin**, CHANGELOG.txt included. It was
|
||||
kept by hand, so it drifted the moment this file was reorganised. The
|
||||
renderer also drops <!-- --> markers, which are invisible in markdown
|
||||
and read as stray punctuation in a text file.
|
||||
|
||||
Fixed
|
||||
-----
|
||||
- The --check that keeps the twins honest is actually run now -
|
||||
readme_txt.py shipped one and its docstring claimed "the test suite runs
|
||||
--check", but nothing invoked it, so a twin could disagree with its
|
||||
markdown indefinitely. tests/PlainTextTwins.Tests.ps1 runs it, and
|
||||
reports inconclusive rather than passing when python is unavailable.
|
||||
|
||||
v1.0.0.0.22 - 2026-08-31
|
||||
------------------------
|
||||
|
||||
Changed
|
||||
-------
|
||||
- The sanitization denylist moved to tests/sanitization-patterns.psd1,
|
||||
so the test suite here and the publisher in the private toolkit read one
|
||||
list instead of keeping two. They had two, and they disagreed: the
|
||||
publisher's scan looked only for secrets, while these rules are about
|
||||
identity — internal project names, product domains, private-only script
|
||||
names, operator paths. It therefore reported "clean" on files this suite
|
||||
rejects. No rule changed; only where they live.
|
||||
|
||||
v1.0.0.0.21 - 2026-08-31
|
||||
------------------------
|
||||
|
||||
Added
|
||||
-----
|
||||
- tests/VerifyPlan.Tests.ps1 — the verification channel-selection rules
|
||||
are pure functions in ZHelpers.ps1 (Get-VerifyAttempts,
|
||||
Get-VerifyTimeout) and Pester pins them, including "a project with no
|
||||
domain must never produce an edge attempt" and the PowerShell 5.1
|
||||
one-element-unroll trap.
|
||||
|
||||
- scripts/readme_txt.py and README.txt — a generated plain-text twin
|
||||
of the README for terminals and pagers. README.txt is generated, never
|
||||
edited by hand.
|
||||
|
||||
Changed
|
||||
-------
|
||||
- **zdeploy verification picks its channel on every retry, and never asks
|
||||
the bare IP** — the check used to choose its channel once, before the wait
|
||||
loop, by probing; the probes raced the app restart the check exists to wait
|
||||
through, so the whole window went to the edge fallback. For a project with
|
||||
no domain that fallback had no Host header, and the proxy can then only
|
||||
answer from its default vhost — a different product: that is how one
|
||||
deploy's check compared another app's build number against its own. Now
|
||||
channels are re-resolved each retry in trust order (docker-network
|
||||
viaProxy → localhost:<port> → edge with the project's Host), the edge
|
||||
is skipped entirely when there is no host to route by, and a project with
|
||||
no trustworthy channel is reported as unverifiable instead of guessed at.
|
||||
The final warning also says which failure happened: a version that never
|
||||
matched (stale/failed build) reads differently from channels that never
|
||||
answered (probably still booting). verify.timeoutSeconds joins the
|
||||
config so a project that is slow to boot — e.g. one that runs database
|
||||
migrations in its entrypoint — can widen its own window instead of
|
||||
warning on every routine success.
|
||||
|
||||
- An interrupted deploy can no longer destroy server-side .env files —
|
||||
the preserve/restore of operator files is transactional: the restore comes
|
||||
from a tarball taken before the tree is replaced, so a deploy that dies
|
||||
mid-flight leaves the previous files in place instead of an empty
|
||||
directory.
|
||||
|
||||
- A successful deploy no longer reports failure — `docker compose
|
||||
restart` writes routine progress to stderr, which PowerShell 5.1 turns
|
||||
into a terminating error under $ErrorActionPreference = 'Stop'; four ssh
|
||||
calls bypassed the wrapper that flattens this. All remote steps now run
|
||||
through it and are judged by exit code alone.
|
||||
|
||||
v1.0.0.0.20 - 2026-08-30
|
||||
------------------------
|
||||
|
||||
Changed
|
||||
-------
|
||||
- **zversion bump is once per release, not once per PR** — the usage text
|
||||
and the bump help line both said "one per PR, one per defect fix". The
|
||||
build number names something that shipped, so a release carrying five PRs
|
||||
moves it by one; PRs that never shipped on their own were never separate
|
||||
builds. Help text only here, but it is the wording people follow: it stamped
|
||||
a single evo.www release as two builds. The historical entry below, which
|
||||
records what the rule was when zversion shipped, is deliberately left as
|
||||
written.
|
||||
|
||||
v1.0.0.0.19 - 2026-08-30
|
||||
------------------------
|
||||
|
||||
Added
|
||||
-----
|
||||
- **Read a live build from inside the docker network, not through the public
|
||||
proxy** — zdeploy, zec2 and zec2online now prefer
|
||||
- Read a live build from inside the docker network, not through the public
|
||||
proxy — zdeploy, zec2 and zec2online now prefer
|
||||
docker exec <viaProxy> curl http://<upstream>/api/build-version when a
|
||||
project sets verify.viaProxy and verify.upstream.
|
||||
Two problems it closes. A build stamp is something many sites
|
||||
deliberately do not serve publicly, and a checker that reads it over the
|
||||
public URL stops working the moment that endpoint is blocked —
|
||||
reporting "unknown", which is indistinguishable from "could not reach
|
||||
it". And the proxy answers from whichever vhost matches the Host header,
|
||||
so a container with no public route was getting another site's version
|
||||
back and failing deploys that had worked.
|
||||
Reading it from a container on the shared network also exercises the
|
||||
real HTTP path, so it proves the app is serving rather than that its
|
||||
database knows a version. Purely additive: projects without those two
|
||||
keys behave exactly as before.
|
||||
|
||||
Two problems it closes. A build stamp is something many sites deliberately
|
||||
do not serve publicly, and a checker that reads it over the public URL stops
|
||||
working the moment that endpoint is blocked — reporting "unknown", which is
|
||||
indistinguishable from "could not reach it". And the proxy answers from
|
||||
whichever vhost matches the Host header, so a container with no public route
|
||||
was getting another site's version back and failing deploys that had
|
||||
worked.
|
||||
|
||||
Reading it from a container on the shared network also exercises the real
|
||||
HTTP path, so it proves the app is serving rather than that its database
|
||||
knows a version. Purely additive: projects without those two keys behave
|
||||
exactly as before.
|
||||
|
||||
v1.0.0.0.14 - 2026-08-20
|
||||
------------------------
|
||||
|
||||
Fixed
|
||||
-----
|
||||
- scp no longer receives an ssh-only flag — the stdin-hang fix added
|
||||
-n to Get-Ec2SshOpts, and the deploy path splats that same array into
|
||||
scp as well as ssh. OpenSSH's scp has no -n: it exits 1 with
|
||||
@ -247,8 +40,6 @@ Fixed
|
||||
without checking; it now points at scp's own output, where the real
|
||||
diagnosis already was.
|
||||
|
||||
v1.0.0.0.8 - 2026-08-12
|
||||
-----------------------
|
||||
|
||||
Fixed
|
||||
-----
|
||||
@ -262,19 +53,16 @@ Fixed
|
||||
there is no prompt on this path worth answering. ServerAlive* bounds a
|
||||
session that dies mid-command (dropped VPN, sleeping laptop, rebooting
|
||||
host) to about a minute instead of hanging.
|
||||
|
||||
- Vendored archives survive the archive filter — files under a
|
||||
vendor/ directory are exempt from the "no archives in the zip" rule.
|
||||
A project that vendors a dependency as vendor/*.tgz needs it in the
|
||||
deploy zip; dropping it makes a Dockerfile's COPY vendor ./vendor
|
||||
fail at image build, a confusing way to learn the filter ate a build
|
||||
input.
|
||||
|
||||
- unzip install is idempotent — the remote step ran
|
||||
apt-get update && apt-get install -y unzip on every deploy; it now
|
||||
checks command -v unzip first and skips the apt round-trip when the
|
||||
binary is already there.
|
||||
|
||||
- zdeploy edge kind now ships asset subdirectories (#42) — the edge
|
||||
deploy uploaded top-level files only, so a project self-hosting assets
|
||||
in folders (fonts/, vendor/) lost them on every deploy: docker
|
||||
@ -283,9 +71,15 @@ Fixed
|
||||
loading. Every subdirectory except nginx-logs/ and .git/ now ships
|
||||
recursively, and the ensure edge dir chown is recursive so scp into
|
||||
docker-created root-owned dirs cannot fail.
|
||||
|
||||
v1.0.0.0.0 - 2026-07-28
|
||||
-----------------------
|
||||
- zdeploy no longer deletes operator-managed files on deploy (#2) —
|
||||
the project-directory replacement preserved only ./.env, silently
|
||||
destroying every other server-side file (.env.db, staged signing
|
||||
keys, certs) on every deploy. All .env* files at the project root are
|
||||
now preserved by default, plus anything listed in the new
|
||||
deploy.preserve array (files or directories); the vite kind, which
|
||||
previously preserved nothing, gets the same protection. Found the hard
|
||||
way: a first production deploy of an auth service wiped its staged DB
|
||||
credentials and RSA signing keys.
|
||||
|
||||
Added
|
||||
-----
|
||||
@ -300,7 +94,6 @@ Added
|
||||
hash verifies the download, the bundled CHECKSUMS.txt verifies the
|
||||
extracted contents, so nobody needs to clone the repo to get a verifiable
|
||||
copy. Released zips are immutable — zrelease refuses to overwrite one.
|
||||
|
||||
- zchecksums + CHECKSUMS.txt — a SHA-256 manifest covering every .ps1
|
||||
and .cmd, so a download can be verified before anything is run. zchecksums
|
||||
checks them; zchecksums -Update regenerates after an intentional edit. The
|
||||
@ -311,18 +104,15 @@ Added
|
||||
It's an integrity check, not a signature — the manifest sits in the same repo
|
||||
as the code, so it catches corruption and accidental drift, not a compromised
|
||||
repo. A Pester test fails if the manifest ever goes stale.
|
||||
|
||||
- Test suite (Pester) — the toolkit now has automated coverage of its own
|
||||
pure logic: Get-ArchiveExcludes (including the deploy-vs-backup rule that
|
||||
keeps .env/uploads out of deploys but in backups), config and project
|
||||
lookups, remote.composeDir fallback, EC2 target composition, and build-label
|
||||
formatting. Run with Invoke-Pester .\tests (Pester 5+). Verified by mutation
|
||||
testing — reintroducing each historical bug turns the suite red.
|
||||
|
||||
- ZCONFIG environment variable — overrides the path to zconfig.json, so
|
||||
a run can target an alternate config. Also gives the test suite a seam for
|
||||
injecting a fixture.
|
||||
|
||||
- zec2_rotatekeys — safely rotate/reset server-side secrets — a new
|
||||
tool for when a secret leaks or a deploy overwrites a production .env
|
||||
with dev values. -Rotate KEY regenerates a key on the server
|
||||
@ -335,12 +125,10 @@ Added
|
||||
the container (up -d --force-recreate, so the new values actually load —
|
||||
a plain restart keeps the old environment). -WhatIf previews the plan
|
||||
without touching anything.
|
||||
|
||||
- zkill all — zkill now accepts all, stopping the dev server of
|
||||
every project that has a ports.dev (edge/docker stacks with no local dev
|
||||
server are skipped). Brings it in line with zdeploy all / zbackup all;
|
||||
the one-shot "stop everything I've got running locally".
|
||||
|
||||
- zdeploy server-side health verification (verify block) — projects
|
||||
not published through the edge proxy can declare
|
||||
"verify": { "port": ..., "path": "/health", "expect": "..." } and the
|
||||
@ -348,19 +136,16 @@ Added
|
||||
over SSH) instead of hitting the public IP. Fixes a false PASS where the
|
||||
proxy's default vhost answered for apps that never started; projects
|
||||
with neither domain nor verify are now reported as NOT verified.
|
||||
|
||||
- zdeploy optional deploy.gitPull — git pull --ff-only in the
|
||||
project root before zipping. zdeploy zips the working tree and doesn't
|
||||
otherwise pull, so a checkout left behind origin after a merged PR would
|
||||
deploy stale code while still bumping the build number — success that
|
||||
changes nothing. A failed pull aborts the deploy instead.
|
||||
|
||||
- Per-project start config block — zstart honors optional pre-start
|
||||
steps from zconfig.json: "gitPull": true runs git pull --ff-only in
|
||||
the project root before starting (never boot a stale checkout), and
|
||||
"env": { ... } sets environment variables for the dev-server process.
|
||||
Example added to zconfig.example.json.
|
||||
|
||||
- Switch-style argument tolerance — a leading dash on a project key is
|
||||
ignored everywhere (zdeploy -myapp == zdeploy myapp), for hands that
|
||||
grew up on per-project switches.
|
||||
@ -372,34 +157,19 @@ Changed
|
||||
all does what bare invocation used to (matching zdeploy). The
|
||||
scheduled task created by setup_backup_schedule.ps1 passes all —
|
||||
re-run it if your task was registered before this change.
|
||||
|
||||
- zbackup parses more DATABASE_URL styles — double/single-quoted
|
||||
values (Prisma convention), postgres:// and postgresql+driver://
|
||||
schemes, and URLs without an explicit port (defaults to 5432) all work;
|
||||
previously these skipped the Postgres dump with "Could not parse
|
||||
DATABASE_URL".
|
||||
|
||||
- zkill / port cleanup kills the whole process tree — listeners on a
|
||||
project's port are now terminated children-first. Auto-reloading servers
|
||||
(uvicorn/watchfiles, nodemon) spawn workers that inherit the listening
|
||||
socket; killing only the parent left orphans serving stale code.
|
||||
|
||||
- zbackup finds DATABASE_URL in backend\.env too — projects with a
|
||||
frontend/backend split get their Postgres dump bundled without needing a
|
||||
root-level .env.
|
||||
|
||||
Fixed
|
||||
-----
|
||||
- zdeploy no longer deletes operator-managed files on deploy (#2) —
|
||||
the project-directory replacement preserved only ./.env, silently
|
||||
destroying every other server-side file (.env.db, staged signing
|
||||
keys, certs) on every deploy. All .env* files at the project root are
|
||||
now preserved by default, plus anything listed in the new
|
||||
deploy.preserve array (files or directories); the vite kind, which
|
||||
previously preserved nothing, gets the same protection. Found the hard
|
||||
way: a first production deploy of an auth service wiped its staged DB
|
||||
credentials and RSA signing keys.
|
||||
|
||||
1.0.0
|
||||
-----
|
||||
|
||||
|
||||
@ -1,45 +1,42 @@
|
||||
6e95736007b3906950d95a830705ac1118ebfc11836d2c717d6dd49cb3157966 setup_backup_schedule.ps1
|
||||
d6ab2ddb273a8ca870bd0673cffdd0907f16f4718367b4413a3e34bb1f1769c8 token-count.ps1
|
||||
30ccf43c371ae95cecd5b443d9214ac8ea71ec83be94bc15bcf359beefb8dee1 zbackup.cmd
|
||||
b3549b346c90a8ae5a05c4c91b7ac370f72467c4cb0d019edcd0ea80b994393e zbackup.ps1
|
||||
b6e5e15f8e2b128219c7b8b9db3e9b7804eee60fd6aed43f184210048e518f3c zbackup_and_sync.ps1
|
||||
b407ef5b298cd6fe94af492e9254b4447e34333d22f8c29d3e9d4d2881651cf3 zbackup_ec2.cmd
|
||||
55df5f2e19e81317b1e33b7b030b252fc4d6f49c0c77229d9e4e86affeb3b8ef zbackup_ec2.ps1
|
||||
64bc3148b09e422ffe4890339fade2354a31f2c7ba004f2728c29a0a39c32f08 zchecksums.cmd
|
||||
3e1c573e446238cae494d22d25376f278333dc25fdbc0a5abbdaa9349722b438 zchecksums.ps1
|
||||
049f8e79fc8c3d8d96ae8ccb1155d191e2dbf2753f4d4d73ceca00aa1ab0481b zdeploy.cmd
|
||||
d2ead96436507c8efedc3c72045e623137a1f844940c4ff57c9c7ebe67645511 zdeploy.ps1
|
||||
30918a9260cb6220d6e2140edcf319fc0fa1e75b7fe36d0efe68b0d10cdde241 zec2.cmd
|
||||
695ec87ea6518933ee64524d369f1d0d5128bf6e54db31f555a888bdddcb9326 zec2.ps1
|
||||
ce3209ed8eaab242286e76aeb11b1249239e2804e3de456a1609cc7caa780b43 zec2_rotatekeys.cmd
|
||||
f58277779b5c54814c29dda55ab567960e305fa9ef515b1206076be97f623093 zec2_rotatekeys.ps1
|
||||
30ed73711eeddb518e02eb5e1968b7cc1dad51f27ecf42155b515b097487c3f3 zec2online.cmd
|
||||
1e8814928910ff4f3da59478511feb75280ba2e12111840fb47d2c5504741f0f zec2online.ps1
|
||||
4ea81dc9e3cafc514b1e8224e633bf22bd88bfdc6e53545ff88a2a2d33715b4f ZHelpers.ps1
|
||||
d97f9b5c68526c3295796b3042ee86194721eacab56b0efa0f3fdf16c881b0ea zkill.cmd
|
||||
587de0b176788de917e713915ac468e44bf00cf9c9c7d6ea9b88aaec41ad6ea7 zkill.ps1
|
||||
66556f55688d1f31e890d2b36143d3d0e2f5fcdded562e3d0a9591c02e827cda ZKiller.ps1
|
||||
2c9e0fa5dabb1544b6600cb60e5f66e89c787f217db246553b830152ab976ec3 ZKillOnly.ps1
|
||||
479d01a4c962eb1dba2dbfee913c3704048f29581a0a06d068c0fb9145c3a51c zmerge.ps1
|
||||
dd1cfde33aec53fc0df4a34e45704a59bf48e094fcafcc0ec49e9d13aa442b6f zpull.cmd
|
||||
9e746ad18b92ee7344061847b38c870f280421cf55d31e36a72a1514772e53a2 zpull.ps1
|
||||
4a57e270fc75ae5419bd27cf01b7dd6d8965be8dd58e0c80af985cb7bf27bdc5 zrelease.cmd
|
||||
a6906e118f13442340cc4e0b14d7523d63e85d2bdb0eff8fd3a7c25567af8962 zrelease.ps1
|
||||
58e5b604cc260af7e644412094ff6e3bf799f80f9e52b9a83a044299f7704107 zrepair.cmd
|
||||
186f7b0fb72808466ece328ea1a4e3a97bb2d4bae9f204ff7055e8b7f009614b zrepair.ps1
|
||||
d97c9cf55b94c53dba49471d13f5c9870f2f50627ac5732ac4b60222da936244 zrestart.cmd
|
||||
32a8484d356c5f740d7a12f18bf0dd51a0c7da66010f6a6f553d976484315eef zrestart.ps1
|
||||
2ebf37f72323cac3963a4a95d1ae414fecf4da1e1e618147663c6cf94d1ece24 zrestartd.cmd
|
||||
0a15ad0d341a5efdcd4c14e22e91d4b45f2bf87d0c5608fcf985200f6797bab4 zsetup.cmd
|
||||
0d3042c5e44c3edf396005d0177f744c0c29536c25a50743e93a92156aa2a96c zsetup.ps1
|
||||
498ad7f8a7a56b332fede299e7172211ba360e76d9244c6c93f7cceae8a58619 zsetup_mail.ps1
|
||||
46b3782d9b05649bdb1bcbd2a007bf6abc889abee420ac2f6e75530fa45a4694 zstart.cmd
|
||||
4b50cadf761f285f4fe655b143e04ec6b3565646ee8fb1ed165c234a201802b7 zstart.ps1
|
||||
2d74dcca3d4f51a28c84fbef5cc134126b61dfcc8ffb35e2d72f9596bf685809 zstart_docker.cmd
|
||||
5016654d9ad68e11b85594be5e05318e19c5c6154174f734c43251970bc2086f zstart_docker.ps1
|
||||
1e1c5990e1dba41a165e797790f73cd33fcee5035eec65dd0d2610ea8b56a023 zstartd.cmd
|
||||
a894876ad9fa1bfac6cacfec2a837914debb773e05b975d9b3cdf56589dd8ed2 zstop.ps1
|
||||
2e165d35d9915099b98a14329c9eb2774b2bf2979c937bc3843d95b5b16400e7 zsync.cmd
|
||||
5d008cc252b978a9f873101139054988dff24562655bdfcd22e781aa5c14029a zsync.ps1
|
||||
d55020eeb926e7190d06cc3de3e4a0d0656e8c73e8a89032c06ffe29c0b661eb zversion.cmd
|
||||
25de4c34219edb98b583be639b798b27c353e978a81f0480d49ba9d965f70f0b zversion.ps1
|
||||
0d890eeecb66a0f90331ad01449828b2d64892105788bae783d625be10f1a6d4 setup_backup_schedule.ps1
|
||||
97bdb5859643c182b30b429fa516c9a8fab99cd587329b3a52315edf03baae8a token-count.ps1
|
||||
18dca87bec5ac095681f5db5d5366539435b606866fbec59ed1c687902a3bb1c zbackup.cmd
|
||||
5e0ee0b9016cb515b83938aa9fda69c4826f6ece881dfa118e2228a3c1ae84ce zbackup.ps1
|
||||
e8e2f0ce7759c994c2a5db55751a312a03ab794699a3ca707896fad804dec09b zbackup_and_sync.ps1
|
||||
87ad8963f3bc6044f51b6d1961588e77c7d830a0cd977a45ffd99c01cbadba00 zbackup_ec2.cmd
|
||||
b2b867d4ef36c988fa86bd765a62ffa106a49a9f70ea0cc0543823b1b9a02c3b zbackup_ec2.ps1
|
||||
6f6990f3e2a989840b0e65b02550e0a984675d5929427d7d42d79bc58d166899 zchecksums.cmd
|
||||
d093e61d75565e15cd40ebd28a053a2e9b1311e4b7dbcf51055709070cba5746 zchecksums.ps1
|
||||
6ffe7af1925428b558eaf753a8feaf01927f9f6087079cc7bccf537357cc5da1 zdeploy.cmd
|
||||
ad0773b227eb11b880ef111758869e2e02e31701f9990365e6d4e0a8a48a9280 zdeploy.ps1
|
||||
13e08c16a0e53538e0e8ae1a4af844c4edfe9ef531067a3329dbe97b9be04c86 zec2.cmd
|
||||
f691c11264f539e4f9a9fc221a253ffb8dd90dc5d05dc5bfa80eaa7de0530e30 zec2.ps1
|
||||
41c44f0221f12041c04128d00892de20eb30e7a683cda754de41b92b797370c0 zec2_rotatekeys.cmd
|
||||
51fe6478dde1c7a7b7808452d98ca5a413caec8dfeb0265f7b6d39668fd07e3a zec2_rotatekeys.ps1
|
||||
655b21ab476151cf39b66ef3ce3dd4a0d66f4a80a85fd2b285a73aa35fdcd00d zec2online.cmd
|
||||
a210794f0538fe9680743e837407b035ab529a48dc76aace6e807c26b9bf4369 zec2online.ps1
|
||||
f21dbe173e88435a7712b390f27cdfa269c55817489a44c4a4b35b9586b87899 ZHelpers.ps1
|
||||
da2c456b20c97791d421caa4d03152eff90f631f137c2dc79e7d85c0874fb384 zkill.cmd
|
||||
0b1cae425ca373b91349184865c6331b5a16d6004d01906df2071709a180d4e4 zkill.ps1
|
||||
8aa7dac20298c1d3e7ccc8cf310ea398d67525b4a4c3256f6c6b2aca0cfb4032 ZKiller.ps1
|
||||
43c139b6d7b9c4229c80ba411d417518293be513d6909773658166c9f0e3f282 ZKillOnly.ps1
|
||||
c2ca2a00aa356b5c84ae09e9a3ddabb73332d58ef842a6473267c2543e9e2076 zrelease.cmd
|
||||
0d63fdc920d00234f0d32055839b9aae981c23d0deb4cba87129739507e82a58 zrelease.ps1
|
||||
bd16a106f02a2a4f6b0727f44811dc5e69cca162b4205e29d9369b74c9dacecc zrepair.cmd
|
||||
30930b29453dab93bc216804f5c616b61297b4a92d5e998df8df3fe38ac526c4 zrepair.ps1
|
||||
1de5d1b39f621525e7700741224139faecff09e45b035b0192e1608f39a8481e zrestart.cmd
|
||||
05db57446ab9ed2d3ab78016da1616dea8d86a6073d6efb42e8dab66ce31639b zrestart.ps1
|
||||
49403374fe9014f2e99ba643a3a6ed08021bd605db9a0ccdaac752b89412296e zrestartd.cmd
|
||||
cb933980235ee6030c64fd5f7ebef377f799d0d68449f7f17694d48e501b9710 zsetup.cmd
|
||||
f17767ca31ad2123ed9a168f2b129aeaf7dbe0d84b06d4748f6e049e5c95835f zsetup.ps1
|
||||
3e705f2520c155eec8318117daa30edbee36cd5a7e90cb2b1674bb502b289dca zsetup_mail.ps1
|
||||
2e8dbc61bda93e3e0b1e79afef43ac84f4f181677312c8ccff3f5eaf6f12e41f zstart.cmd
|
||||
587af8bcb48a8750197ec3885e6786b8b2d562de3df0b810a1462476f0211eee zstart.ps1
|
||||
e2f807591602f3b0ff01646135dba964fa6866d112044c566276a6f4372c9e5d zstart_docker.cmd
|
||||
b67f06bf63a0413c53591960b0d1452ce831cd34f553fcf050e85527594a00ef zstart_docker.ps1
|
||||
c73301f2b2074ac8ce75b6f3e5cd3644a74d8f661749469f9bea038bc4ec8b40 zstartd.cmd
|
||||
6e21310d9ff354a34b926cc8bed083713b725ff9239cddf9e2a473f303027b7b zstop.ps1
|
||||
5e0a47360b951625883762abfc4c9a9de35241d6277c423894a01119c04a3f82 zsync.cmd
|
||||
130a71a17eedcdff72621cfaeb13e7e0ee906c793a3af2c643eecffebc4cd276 zsync.ps1
|
||||
0b13c6c8bbca0fbd75d8983121de37091573578ffff58873ac926c2591f8c299 zversion.cmd
|
||||
68edfabca7f99822d96da1e5b56d6f9f3a06e46f4786fe28d51b7519aa4ebc4c zversion.ps1
|
||||
|
||||
@ -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
|
||||
Licensed under the MIT License. See LICENSE.
|
||||
-->
|
||||
|
||||
@ -1,38 +0,0 @@
|
||||
evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
Created by Kelly Michels · dev@evomedia.net
|
||||
Licensed under the MIT License. See LICENSE.
|
||||
|
||||
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.
|
||||
41
README.md
41
README.md
@ -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
|
||||
Licensed under the MIT License. See LICENSE.
|
||||
-->
|
||||
@ -91,7 +91,6 @@ Set the `ZCONFIG` environment variable to point at a config somewhere else — h
|
||||
"deploy": {
|
||||
"zipName": "MyAppDeploy.zip", // optional: defaults to <key>Deploy.zip
|
||||
"gitPull": true, // optional: git pull --ff-only before zipping
|
||||
"tagOnDeploy": false, // optional: tag the deployed commit with its build number
|
||||
"exclude": ["docs", "big-data-folder"] // optional: extra top-level dirs/files to skip
|
||||
}
|
||||
}
|
||||
@ -102,10 +101,9 @@ Set the `ZCONFIG` environment variable to point at a config somewhere else — h
|
||||
Optional blocks do real work:
|
||||
|
||||
- **`install`** — how `zsetup` installs a python project's dependencies into its `.venv`: the pip args, e.g. `"-e ."`, `"-e backend"` (deps in a subfolder), or `"-r requirements.txt"`. Omit it and `zsetup` auto-detects a root `pyproject.toml`/`setup.py` (`-e .`) or `requirements.txt` (`-r requirements.txt`). `zstart` never installs — run `zsetup <key>` once, then `zstart <key>`.
|
||||
- **`start`** — pre-start steps for `zstart`: `gitPull: true` fast-forwards the checkout from its upstream first, and when it can't — no upstream, diverged history, a remote that wants credentials — says so and starts the server anyway. A pull is never allowed to stand between you and a running dev server (that is `deploy.gitPull`'s job, below, where refusing is correct). `env` sets environment variables for the dev-server process (feature flags, reload switches).
|
||||
- **`start`** — pre-start steps for `zstart`: `gitPull: true` runs `git pull --ff-only` in the project root first (never starts a stale checkout), and `env` sets environment variables for the dev-server process (feature flags, reload switches).
|
||||
- **`db`** — deploys wait for `pg_isready` and `zbackup_ec2` pulls a `pg_dump`, both against the compose service named `db`. Omit it and those steps are skipped cleanly.
|
||||
- **`deploy.gitPull`** — `git pull --ff-only` in the project root before zipping, so a merged PR actually ships. Since `zdeploy` zips your working tree, a checkout left behind `origin` would otherwise deploy stale code *and still bump the build number* — a silent no-op that looks like success. A failed pull (dirty tree that conflicts, diverged history) aborts the deploy rather than shipping uncertain code.
|
||||
- **`deploy.tagOnDeploy`** — after a deploy whose live build number has been *verified*, lay an annotated git tag for that number on the deployed commit and push it. Off by default, and deliberately opt-in: a project that already tags its releases through a pull request must not also collect a tag per deploy, because a release ledger and a deploy counter are two different numbers. Nothing here can fail a deploy — an existing tag is left alone, a failed push leaves the tag local and tells you the command to finish it, and a working tree with uncommitted changes gets a warning that the tagged commit is not everything that shipped.
|
||||
- **`migrations": "prisma"`** — runs `npx prisma migrate deploy` inside the app container after each deploy.
|
||||
- **Compose service-name conventions** — handlers assume the app service is named `app` (python) or `web` (nextjs) and the database service `db`. Override the app service with `remote.appService`.
|
||||
- **Edge extras** — an `edge`-kind project can set `proxyContainer` (the nginx container's name, used for reloads and stale-container cleanup) and `certsSource` (a host path with TLS certs, mounted read-only when validating `nginx.conf`).
|
||||
@ -136,8 +134,6 @@ The `.cmd` wrappers are the everyday interface. Every command takes one or more
|
||||
| `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) |
|
||||
| `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
|
||||
|
||||
@ -382,21 +378,6 @@ Give the project a `verify` block instead, and `zdeploy` checks the app **from t
|
||||
|
||||
`port` is the host port the app publishes on the server; `path` defaults to `/`; `expect` is an optional substring the response must contain (an app version string makes this equivalent to build-number verification). Python-kind projects use `verify` automatically when there's no `build_version_tool.py` — and projects with *neither* a `domain` nor a `verify` block are now honestly reported as **NOT verified** instead of green-lighting the proxy's default page.
|
||||
|
||||
Two more keys make the check unambiguous and patient:
|
||||
|
||||
```json
|
||||
"verify": {
|
||||
"port": 8005, "path": "/health",
|
||||
"viaProxy": "my_edge_proxy", "upstream": "myapp_container:8000",
|
||||
"timeoutSeconds": 120
|
||||
}
|
||||
```
|
||||
|
||||
- **`viaProxy` + `upstream`** — read the version **over the docker network**, by exec-ing a curl inside the named container (usually the edge proxy, since it is on every stack's network). This is the most trustworthy channel there is: it cannot answer from the wrong product, and it works for apps that publish no host port at all.
|
||||
- **`timeoutSeconds`** — how long verification may wait. An app that runs database migrations in its entrypoint exceeds a 30-second window *on every deploy that ships one*, and a warning that fires on routine success teaches you to ignore the one that matters.
|
||||
|
||||
Verification walks its channels in trust order — docker-network `viaProxy`, then `localhost:<port>`, then the edge with the project's own `Host` header — and **re-picks the channel on every retry**. That last part is the point: the check runs while the app is restarting, which is exactly when the good channels are briefly down, and locking the choice in up front is how a whole verification window gets spent asking the wrong thing. A project with no domain is **never** asked for via the edge: without a `Host` header the proxy can only answer from its default vhost — a different product — and no answer beats somebody else's answer.
|
||||
|
||||
`zec2` and `zec2online` use these same endpoints to show what's live and flag local/server version drift.
|
||||
|
||||
---
|
||||
@ -411,24 +392,6 @@ 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:
|
||||
|
||||
**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
|
||||
sha256sum -c zscripts-v1.0.0.0.0.zip.sha256 # verify the download
|
||||
unzip zscripts-v1.0.0.0.0.zip -d zscripts # extract
|
||||
|
||||
454
README.txt
454
README.txt
@ -1,454 +0,0 @@
|
||||
evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
Created by Kelly Michels · dev@evomedia.net
|
||||
Licensed under the MIT License. See LICENSE.
|
||||
|
||||
zscripts Token Savers
|
||||
=====================
|
||||
|
||||
When an AI coding agent orchestrates your infrastructure — starting dev servers, deploying to EC2, diagnosing 502s — it spends hundreds to thousands of tokens per operation on SSH plumbing, Docker output, and retry logic. Those tokens should go to code.
|
||||
|
||||
Token Savers gives you short, one-word commands to run those parts yourself: zdeploy myapp, zrepair myapp, zstart myapp. You handle the deterministic infrastructure; your agent handles code. Running these scripts manually instead of asking your agent to orchestrate them keeps measured script output out of your agent's context window — ~26,500 tokens per active development day at a typical run cadence. Per-run figures are measured; the daily total applies typical run counts. See TOKEN_SAVINGS.md (TOKEN_SAVINGS.md) for the numbers and method.
|
||||
|
||||
Every command is a tiny PowerShell script driven by a single JSON config file. The project key you define in that config is the command argument — add myapp to the config and zstart myapp, zdeploy myapp, zbackup myapp all just work, no script edits needed.
|
||||
|
||||
Requirements: Windows, PowerShell 5.1+, OpenSSH client (ssh/scp, ships with Windows 10/11), and Docker + docker compose on the remote host for the deploy scripts.
|
||||
|
||||
---
|
||||
|
||||
Install
|
||||
-------
|
||||
|
||||
No installer. Clone the repo and add the folder to your PATH:
|
||||
|
||||
git clone https://github.com/evomedia-net/evo.zscripts.git C:\tools\zscripts
|
||||
|
||||
# Add to your user PATH (new terminals pick it up automatically)
|
||||
[Environment]::SetEnvironmentVariable(
|
||||
"Path",
|
||||
[Environment]::GetEnvironmentVariable("Path", "User") + ";C:\tools\zscripts",
|
||||
"User"
|
||||
)
|
||||
|
||||
Open a new terminal and every command below works from any directory. The .cmd wrappers invoke PowerShell with -ExecutionPolicy Bypass, so no execution-policy changes are needed — zstart myapp just works from cmd, PowerShell, or a VS Code terminal.
|
||||
|
||||
---
|
||||
|
||||
Configure
|
||||
---------
|
||||
|
||||
All machine-specific values (server IP, SSH user and key, folder paths, project definitions) live in one file: zconfig.json. It is gitignored — your secrets never leave your machine.
|
||||
|
||||
cd C:\tools\zscripts
|
||||
copy zconfig.example.json zconfig.json
|
||||
notepad zconfig.json
|
||||
|
||||
The example config ships with sample projects named by their kind — pyapp, viteapp, nextapp, edge, analytics. Rename the keys to your own project names; the key is what you type as the command argument. Add as many projects as you like — no script edits ever needed.
|
||||
|
||||
Set the ZCONFIG environment variable to point at a config somewhere else — handy for a second machine profile, or for running against a scratch config without touching your real one.
|
||||
|
||||
Config reference
|
||||
----------------
|
||||
|
||||
{
|
||||
"ec2": {
|
||||
"ip": "203.0.113.10", // your server's public IP
|
||||
"user": "youruser", // SSH user on the server
|
||||
"pemKey": "C:\\Users\\You\\.ssh\\key.pem", // path to your SSH private key
|
||||
"stackRoot": "/home/youruser/stack" // parent dir for all deployed projects
|
||||
},
|
||||
"paths": {
|
||||
"temp": "C:\\dev\\temp", // deploy zips staged here (auto-deleted)
|
||||
"backupsLocal": "C:\\dev\\backups\\projects", // zbackup output
|
||||
"backupsEc2": "C:\\dev\\backups\\ec2", // zbackup_ec2 output
|
||||
"scriptsRoot": "C:\\tools\\zscripts", // this folder
|
||||
"oneDriveBackups": "" // zsync destination ("" disables)
|
||||
},
|
||||
"projects": {
|
||||
"myapp": {
|
||||
"label": "My App", // display name in output
|
||||
"kind": "python", // python | vite | nextjs | edge | docker
|
||||
"localRoot": "C:\\dev\\myapp", // project folder on this machine
|
||||
"startModule": "myapp.main", // python kind: runs "python -m myapp.main"
|
||||
// "startApp": "app.main:app", // ...or, for ASGI/FastAPI: uvicorn app.main:app --port <dev> --reload
|
||||
"install": "-e .", // optional: pip args `zsetup` uses (auto-detects "-e ." / "-r requirements.txt")
|
||||
"ports": { "dev": 8080, "prod": 3000 }, // local dev port / direct server port
|
||||
"domain": "www.myapp.com", // public domain (health checks + verification)
|
||||
"start": { // optional zstart pre-steps
|
||||
"gitPull": true, // git pull --ff-only before starting
|
||||
"env": { "MYAPP_DEBUG": "1" } // env vars for the dev server process
|
||||
},
|
||||
"db": { "user": "dbuser", "name": "dbname" }, // optional: enables db dump/wait steps
|
||||
"migrations": "prisma", // optional: run prisma migrate on deploy
|
||||
"remote": {
|
||||
"path": "/home/youruser/stack/myapp", // deploy target on the server
|
||||
"composeDir": "/home/youruser/stack/myapp/docker", // optional: if compose isn't at path root
|
||||
"appService": "app", // optional: compose service name override
|
||||
"containerName": "myapp" // optional (vite): container to read build-version from
|
||||
},
|
||||
"deploy": {
|
||||
"zipName": "MyAppDeploy.zip", // optional: defaults to <key>Deploy.zip
|
||||
"gitPull": true, // optional: git pull --ff-only before zipping
|
||||
"tagOnDeploy": false, // optional: tag the deployed commit with its build number
|
||||
"exclude": ["docs", "big-data-folder"] // optional: extra top-level dirs/files to skip
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Optional blocks do real work:
|
||||
|
||||
- install — how zsetup installs a python project's dependencies into its .venv: the pip args, e.g. "-e .", "-e backend" (deps in a subfolder), or "-r requirements.txt". Omit it and zsetup auto-detects a root pyproject.toml/setup.py (-e .) or requirements.txt (-r requirements.txt). zstart never installs — run zsetup <key> once, then zstart <key>.
|
||||
- start — pre-start steps for zstart: gitPull: true fast-forwards the checkout from its upstream first, and when it can't — no upstream, diverged history, a remote that wants credentials — says so and starts the server anyway. A pull is never allowed to stand between you and a running dev server (that is deploy.gitPull's job, below, where refusing is correct). env sets environment variables for the dev-server process (feature flags, reload switches).
|
||||
- db — deploys wait for pg_isready and zbackup_ec2 pulls a pg_dump, both against the compose service named db. Omit it and those steps are skipped cleanly.
|
||||
- deploy.gitPull — git pull --ff-only in the project root before zipping, so a merged PR actually ships. Since zdeploy zips your working tree, a checkout left behind origin would otherwise deploy stale code and still bump the build number — a silent no-op that looks like success. A failed pull (dirty tree that conflicts, diverged history) aborts the deploy rather than shipping uncertain code.
|
||||
- deploy.tagOnDeploy — after a deploy whose live build number has been verified, lay an annotated git tag for that number on the deployed commit and push it. Off by default, and deliberately opt-in: a project that already tags its releases through a pull request must not also collect a tag per deploy, because a release ledger and a deploy counter are two different numbers. Nothing here can fail a deploy — an existing tag is left alone, a failed push leaves the tag local and tells you the command to finish it, and a working tree with uncommitted changes gets a warning that the tagged commit is not everything that shipped.
|
||||
- migrations": "prisma" — runs npx prisma migrate deploy inside the app container after each deploy.
|
||||
- Compose service-name conventions — handlers assume the app service is named app (python) or web (nextjs) and the database service db. Override the app service with remote.appService.
|
||||
- Edge extras — an edge-kind project can set proxyContainer (the nginx container's name, used for reloads and stale-container cleanup) and certsSource (a host path with TLS certs, mounted read-only when validating nginx.conf).
|
||||
|
||||
---
|
||||
|
||||
Commands
|
||||
--------
|
||||
|
||||
The .cmd wrappers are the everyday interface. Every command takes one or more project keys from your config; several also accept all. A leading dash is tolerated (zdeploy -myapp works the same as zdeploy myapp) for anyone with switch-style muscle memory.
|
||||
|
||||
| Command | What it does |
|
||||
|---|---|
|
||||
| zsetup <key> ... | Create the project's Python venv + install deps (or npm install for node) |
|
||||
| zstart <key> ... | Start local dev server(s) |
|
||||
| zstartd <key> ... | Same, detached (new window, returns immediately) |
|
||||
| zkill <key> ... \| all | Kill local dev server(s) by port |
|
||||
| zrestart <key> ... | Kill + start in one step |
|
||||
| zrestartd <key> ... | Kill + start detached |
|
||||
| zdeploy <key> ... \| all | Zip → upload → rebuild → verify a project on the server |
|
||||
| zec2 [<key> ...] | Quick reachability check (TCP + HTTP + live build version) |
|
||||
| zec2online [<key> ...] | Deep health check; auto-starts downed stacks, streams diagnostics |
|
||||
| zrepair <key> ... | Audit + repair compose/proxy state on the server |
|
||||
| zec2_rotatekeys <key> | Rotate/reset secret keys in a project's server-side .env (values generated server-side; never printed) |
|
||||
| zbackup <key> ... \| all | Zip local project sources (+ DB dump) to the backups folder |
|
||||
| zbackup_ec2 [<key> ...] | Pull DB dumps + server-side data files down from the server |
|
||||
| zsync [<key>] | Copy new backups offsite (or build + mirror a vite dist) |
|
||||
| zstart_docker | Run a local docker compose stack from scriptsRoot\docker\ |
|
||||
| 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) |
|
||||
| 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
|
||||
-----------------
|
||||
|
||||
zstart — start dev servers
|
||||
--------------------------
|
||||
|
||||
zstart <project> [<project> ...] [-Port N] [-BindHost <host>] [-Detached]
|
||||
|
||||
Starts each project's dev server using the handler for its kind: python runs python -m <startModule> — or, for an ASGI/FastAPI app, uvicorn <startApp> (e.g. app.main:app) with the dev port and --reload — preferring the project's .venv; vite runs npm run dev -- --host --port, nextjs runs npm run dev with PORT set. Runs npm install automatically if node_modules is missing. A project's optional start config block runs first — gitPull fast-forwards the checkout and env sets process environment variables. Two more opt-in conveniences: if the project has a motd/ folder of .txt files, one is shown (rotating) at startup; if it has scripts/build_version_tool.py, the build number is bumped on each start.
|
||||
|
||||
zstart viteapp # dev server on its configured port
|
||||
zstart pyapp -Port 9000 # override the port
|
||||
zstart viteapp -BindHost 0.0.0.0 # expose on the LAN
|
||||
zstartd nextapp # detached: window opens, prompt returns
|
||||
|
||||
zkill — stop dev servers
|
||||
------------------------
|
||||
|
||||
zkill <project> [<project> ...] | all [-Port N] [-KillAll]
|
||||
|
||||
Finds whatever is LISTENING on each project's dev port and kills it — along with its whole process tree, children first. That matters for auto-reloading servers (uvicorn/watchfiles, nodemon): their worker processes inherit the listening socket and would otherwise survive as orphans, serving stale code. -KillAll also hunts down stray node/python/next-server processes whose command line references the project folder. all targets every project that has a dev port — the one-shot "stop everything I've got running locally".
|
||||
|
||||
zkill viteapp # free the port
|
||||
zkill pyapp viteapp nextapp # nuke everything
|
||||
zkill all # stop every project's dev server
|
||||
zkill nextapp -KillAll # also kill orphaned runtime processes
|
||||
|
||||
zrestart — kill then start
|
||||
--------------------------
|
||||
|
||||
zrestart <project> [<project> ...] [-Port N] [-KillAll] [-NoRestart] [-Detached] [-BindHost <host>]
|
||||
|
||||
The "it's wedged, bounce it" command: kill phase, then start phase with the same flags. -NoRestart makes it kill-only; zrestartd restarts detached.
|
||||
|
||||
Server deployment & operations
|
||||
------------------------------
|
||||
|
||||
zdeploy — deploy to the server
|
||||
------------------------------
|
||||
|
||||
zdeploy <project> [<project> ...] [-Note "message"]
|
||||
zdeploy all [-Note "message"]
|
||||
|
||||
The core workflow, per project kind (projects with deploy.gitPull first git pull --ff-only so a merged PR isn't left behind):
|
||||
|
||||
- python / vite / nextjs — zip the local source (excluding .git, node_modules, envs, archives, junk, plus anything in deploy.exclude), free disk space on the server (docker prune; aborts if under 1.5 GB free), scp the zip up, unzip into remote.path preserving all server-side .env* files plus anything listed in deploy.preserve (staged keys, certs, seed data — files or directories), docker compose build + up -d, then verify the live site reports the new build version (see Enabling deploy verification (#enabling-deploy-verification)). nextjs additionally waits for Postgres (db block) and applies migrations (migrations field). Zips are always deleted locally afterward.
|
||||
- edge — uploads every top-level file in the edge folder (nginx.conf, compose, css, htpasswd, …), validates the new config with nginx -t before switching over, then recreates the proxy.
|
||||
- docker — uploads the compose folder's files, docker compose pull + up -d. For stacks that run stock images (analytics, mail, etc.).
|
||||
|
||||
all deploys every project — edge kinds first, then the rest in config order — and stops at the first failure.
|
||||
|
||||
zdeploy viteapp
|
||||
zdeploy pyapp -Note "fix billing banner"
|
||||
zdeploy all -Note "weekly release"
|
||||
|
||||
zec2 — reachability check
|
||||
-------------------------
|
||||
|
||||
zec2 [<project> ...] # no args = every project with a domain
|
||||
|
||||
For each project: TCP connect, then an HTTP GET with the project's domain as the Host header, then the live build version. Fast "is it up?" answer with firewall hints when it isn't.
|
||||
|
||||
zec2online — health check with auto-recovery
|
||||
--------------------------------------------
|
||||
|
||||
zec2online [<project> ...] # no args = every project with a domain
|
||||
|
||||
The heavier sibling: verifies each app over HTTP, compares the local build version against what the server is actually serving (a mismatch means "redeploy?" — or a stale cache), and if a site is down it SSHes in, runs docker compose up -d for the app and the edge proxy, waits up to 30 s, and streams compose logs and system diagnostics if recovery fails.
|
||||
|
||||
zrepair — fix server routing
|
||||
----------------------------
|
||||
|
||||
zrepair <project> [<project> ...]
|
||||
|
||||
Validates the edge proxy's nginx config (if an edge project is defined), shows each stack's compose status, starts anything that's down, and smoke-tests the live domain. For the "deploy succeeded but the site 502s" class of problem.
|
||||
|
||||
zstop.ps1 — stop server stacks
|
||||
------------------------------
|
||||
|
||||
zstop <project> [<project> ...]
|
||||
|
||||
docker compose down for the selected stacks on the server. Data volumes are preserved; zdeploy <project> brings a stack back. (PowerShell script only, no .cmd wrapper.)
|
||||
|
||||
zec2_rotatekeys — rotate server-side secrets
|
||||
--------------------------------------------
|
||||
|
||||
zec2_rotatekeys <project> [-Rotate KEY,KEY] [-Set KEY,KEY] [-EnvFile rel/path] [-Restart] [-WhatIf]
|
||||
|
||||
For when a secret leaks or a deploy overwrites a production .env with dev values: rotate or reset keys in a project's server-side .env without the values ever passing through this machine's shell history, a command argument, or your screen. -Rotate keys are regenerated on the server with openssl rand -hex 32 — the new value is written straight into the .env there and never leaves the box. -Set keys are typed into a masked prompt and streamed to the server over SSH stdin (never a command argument, never echoed), for operator-known values like DATABASE_URL or ADMIN_EMAIL. The current server .env is copied to a timestamped .bak before any change; the KEY line is updated atomically, matching an existing key or appending it. The env file is auto-detected from the project's deploy.preserve (first *.env) or defaults to .env — override with -EnvFile backend/.env. Nothing touches the running app unless you pass -Restart, which recreates the container (docker compose up -d --force-recreate <svc>) so it actually reloads the new .env — a plain restart would keep the old environment. Being high-impact, it confirms before writing; -WhatIf prints the exact plan and changes nothing.
|
||||
|
||||
# Preview only — see exactly what would change, change nothing:
|
||||
zec2_rotatekeys pyapp -Rotate JWT_SECRET -Set DATABASE_URL,ADMIN_EMAIL -WhatIf
|
||||
|
||||
# Regenerate the JWT secret, restore the operator-known values, then restart:
|
||||
zec2_rotatekeys pyapp -Rotate JWT_SECRET -Set DATABASE_URL,ADMIN_EMAIL,ADMIN_PASSWORD -Restart
|
||||
|
||||
Backups
|
||||
-------
|
||||
|
||||
zbackup — local backups
|
||||
-----------------------
|
||||
|
||||
zbackup <project> [<project> ...] [-Tag "label"]
|
||||
zbackup all # every project + this scripts folder
|
||||
zbackup scripts # just this scripts folder ('scripts' is reserved)
|
||||
|
||||
Zips each project's source into paths.backupsLocal\<key>\<timestamp>_<key>[_tag].zip. If the project's .env declares a DATABASE_URL, a Postgres dump is bundled into the zip automatically — quoted values (Prisma-style), postgres:///postgresql+driver:// schemes, and URLs without an explicit port all parse. backend\.env is checked too, for frontend/backend split projects. -Tag labels the archive — handy before risky changes.
|
||||
|
||||
zbackup all # everything
|
||||
zbackup pyapp -Tag "pre-migration"
|
||||
|
||||
zbackup_ec2 — pull backups from the server
|
||||
------------------------------------------
|
||||
|
||||
zbackup_ec2 [<project> ...] # no args = every project with a remote.path
|
||||
|
||||
For projects with a db block, runs pg_dump inside the server's db container. Also zips server-side data dirs (uploads/, archive/, dist/) when present, then downloads everything to paths.backupsEc2 and cleans up the remote temp files.
|
||||
|
||||
zsync — sync backups offsite
|
||||
----------------------------
|
||||
|
||||
zsync # new backup files -> paths.oneDriveBackups
|
||||
zsync <viteproject> -Destination <path> # npm run build, then mirror dist/ to path
|
||||
|
||||
The no-args mode copies only files that don't already exist at the destination (never overwrites, never deletes). The project mode is for mirroring a static build; it also honors $env:ZSYNC_DEST.
|
||||
|
||||
zbackup_and_sync.ps1 — both in one
|
||||
----------------------------------
|
||||
|
||||
zbackup_and_sync.ps1 <project> [<project> ...] | all
|
||||
|
||||
Runs zbackup, then zsync. This is what the scheduled task calls (with all).
|
||||
|
||||
setup_backup_schedule.ps1 — nightly automation
|
||||
----------------------------------------------
|
||||
|
||||
Run as Administrator once. Creates a Windows Scheduled Task that runs zbackup_and_sync.ps1 all daily at 2:00 AM.
|
||||
|
||||
Utilities
|
||||
---------
|
||||
|
||||
zstart_docker — local compose stack
|
||||
-----------------------------------
|
||||
|
||||
zstart_docker [-Build] [-Attached] [-Solo]
|
||||
|
||||
Brings up a docker compose stack from scriptsRoot\docker\docker-compose.yml (or docker-compose.solo.yml with -Solo). Checks that Docker Desktop is actually running and tells you how to unwedge it if not.
|
||||
|
||||
zsetup_mail.ps1 — provision mail accounts
|
||||
-----------------------------------------
|
||||
|
||||
zsetup_mail.ps1 -domain yourdomain.com [-mailHost mail.yourdomain.com]
|
||||
|
||||
Creates admin@ and noreply@ mailboxes (with generated passwords) in a docker-mailserver container on the server, then prints the exact DNS records (MX, SPF, A) and SMTP/IMAP settings to plug into your app.
|
||||
|
||||
ZHelpers.ps1 — shared library
|
||||
-----------------------------
|
||||
|
||||
Not run directly. Dot-sourced by the other scripts; provides config loading (Get-ZConfig, Get-ZProject), SSH helpers (Invoke-Ec2Step), the deploy/backup archiver (New-ProjectArchive), and process-kill helpers. Extend here if you're adding your own scripts.
|
||||
|
||||
---
|
||||
|
||||
Enabling deploy verification
|
||||
----------------------------
|
||||
|
||||
Why not just check for HTTP 200? Because a 200 proves nothing — a stale cached build serves 200 all day. These scripts verify a deploy by comparing build numbers: your app exposes its build version, the deploy expects to see the new number live, and a mismatch means the upload or Docker build failed (or you're looking at a cached build).
|
||||
|
||||
It's optional — deploys still work without it, ending in a WARNING instead of a PASS — but it's the difference between "the server answered" and "the code I just shipped is actually running."
|
||||
|
||||
1. Add a version file to your project
|
||||
-------------------------------------
|
||||
|
||||
// build-version.json (vite: in public/ · nextjs: in public/ · python: project root)
|
||||
{ "productVersion": "1.0", "buildNumber": 42 }
|
||||
|
||||
2. Bump it during the server-side Docker build
|
||||
----------------------------------------------
|
||||
|
||||
The convention: each deploy's Docker build increments buildNumber by one, so the deploy script expects local buildNumber + 1 to show up live. One line in your Dockerfile does it:
|
||||
|
||||
RUN node -e "const f='public/build-version.json',v=require('./'+f);v.buildNumber++;require('fs').writeFileSync(f,JSON.stringify(v))"
|
||||
|
||||
3. Expose it
|
||||
------------
|
||||
|
||||
Vite / static sites — nothing to do: public/build-version.json is served at /build-version.json, which is where verification looks. (If your edge proxy blocks it from outside, set remote.containerName in config and verification reads it inside the container instead.)
|
||||
|
||||
Next.js — add an API route at /api/build-version:
|
||||
|
||||
// app/api/build-version/route.ts
|
||||
import { NextResponse } from "next/server";
|
||||
import bv from "@/public/build-version.json";
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({ build_version: `v${bv.productVersion}.${bv.buildNumber}` });
|
||||
}
|
||||
|
||||
Python (FastAPI shown; any framework works) — expose /api/build-version:
|
||||
|
||||
import json, pathlib
|
||||
|
||||
@app.get("/api/build-version")
|
||||
def build_version():
|
||||
bv = json.loads(pathlib.Path("build-version.json").read_text())
|
||||
return {"build_version": f"v{bv['productVersion']}.{bv['buildNumber']}"}
|
||||
|
||||
Python projects can go further with a scripts/build_version_tool.py supporting get / set / bump subcommands — if present, zdeploy bumps the version inside the running container, records it, and zstart bumps on every dev start.
|
||||
|
||||
Alternative: server-side health check (verify block)
|
||||
----------------------------------------------------
|
||||
|
||||
Not every stack is published through the edge proxy — internal APIs, apps whose host port the firewall blocks, services waiting on a DNS record. For those, the old fallback (GET http://<server-ip>/) was worse than nothing: the edge proxy's default vhost answers with a 200 and the deploy "passes" even if your app never started.
|
||||
|
||||
Give the project a verify block instead, and zdeploy checks the app from the server itself over SSH:
|
||||
|
||||
"verify": { "port": 8005, "path": "/health", "expect": "\"status\":\"ok\"" }
|
||||
|
||||
port is the host port the app publishes on the server; path defaults to /; expect is an optional substring the response must contain (an app version string makes this equivalent to build-number verification). Python-kind projects use verify automatically when there's no build_version_tool.py — and projects with neither a domain nor a verify block are now honestly reported as NOT verified instead of green-lighting the proxy's default page.
|
||||
|
||||
Two more keys make the check unambiguous and patient:
|
||||
|
||||
"verify": {
|
||||
"port": 8005, "path": "/health",
|
||||
"viaProxy": "my_edge_proxy", "upstream": "myapp_container:8000",
|
||||
"timeoutSeconds": 120
|
||||
}
|
||||
|
||||
- viaProxy + upstream — read the version over the docker network, by exec-ing a curl inside the named container (usually the edge proxy, since it is on every stack's network). This is the most trustworthy channel there is: it cannot answer from the wrong product, and it works for apps that publish no host port at all.
|
||||
- timeoutSeconds — how long verification may wait. An app that runs database migrations in its entrypoint exceeds a 30-second window on every deploy that ships one, and a warning that fires on routine success teaches you to ignore the one that matters.
|
||||
|
||||
Verification walks its channels in trust order — docker-network viaProxy, then localhost:<port>, then the edge with the project's own Host header — and re-picks the channel on every retry. That last part is the point: the check runs while the app is restarting, which is exactly when the good channels are briefly down, and locking the choice in up front is how a whole verification window gets spent asking the wrong thing. A project with no domain is never asked for via the edge: without a Host header the proxy can only answer from its default vhost — a different product — and no answer beats somebody else's answer.
|
||||
|
||||
zec2 and zec2online use these same endpoints to show what's live and flag local/server version drift.
|
||||
|
||||
---
|
||||
|
||||
Adding a new project
|
||||
--------------------
|
||||
|
||||
1. Add a key under projects in zconfig.json — copy the sample of the matching kind and rename it.
|
||||
2. That's it: zstart, zkill, zrestart, zbackup, zdeploy, zec2, zec2online, zrepair, zstop all accept the new key immediately.
|
||||
3. A project whose deploy doesn't fit the python/vite/nextjs/edge/docker patterns needs its own Invoke-<Kind>Deploy function in zdeploy.ps1 — copy an existing handler; they're all variations on zip → upload → compose up → verify.
|
||||
|
||||
Downloading without cloning
|
||||
---------------------------
|
||||
|
||||
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
|
||||
unzip zscripts-v1.0.0.0.0.zip -d zscripts # extract
|
||||
cd zscripts && sha256sum -c CHECKSUMS.txt # verify the contents
|
||||
|
||||
The zip contains every command, CHECKSUMS.txt, zconfig.example.json, and the docs. Versions follow v{major}.{rc}.{beta}.{alpha}.{build}; every script header carries the release version it shipped in, so even a single copied file can be traced to its release.
|
||||
|
||||
Verifying what you downloaded
|
||||
-----------------------------
|
||||
|
||||
CHECKSUMS.txt holds a SHA-256 for every .ps1 and .cmd in the repo. Check them before running anything:
|
||||
|
||||
zchecksums
|
||||
|
||||
Or with the standard tool on Linux/macOS/WSL — the manifest is sha256sum format:
|
||||
|
||||
sha256sum -c CHECKSUMS.txt
|
||||
|
||||
The hashes are identical on every platform: .gitattributes pins .ps1/.cmd to CRLF everywhere, so a file is byte-for-byte the same whether you cloned on Windows or Linux.
|
||||
|
||||
zchecksums flags three things — a file whose contents changed, a listed file that's gone, and a script on disk that isn't in the manifest (so something added quietly still gets noticed). It exits non-zero on any of them.
|
||||
|
||||
If you edit a script yourself, regenerate and commit the manifest with it:
|
||||
|
||||
zchecksums -Update
|
||||
|
||||
What this does and doesn't prove. CHECKSUMS.txt lives in the same repo as the scripts, so anyone who could alter a script could alter the manifest too. It's an integrity check, not a signature: it reliably catches a truncated clone, a local edit you forgot about, or a file added outside a commit. It does not prove the code came from this project — for that you'd need a signature or a hash published outside this repo.
|
||||
|
||||
Tests
|
||||
-----
|
||||
|
||||
The toolkit has its own Pester (https://pester.dev) suite covering the pure logic — the exclude lists, config lookups, and version-label formatting that the deploy and backup paths depend on:
|
||||
|
||||
Invoke-Pester .\tests
|
||||
|
||||
Needs Pester 5+ (Install-Module Pester -Scope CurrentUser); Windows ships 3.x, which won't run these. The suite injects a fixture config through ZCONFIG, so it never reads your real zconfig.json and runs fine on a machine that has never been configured.
|
||||
|
||||
The high-value case is the deploy-vs-backup split: deploys must exclude .env files and uploads/, backups must keep them. Get that backwards in either direction and you either ship secrets to production or quietly write backups that can't restore — neither fails loudly at runtime.
|
||||
|
||||
Troubleshooting
|
||||
---------------
|
||||
|
||||
- "zconfig.json not found" — you haven't copied zconfig.example.json yet. Every script tells you this and exits.
|
||||
- "Unknown project key" — the argument doesn't match a key in zconfig.json; the error lists the valid keys.
|
||||
- "PEM key not found" — fix ec2.pemKey in zconfig.json.
|
||||
- Deploy aborts with "less than 1.5 GB free" — the server's disk is full even after auto-pruning. Grow the volume, or SSH in and run sudo docker system prune -af.
|
||||
- Deploy ends with a version WARNING — the new build isn't what's being served: check the Docker build output, and see Enabling deploy verification (#enabling-deploy-verification) if you haven't set it up.
|
||||
- Port already in use when starting — zkill <project> first, or just use zrestart.
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
MIT (LICENSE)
|
||||
68
SECURITY.md
68
SECURITY.md
@ -1,68 +0,0 @@
|
||||
# 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
73
SECURITY.txt
@ -1,73 +0,0 @@
|
||||
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.
|
||||
@ -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
|
||||
Licensed under the MIT License. See LICENSE.
|
||||
-->
|
||||
|
||||
@ -1,296 +0,0 @@
|
||||
evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
Created by Kelly Michels · dev@evomedia.net
|
||||
Licensed under the MIT License. See LICENSE.
|
||||
|
||||
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.
|
||||
422
ZHelpers.ps1
422
ZHelpers.ps1
@ -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
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.28
|
||||
# Version: v1.0.0.0.19
|
||||
|
||||
# ZHelpers.ps1 — shared library dot-sourced by every z script. Not run directly.
|
||||
|
||||
@ -15,8 +15,8 @@ $script:ArchiveExtensions = @(
|
||||
# exempt from ArchiveExtensions. A project that vendors a dependency as
|
||||
# vendor/*.tgz (common when a bundler cannot resolve `file:` links outside the
|
||||
# project root) needs that tarball in the deploy zip - dropping it makes a
|
||||
# Dockerfile's `COPY vendor ./vendor` fail at image build, which is a
|
||||
# confusing way to discover the archive filter ate a required build input.
|
||||
# Dockerfile's `COPY vendor ./vendor` fail at image build, which is a confusing
|
||||
# way to discover the archive filter ate a required build input.
|
||||
$script:ArchiveKeepDirNames = @('vendor')
|
||||
$script:ScriptExtensions = @('.ps1', '.cmd', '.bat')
|
||||
$script:JunkExtensions = @(
|
||||
@ -42,9 +42,8 @@ $script:JunkDirNames = @(
|
||||
$script:ZConfigCache = $null
|
||||
|
||||
# Where zconfig.json lives. Defaults to next to the scripts; override with the
|
||||
# ZCONFIG environment variable, matching the bash port (zhelpers.sh does the
|
||||
# same). Useful for pointing a run at an alternate config, and it is the seam
|
||||
# the test suite uses to inject a fixture.
|
||||
# ZCONFIG environment variable. Useful for pointing a run at an alternate
|
||||
# config, and it is the seam the test suite uses to inject a fixture.
|
||||
function Get-ZConfigPath {
|
||||
if ($env:ZCONFIG) { return $env:ZCONFIG }
|
||||
return (Join-Path $PSScriptRoot "zconfig.json")
|
||||
@ -105,33 +104,6 @@ function Get-ZEdgeProject {
|
||||
}
|
||||
|
||||
# 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 {
|
||||
param([Parameter(Mandatory)][string]$Key)
|
||||
$proj = Get-ZProject -Key $Key
|
||||
@ -150,19 +122,18 @@ function Get-Ec2Home {
|
||||
return "/home/$((Get-ZConfig).ec2.user)"
|
||||
}
|
||||
|
||||
# Options every deploy-path ssh/scp carries. Splat with @sshOpts, matching the
|
||||
# idiom in zsetup_mail.ps1 and zec2_rotatekeys.ps1.
|
||||
# Options every deploy-path ssh/scp carries. Splat with @sshOpts.
|
||||
#
|
||||
# BatchMode=yes is the one that matters. Without it ssh PROMPTS - for a
|
||||
# passphrase, a password, a sudo password - and waits forever. The deploy pipes
|
||||
# stderr into the pipeline (2>&1 | ForEach-Object) so the prompt is swallowed
|
||||
# on its way to the screen: the run simply stops under whatever step label was
|
||||
# printed last, with nothing to explain it and no obvious reason why that
|
||||
# particular step would be slow. `zdeploy umami` appeared to hang on "ensure
|
||||
# shared web network", a step whose entire body is `docker network create web
|
||||
# particular step would be slow. One deploy appeared to hang on "ensure shared
|
||||
# web network", a step whose entire body is `docker network create web
|
||||
# 2>/dev/null || true` against a network that already existed.
|
||||
#
|
||||
# There is no prompt here we would ever want to answer - the deploy key is
|
||||
# There is no prompt here you would ever want to answer - a deploy key is
|
||||
# unencrypted and sudo on the box is passwordless - so failing immediately is
|
||||
# strictly better than waiting on input that is never coming.
|
||||
#
|
||||
@ -176,16 +147,16 @@ function Get-Ec2Home {
|
||||
# reads its stdin and forwards it to the remote command, and under PowerShell it
|
||||
# inherits the console handle - so it can block forever waiting on input nobody
|
||||
# is going to type. The timeouts above cannot help: they bound a connection that
|
||||
# is dying, and this one was never established. A deploy on 2026-08-19 stopped
|
||||
# under "ensure unzip installed", a step whose body short-circuits on an already
|
||||
# installed unzip; the server showed no ssh session at all (`who` empty, no
|
||||
# docker build running), which is what a client-side stdin block looks like from
|
||||
# the other end. The zip had uploaded and prod stayed a PR behind.
|
||||
# is dying, and this one was never established. One deploy stopped under
|
||||
# "ensure unzip installed", a step whose body short-circuits when unzip is
|
||||
# already present; the server showed no ssh session at all (`who` empty, no
|
||||
# docker build running), which is what a client-side stdin block looks like
|
||||
# from the other end. The zip had uploaded and prod stayed a release behind.
|
||||
#
|
||||
# Safe here because nothing that pipes stdin INTO ssh uses these options:
|
||||
# zdeploy passes only command strings, and the scripts that do pipe
|
||||
# (one-off registration and key-rotation scripts) build their own
|
||||
# option arrays. Adding -n to those would break them - do not hoist it.
|
||||
# zdeploy passes only command strings. Any script that DOES pipe into ssh must
|
||||
# build its own option array - adding -n to those would break them, so do not
|
||||
# hoist this beyond the deploy path.
|
||||
function Get-Ec2SshOpts {
|
||||
return @(
|
||||
'-n',
|
||||
@ -199,9 +170,9 @@ function Get-Ec2SshOpts {
|
||||
|
||||
# The same options for scp, which does NOT accept -n: OpenSSH's scp exits 1 with
|
||||
# "unknown option -- n" and prints its usage block. That failure is easy to
|
||||
# misread, because the caller's own error text is what the operator sees and the
|
||||
# usage text scrolls past above it - a deploy on 2026-08-19 reported "Likely
|
||||
# server disk space" while the box sat at 79% with 8.1 GB free.
|
||||
# misread, because the caller's own error text is what the operator sees while
|
||||
# the usage text scrolls past above it - one deploy reported "Likely server disk
|
||||
# space" on a box with plenty of room.
|
||||
#
|
||||
# Derived from Get-Ec2SshOpts rather than duplicated, so the timeouts can never
|
||||
# drift apart between the two transports.
|
||||
@ -246,11 +217,11 @@ function Invoke-Ec2Step {
|
||||
#
|
||||
# Deploys ship the default branch, so this SWITCHES to it rather than pulling
|
||||
# whatever branch happens to be checked out. The old behaviour pulled the
|
||||
# current branch, which broke the day delete_branch_on_merge went on
|
||||
# fleet-wide (2026-08-07): a repo still sitting on its just-merged PR branch
|
||||
# pulls a ref the merge deleted, and the deploy dies on "no such ref was
|
||||
# fetched". Worse, when the ref DID still exist, pulling the feature branch
|
||||
# meant a deploy could ship a branch, not main.
|
||||
# current branch, which breaks as soon as the remote deletes branches on merge:
|
||||
# a checkout still sitting on its just-merged PR branch pulls a ref the merge
|
||||
# deleted, and the deploy dies on "no such ref was fetched". Worse, when the ref
|
||||
# DID still exist, pulling the feature branch meant a deploy could ship a
|
||||
# branch rather than the default.
|
||||
#
|
||||
# The switch refuses to run over local changes: a dirty tree aborts the deploy
|
||||
# with the file list rather than risk tangling uncommitted work. The stale
|
||||
@ -304,11 +275,12 @@ function Invoke-DeployGitPull {
|
||||
# each deploy block the next one - the operator had to commit or
|
||||
# stash a change they never made. The guard exists to stop
|
||||
# unreviewed SOURCE shipping; a stamp the script just wrote is not
|
||||
# that. It is still committed separately, one bump per release,
|
||||
# per the versioning rule - this only stops it being a gate.
|
||||
# that. It is still committed separately, one bump per PR, per the
|
||||
# versioning rule - this only stops it being a gate.
|
||||
$deployWritten = @('build-version.json', 'CHANGELOG.md')
|
||||
$dirty = $dirty | Where-Object {
|
||||
$path = ($_ -replace '^..\s+', '') -replace '^.*/', ''
|
||||
(Get-DeployStampFiles) -notcontains $path
|
||||
$deployWritten -notcontains $path
|
||||
}
|
||||
if ($dirty) {
|
||||
$files = ($dirty | ForEach-Object { " $_" }) -join "`n"
|
||||
@ -341,179 +313,6 @@ function Invoke-DeployGitPull {
|
||||
}
|
||||
}
|
||||
|
||||
# ── Files the deploy itself writes ──────────────────────────────────────────
|
||||
#
|
||||
# zdeploy stamps the bumped build version, and appends a changelog line, into
|
||||
# the working tree after a successful run. Neither is unreviewed SOURCE, so
|
||||
# neither should make a tree look dirty to the guards below - leaving them in
|
||||
# scope made each deploy block the next one, over a change the operator never
|
||||
# made.
|
||||
#
|
||||
# One list, because two copies drift: the first copy knew about CHANGELOG.md
|
||||
# and not build_changelog.md, which is the name a project's own changelog
|
||||
# tool may write.
|
||||
function Get-DeployStampFiles {
|
||||
return @('build-version.json', 'CHANGELOG.md', 'build_changelog.md')
|
||||
}
|
||||
|
||||
# Tracked modifications, minus those stamps. Runs in the CURRENT directory;
|
||||
# both callers are inside a Push-Location on the project root.
|
||||
function Get-TrackedChangesExcludingStamps {
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$dirty = git status --porcelain --untracked-files=no
|
||||
$stamps = Get-DeployStampFiles
|
||||
return @($dirty | Where-Object {
|
||||
$path = ($_ -replace '^..\s+', '') -replace '^.*/', ''
|
||||
$stamps -notcontains $path
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
# ── The release tag zdeploy owes the versioning scheme ──────────────────────
|
||||
#
|
||||
# The scheme says every release lays an annotated tag alongside its version
|
||||
# stamp. For a project whose build number lives OUTSIDE git nothing enforced
|
||||
# that, and the tag history quietly stopped tracking reality: one project kept
|
||||
# its number in a database and reached thirty-eight builds with four tags.
|
||||
# Those builds were not recoverable - the number only ever existed in the
|
||||
# database - so this stops the bleeding rather than back-filling.
|
||||
#
|
||||
# Opt-in per project, via deploy.tagOnDeploy. A project that already tags its
|
||||
# releases through a pull request must NOT also get a tag per deploy: a
|
||||
# git-side release ledger and a container's own deploy counter are two
|
||||
# different numbers on purpose.
|
||||
#
|
||||
# Runs only after the live build has been VERIFIED, and never throws. A deploy
|
||||
# that reached production must not be reported as failed because a tag could
|
||||
# not be written afterwards.
|
||||
function New-DeployTag {
|
||||
param(
|
||||
[Parameter(Mandatory)]$Proj,
|
||||
[Parameter(Mandatory)][string]$Version,
|
||||
[string]$Note = "Build deployed"
|
||||
)
|
||||
if (-not ($Proj.deploy -and $Proj.deploy.tagOnDeploy)) { return }
|
||||
$root = $Proj.localRoot
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $root ".git"))) {
|
||||
Write-Host " tagOnDeploy is set but '$root' is not a git repo - no tag written." -ForegroundColor Yellow
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "`n--- [6] Tagging the deployed commit as $Version ---" -ForegroundColor Cyan
|
||||
Push-Location -LiteralPath $root
|
||||
try {
|
||||
# The same PS 5.1 trap the rest of this file documents: success is
|
||||
# judged by $LASTEXITCODE, and git writes ordinary progress to stderr.
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
$sha = git rev-parse HEAD
|
||||
if ($LASTEXITCODE -ne 0 -or -not $sha) {
|
||||
Write-Host " Could not read HEAD - no tag written. The deploy stands." -ForegroundColor Yellow
|
||||
return
|
||||
}
|
||||
$sha = "$sha".Trim()
|
||||
$short = $sha.Substring(0, 7)
|
||||
|
||||
# The zip is taken from the WORKING TREE, so uncommitted changes ship
|
||||
# while the commit this tag names does not contain them. Say so rather
|
||||
# than let a tag quietly claim to describe the build.
|
||||
$dirty = Get-TrackedChangesExcludingStamps
|
||||
if ($dirty.Count -gt 0) {
|
||||
Write-Host " Working tree has $($dirty.Count) uncommitted change(s): $Version names $short, which is NOT everything that shipped." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
git rev-parse -q --verify "refs/tags/$Version" *> $null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host " Tag $Version already exists - left alone." -ForegroundColor DarkYellow
|
||||
return
|
||||
}
|
||||
|
||||
git tag -a $Version -m "$Version - $Note"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host " git tag failed - the deploy stands, the tag does not." -ForegroundColor Yellow
|
||||
return
|
||||
}
|
||||
git push origin $Version
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host " $Version written locally but the push failed. Push it when you can: git push origin $Version" -ForegroundColor Yellow
|
||||
return
|
||||
}
|
||||
Write-Host " Tagged $Version at $short and pushed." -ForegroundColor Green
|
||||
}
|
||||
catch {
|
||||
Write-Host " Tagging failed ($($_.Exception.Message)) - the deploy stands." -ForegroundColor Yellow
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# ── zstart's pull: fast-forward if you can, start regardless ─────────────────
|
||||
#
|
||||
# The OPPOSITE failure mode from Invoke-DeployGitPull above, on purpose. A
|
||||
# deploy that cannot prove it has the default branch must refuse - shipping
|
||||
# stale code and still bumping the build is a silent lie. A dev server has no
|
||||
# such stake: the person is already at a checkout they chose, often a feature
|
||||
# branch, and a pull that cannot complete is information, not a reason to
|
||||
# leave them without a server. So this never throws, never switches branch,
|
||||
# and never touches a dirty tree - it reports, and zstart carries on.
|
||||
#
|
||||
# It shares the deploy helper's one hard-won mechanic. Under zstart's
|
||||
# $ErrorActionPreference = 'Stop', the previous inline `git pull --ff-only
|
||||
# 2>&1` wrapped every stderr line in a terminating ErrorRecord - and git
|
||||
# prints ordinary fetch progress ("From https://...") on stderr. A pull that
|
||||
# SUCCEEDED aborted the start before its own skip-and-continue branch could
|
||||
# run (#130). Success is judged by $LASTEXITCODE, nothing is redirected, and
|
||||
# the preference drops to Continue for this function's scope only.
|
||||
#
|
||||
# Fetch, then merge --ff-only against the branch's upstream, rather than
|
||||
# `git pull` - see the FETCH_HEAD race note on Invoke-DeployGitPull.
|
||||
function Invoke-StartGitPull {
|
||||
param([Parameter(Mandatory)][string]$Root)
|
||||
$result = [pscustomobject]@{ Ok = $false; Skipped = $false; Message = '' }
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $Root '.git'))) {
|
||||
$result.Skipped = $true
|
||||
$result.Message = "'$Root' is not a git repo"
|
||||
return $result
|
||||
}
|
||||
Push-Location -LiteralPath $Root
|
||||
# GIT_TERMINAL_PROMPT=0 so a repo that needs credentials fails fast
|
||||
# instead of blocking the server start on a "Username for ..." prompt.
|
||||
$prevPrompt = $env:GIT_TERMINAL_PROMPT
|
||||
$env:GIT_TERMINAL_PROMPT = '0'
|
||||
try {
|
||||
$ErrorActionPreference = 'Continue'
|
||||
git fetch origin --prune
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
$result.Skipped = $true
|
||||
$result.Message = 'git fetch failed - credentials, or the remote'
|
||||
return $result
|
||||
}
|
||||
$branch = git rev-parse --abbrev-ref HEAD
|
||||
$upstream = git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>$null
|
||||
if ($LASTEXITCODE -ne 0 -or -not $upstream) {
|
||||
$result.Skipped = $true
|
||||
$result.Message = "'$branch' has no upstream to fast-forward from"
|
||||
return $result
|
||||
}
|
||||
$before = git rev-parse HEAD
|
||||
git merge --ff-only $upstream
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
$result.Message = "cannot fast-forward '$branch' onto $upstream (diverged, or local changes in the way) - left as is"
|
||||
return $result
|
||||
}
|
||||
$result.Ok = $true
|
||||
$result.Message = if ((git rev-parse HEAD) -eq $before) { 'Already up to date.' }
|
||||
else { "Now at: $(git log -1 --oneline)" }
|
||||
return $result
|
||||
}
|
||||
finally {
|
||||
$env:GIT_TERMINAL_PROMPT = $prevPrompt
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
# ── Build-version helpers ────────────────────────────────────────────────────
|
||||
|
||||
function Read-JsonBuildVersion {
|
||||
@ -529,36 +328,32 @@ function Get-ServerSideVersionCommand {
|
||||
$null when the project has not configured one.
|
||||
|
||||
.DESCRIPTION
|
||||
Four tools read a live build number, and all four did it by asking
|
||||
the public edge: zdeploy's post-deploy check, zec2, zec2online, and
|
||||
bash/zhelpers.sh. That works only for as long as the endpoint is
|
||||
public, and it should not be: www's /build-version.json has been
|
||||
blocked at the edge since an earlier security pass, and one app
|
||||
answering /api/build-version to anyone is the inconsistency this
|
||||
closes.
|
||||
Reading a live build number through the public proxy only works while
|
||||
that endpoint IS public - and a build stamp is something many sites
|
||||
deliberately do not serve to the world. Blocking it at the proxy
|
||||
without moving the readers first leaves every tool quietly reporting
|
||||
"unknown", which looks identical to "could not reach it".
|
||||
|
||||
Going through the edge is also how a check reads the WRONG product.
|
||||
The proxy answers from whichever vhost matches the Host header, so a
|
||||
service with no public route gets somebody else's version back --
|
||||
one project's deploy check compared another project's build against
|
||||
its own and reported a failure on a deploy that had worked.
|
||||
Going through the proxy is also how a check reads the WRONG service:
|
||||
the proxy answers from whichever vhost matches the Host header, so a
|
||||
container with no public route gets another site's version back.
|
||||
|
||||
A project reached only on the shared docker network cannot be curled
|
||||
from the host: an app container that publishes no port. It IS reachable by name
|
||||
from another container on that network, which also exercises the real
|
||||
HTTP path -- so this proves the app is serving, not merely that its
|
||||
A service reached only on a shared docker network cannot be curled
|
||||
from the host when it publishes no port. It IS reachable by name from
|
||||
another container on that network, which also exercises the real HTTP
|
||||
path - so this proves the app is serving, not merely that its
|
||||
database knows a version.
|
||||
|
||||
Config, on the project's `verify` block:
|
||||
|
||||
"verify": {
|
||||
"path": "/api/build-version",
|
||||
"viaProxy": "evo_edge_proxy",
|
||||
"upstream": "myapp_app:80"
|
||||
"viaProxy": "edge_proxy_container",
|
||||
"upstream": "app_container:80"
|
||||
}
|
||||
|
||||
Returns $null when either key is missing, so every project without
|
||||
this config keeps exactly the behaviour it has today.
|
||||
this config keeps the behaviour it has today.
|
||||
#>
|
||||
param($Proj)
|
||||
|
||||
@ -575,10 +370,9 @@ function Get-LabelFromVersionJson {
|
||||
The build label out of a version endpoint's JSON text, or $null.
|
||||
|
||||
.DESCRIPTION
|
||||
Two field names in the fleet: one app answers `build_version` on
|
||||
/api/build-version, another answers `version` on /health. Both mean
|
||||
"the build that is live", so both are accepted rather than making an
|
||||
app rename its own field.
|
||||
Apps disagree about the field name - some answer `build_version`,
|
||||
others `version`. Both mean "the build that is live", so both are
|
||||
accepted rather than making an app rename its own field.
|
||||
#>
|
||||
param([string]$Text)
|
||||
|
||||
@ -590,50 +384,14 @@ function Get-LabelFromVersionJson {
|
||||
}
|
||||
|
||||
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)
|
||||
if (-not $obj) { return $null }
|
||||
|
||||
# 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}
|
||||
# Five-segment scheme: v{major}.{rc}.{beta}.{alpha}.{build}
|
||||
if ($null -ne $obj.build -or $null -ne $obj.alpha) {
|
||||
$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)"
|
||||
}
|
||||
|
||||
# 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)"
|
||||
}
|
||||
|
||||
@ -1074,85 +832,3 @@ function Stop-ZTracking {
|
||||
Remove-Item -LiteralPath $tp -Force -ErrorAction SilentlyContinue
|
||||
Write-ZTrailer -FinalNote $FinalNote
|
||||
}
|
||||
|
||||
# ── Deploy-verification planning (pure; unit-tested in tests/) ──────────────
|
||||
|
||||
function Get-VerifyTimeout {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Seconds the live-build verification may wait, per project.
|
||||
|
||||
.DESCRIPTION
|
||||
verify.timeoutSeconds in zconfig.json lets a project that is slow to
|
||||
BOOT say so, instead of every deploy of it warning on a success. An
|
||||
app that runs database migrations in its entrypoint exceeds a 30s
|
||||
window on every deploy that ships one - and a warning that fires on
|
||||
routine success trains people to ignore the one that matters.
|
||||
#>
|
||||
param($Proj, [int]$DefaultSec)
|
||||
if ($Proj.verify -and $Proj.verify.timeoutSeconds) {
|
||||
return [int]$Proj.verify.timeoutSeconds
|
||||
}
|
||||
return $DefaultSec
|
||||
}
|
||||
|
||||
function Get-VerifyAttempts {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
The ordered ways to read this project's live build, most-trustworthy
|
||||
first. Pure: config in, plan out - so the ordering rules are testable
|
||||
without ssh.
|
||||
|
||||
.DESCRIPTION
|
||||
Three channels exist, and their order is the whole point:
|
||||
|
||||
exec - docker-network read via verify.viaProxy/upstream. Cannot
|
||||
answer from the wrong product, works for apps with no
|
||||
published port.
|
||||
port - localhost:<verify.port> on the server. Same-box, still
|
||||
unambiguous; used only if the body carries a version.
|
||||
edge - http://<ip> with a Host header. The proxy answers from
|
||||
whichever vhost MATCHES that header, so without one this
|
||||
channel can only reach the default vhost - which is a
|
||||
different product (that is how one project's check once read
|
||||
another's build number). It is therefore included ONLY
|
||||
when the project has a host to route by, and never
|
||||
otherwise: no answer at all beats somebody else's answer.
|
||||
|
||||
The caller must walk this list EVERY retry, not once up front: the
|
||||
verification runs while the app is restarting, which is exactly when
|
||||
the good channels are briefly down. Deciding the channel before the
|
||||
wait loop is how the whole window got spent on the worst one.
|
||||
#>
|
||||
param($Proj, [string]$ExecCmd)
|
||||
$attempts = @()
|
||||
if ($ExecCmd) {
|
||||
$attempts += [pscustomobject]@{
|
||||
Kind = 'exec'
|
||||
Label = "docker network: $($Proj.verify.upstream) (via $($Proj.verify.viaProxy))"
|
||||
}
|
||||
}
|
||||
if ($Proj.verify -and $Proj.verify.port) {
|
||||
$vPath = "/api/build-version"
|
||||
if ($Proj.verify.path) { $vPath = [string]$Proj.verify.path }
|
||||
$attempts += [pscustomobject]@{
|
||||
Kind = 'port'
|
||||
Port = [int]$Proj.verify.port
|
||||
Path = $vPath
|
||||
Label = "server localhost:$($Proj.verify.port)$vPath"
|
||||
}
|
||||
}
|
||||
$verifyHost = $null
|
||||
if ($Proj.deploy -and $Proj.deploy.verifyHost) { $verifyHost = [string]$Proj.deploy.verifyHost }
|
||||
elseif ($Proj.domain) { $verifyHost = [string]$Proj.domain }
|
||||
if ($verifyHost) {
|
||||
$attempts += [pscustomobject]@{
|
||||
Kind = 'edge'
|
||||
HostHeader = $verifyHost
|
||||
Label = "edge with Host: $verifyHost"
|
||||
}
|
||||
}
|
||||
# The comma stops PS 5.1 unrolling a one-element array into a bare
|
||||
# object - the same pipeline trap that deadlocked ztests day 2.
|
||||
return ,$attempts
|
||||
}
|
||||
|
||||
@ -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
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.28
|
||||
# Version: v1.0.0.0.19
|
||||
|
||||
# ZKillOnly.ps1 — stop local dev server listeners for any project in zconfig.json.
|
||||
#
|
||||
|
||||
@ -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
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.28
|
||||
# Version: v1.0.0.0.19
|
||||
|
||||
# ZKiller.ps1 — kill then restart dev servers for any project in zconfig.json.
|
||||
#
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "v1.0.0.0.28"
|
||||
"version": "v1.0.0.0.19"
|
||||
}
|
||||
|
||||
Binary file not shown.
@ -1 +0,0 @@
|
||||
e59dc1a0f6fd4d2af8cefd6698a60e2812b43ade74e6205cb0f2c75fdcaa925a zscripts-v1.0.0.0.20.zip
|
||||
Binary file not shown.
@ -1 +0,0 @@
|
||||
fa9d023d2641044ff154d12047d87a31250741e0699fc5b1360ab359bd68b5f0 zscripts-v1.0.0.0.21.zip
|
||||
Binary file not shown.
@ -1 +0,0 @@
|
||||
446428c605d06ee500f58145a7915f7b7ee7482dbf60c5499f64436aaf945e86 zscripts-v1.0.0.0.22.zip
|
||||
Binary file not shown.
@ -1 +0,0 @@
|
||||
27d6c8f0ea632c0ca940c7900d8f4682066b5963847b9838a474cc5d4cbb758f zscripts-v1.0.0.0.23.zip
|
||||
Binary file not shown.
@ -1 +0,0 @@
|
||||
250f68fe5fd46b95e3c393c19352aff1e7d2afc16cff2d76efb2487f1c8ba6df zscripts-v1.0.0.0.24.zip
|
||||
Binary file not shown.
@ -1 +0,0 @@
|
||||
6f6f1bf1b46e014e0518ef4ffa2b64e455e894681d5b337ffdd947b0efd0d538 zscripts-v1.0.0.0.25.zip
|
||||
Binary file not shown.
@ -1 +0,0 @@
|
||||
dfaa85f53e4dd16789ac0f1f8c9d7e506b56c0c068f4c5f8e2c4d1cce7db8d95 zscripts-v1.0.0.0.26.zip
|
||||
Binary file not shown.
@ -1 +0,0 @@
|
||||
f5b940344ffd832032b0c62e65e77f94bc2de294e690c9fd68362deb99d21e82 zscripts-v1.0.0.0.27.zip
|
||||
Binary file not shown.
@ -1 +0,0 @@
|
||||
5eca5198ba500bb0e1ceb1e5000cfb405d5fb5024fa500daa8f64f9c012c1291 zscripts-v1.0.0.0.28.zip
|
||||
@ -1,113 +0,0 @@
|
||||
# evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
|
||||
"""Render this repo's markdown to plain-text twins with the markup removed.
|
||||
|
||||
The .txt files exist for terminals, pagers and anywhere markdown doesn't
|
||||
render. They are generated - never edit one by hand:
|
||||
|
||||
python scripts/plaintext_twins.py # rewrite every .txt twin
|
||||
python scripts/plaintext_twins.py --check # exit 1 if any is out of sync
|
||||
|
||||
tests/PlainTextTwins.Tests.ps1 runs --check, so a markdown edit that forgets
|
||||
to regenerate fails the suite instead of shipping a twin that disagrees with
|
||||
the file it mirrors.
|
||||
|
||||
Was readme_txt.py, which did README only. CHANGELOG.txt was kept by hand and
|
||||
drifted the moment CHANGELOG.md was reorganised - and its docstring claimed a
|
||||
--check the suite never actually ran. Both are fixed here: one renderer, every
|
||||
pair, and a test that invokes it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
def pairs() -> tuple[tuple[str, str], ...]:
|
||||
"""Every root-level markdown file, with the twin it owes.
|
||||
|
||||
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:
|
||||
text = re.sub(r"!\[([^\]]*)\]\([^)]*\)", r"\1", text) # images -> alt text
|
||||
text = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r"\1 (\2)", text) # links -> text (url)
|
||||
text = re.sub(r"\*\*([^*]+)\*\*", r"\1", text) # bold
|
||||
text = re.sub(r"(?<!\*)\*([^*\n]+)\*(?!\*)", r"\1", text) # italic
|
||||
text = re.sub(r"`([^`]+)`", r"\1", text) # inline code
|
||||
return text
|
||||
|
||||
|
||||
def render(md: str) -> str:
|
||||
out: list[str] = []
|
||||
in_fence = False
|
||||
for line in md.splitlines():
|
||||
if line.lstrip().startswith("```"):
|
||||
# Drop the fence markers; the code itself stays, indented so it
|
||||
# still reads as a block without the backticks.
|
||||
in_fence = not in_fence
|
||||
continue
|
||||
if in_fence:
|
||||
out.append((" " + line) if line else "")
|
||||
continue
|
||||
# An HTML comment is invisible in rendered markdown, but its markers
|
||||
# are not invisible in a text file - they read as stray punctuation.
|
||||
# Keep what the comment says, drop the <!-- --> around it.
|
||||
if line.strip() in ("<!--", "-->"):
|
||||
continue
|
||||
heading = re.match(r"^(#{1,6})\s+(.*)$", line)
|
||||
if heading:
|
||||
text = _inline(heading.group(2))
|
||||
out.append(text)
|
||||
out.append(("=" if len(heading.group(1)) == 1 else "-") * len(text))
|
||||
continue
|
||||
out.append(_inline(line))
|
||||
text = "\n".join(out)
|
||||
text = re.sub(r"\n{3,}", "\n\n", text)
|
||||
return text.strip() + "\n"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
check = "--check" in sys.argv
|
||||
stale: list[str] = []
|
||||
for md_name, txt_name in PAIRS:
|
||||
source = ROOT / md_name
|
||||
if not source.exists():
|
||||
print(f"{md_name} is missing - nothing to render")
|
||||
return 1
|
||||
rendered = render(source.read_text(encoding="utf-8"))
|
||||
target = ROOT / txt_name
|
||||
if check:
|
||||
current = target.read_text(encoding="utf-8") if target.exists() else ""
|
||||
if current != rendered:
|
||||
stale.append(txt_name)
|
||||
continue
|
||||
target.write_text(rendered, encoding="utf-8", newline="\n")
|
||||
print(f"Wrote {target} ({len(rendered.splitlines())} lines)")
|
||||
|
||||
if check:
|
||||
if stale:
|
||||
print(f"out of sync: {', '.join(stale)}"
|
||||
f" - run: python scripts/plaintext_twins.py")
|
||||
return 1
|
||||
print(f"in sync: {', '.join(t for _, t in PAIRS)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@ -1,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
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.28
|
||||
# Version: v1.0.0.0.19
|
||||
|
||||
# setup_backup_schedule.ps1 — create a scheduled task for daily backups + OneDrive sync
|
||||
#
|
||||
|
||||
108
site/index.html
108
site/index.html
@ -1,108 +0,0 @@
|
||||
<!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
BIN
site/og-card.png
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
@ -1,7 +0,0 @@
|
||||
# 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: /
|
||||
@ -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
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
|
||||
@ -73,30 +73,14 @@ BeforeAll {
|
||||
$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.
|
||||
#
|
||||
# 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 {
|
||||
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
|
||||
$out = & powershell -NoProfile -ExecutionPolicy Bypass -File $path @ScriptArgs 2>&1 | ForEach-Object { "$_" }
|
||||
$result = [pscustomobject]@{
|
||||
return [pscustomobject]@{
|
||||
ExitCode = $LASTEXITCODE
|
||||
Output = ($out -join "`n")
|
||||
}
|
||||
$script:zRuns[$key] = $result
|
||||
return $result
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
|
||||
|
||||
@ -1,197 +0,0 @@
|
||||
# zdeploy lays the release tag it already knows the number for.
|
||||
#
|
||||
# Invoke-Pester .\tests
|
||||
#
|
||||
# A versioning scheme that asks every release to lay an annotated tag needs
|
||||
# something to enforce it. For a project whose build number lives OUTSIDE git -
|
||||
# in a database, say - nothing did, and one project reached thirty-eight builds
|
||||
# with four tags. Those builds were not recoverable: the number only ever
|
||||
# existed in the database.
|
||||
#
|
||||
# These tests drive REAL git against a real bare remote in a temp directory,
|
||||
# the way StartGitPull.Tests.ps1 does. A stand-in that faked git would prove
|
||||
# nothing about the two properties that matter most here: that the tag is
|
||||
# annotated and pushed, and that NOTHING this function does can fail a deploy
|
||||
# which already reached production.
|
||||
|
||||
BeforeAll {
|
||||
. (Join-Path (Split-Path -Parent $PSScriptRoot) "ZHelpers.ps1")
|
||||
|
||||
$script:tmpRoots = New-Object System.Collections.ArrayList
|
||||
|
||||
function New-TempDir {
|
||||
param([string]$Tag)
|
||||
$dir = Join-Path ([IO.Path]::GetTempPath()) ("zdeploy-tag-$Tag-" + [guid]::NewGuid().ToString('N').Substring(0, 8))
|
||||
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||
[void]$script:tmpRoots.Add($dir)
|
||||
return $dir
|
||||
}
|
||||
|
||||
function Invoke-Git {
|
||||
# Test plumbing only. The thing under test does its own git handling
|
||||
# and must not go through here.
|
||||
param([string]$In, [string[]]$GitArgs)
|
||||
Push-Location -LiteralPath $In
|
||||
try {
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$quoted = $GitArgs | ForEach-Object { if ($_ -match '\s') { '"' + $_ + '"' } else { $_ } }
|
||||
$out = & cmd /c ("git " + ($quoted -join ' ') + " 2>&1")
|
||||
if ($LASTEXITCODE -ne 0) { throw "test plumbing: git $($GitArgs -join ' ') failed in $In`n$out" }
|
||||
return $out
|
||||
}
|
||||
finally { Pop-Location }
|
||||
}
|
||||
|
||||
function New-RepoWithRemote {
|
||||
param([string]$Tag)
|
||||
$remote = New-TempDir "$Tag-remote"
|
||||
Invoke-Git -In $remote -GitArgs @('init', '--bare', '-q') | Out-Null
|
||||
$work = New-TempDir "$Tag-work"
|
||||
Invoke-Git -In $work -GitArgs @('init', '-q', '-b', 'main') | Out-Null
|
||||
Invoke-Git -In $work -GitArgs @('config', 'user.email', 'test@example.com') | Out-Null
|
||||
Invoke-Git -In $work -GitArgs @('config', 'user.name', 'Test') | Out-Null
|
||||
Set-Content -LiteralPath (Join-Path $work 'app.txt') -Value 'v1' -Encoding utf8
|
||||
Invoke-Git -In $work -GitArgs @('add', '-A') | Out-Null
|
||||
Invoke-Git -In $work -GitArgs @('commit', '-q', '-m', 'first') | Out-Null
|
||||
Invoke-Git -In $work -GitArgs @('remote', 'add', 'origin', $remote) | Out-Null
|
||||
Invoke-Git -In $work -GitArgs @('push', '-q', '-u', 'origin', 'main') | Out-Null
|
||||
return @{ Work = $work; Remote = $remote }
|
||||
}
|
||||
|
||||
function New-Proj {
|
||||
param([string]$Root, $TagOnDeploy = $true)
|
||||
$deploy = if ($null -eq $TagOnDeploy) {
|
||||
[pscustomobject]@{ zipName = 'X.zip' }
|
||||
} else {
|
||||
[pscustomobject]@{ zipName = 'X.zip'; tagOnDeploy = $TagOnDeploy }
|
||||
}
|
||||
return [pscustomobject]@{ localRoot = $Root; deploy = $deploy }
|
||||
}
|
||||
|
||||
function Get-Tags {
|
||||
param([string]$In)
|
||||
$out = Invoke-Git -In $In -GitArgs @('tag', '--list')
|
||||
return @($out | Where-Object { $_ -and $_.ToString().Trim() } |
|
||||
ForEach-Object { $_.ToString().Trim() })
|
||||
}
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
foreach ($d in $script:tmpRoots) {
|
||||
Remove-Item -LiteralPath $d -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'New-DeployTag' {
|
||||
|
||||
It 'tags the deployed commit and pushes it' {
|
||||
$r = New-RepoWithRemote 'happy'
|
||||
New-DeployTag -Proj (New-Proj $r.Work) -Version 'v0.0.1.0.39' -Note 'Build deployed'
|
||||
|
||||
Get-Tags -In $r.Work | Should -Contain 'v0.0.1.0.39'
|
||||
# On the remote too: a tag only in the local checkout is not a record.
|
||||
$remoteTags = Invoke-Git -In $r.Work -GitArgs @('ls-remote', '--tags', 'origin')
|
||||
($remoteTags -join "`n") | Should -Match 'refs/tags/v0\.0\.1\.0\.39'
|
||||
}
|
||||
|
||||
It 'writes an ANNOTATED tag carrying the version and the note' {
|
||||
$r = New-RepoWithRemote 'annotated'
|
||||
New-DeployTag -Proj (New-Proj $r.Work) -Version 'v1.2.3.4.5' -Note 'Ask AI sources'
|
||||
|
||||
# cat-file says "tag" for an annotated object and "commit" for a
|
||||
# lightweight one. The scheme asks for annotated.
|
||||
$type = Invoke-Git -In $r.Work -GitArgs @('cat-file', '-t', 'v1.2.3.4.5')
|
||||
($type -join '').Trim() | Should -Be 'tag'
|
||||
$msg = Invoke-Git -In $r.Work -GitArgs @('tag', '-l', 'v1.2.3.4.5', '--format=%(contents)')
|
||||
($msg -join ' ') | Should -Match 'v1\.2\.3\.4\.5'
|
||||
($msg -join ' ') | Should -Match 'Ask AI sources'
|
||||
}
|
||||
|
||||
It 'points the tag at the commit that was deployed' {
|
||||
$r = New-RepoWithRemote 'sha'
|
||||
$head = (Invoke-Git -In $r.Work -GitArgs @('rev-parse', 'HEAD') -join '').Trim()
|
||||
New-DeployTag -Proj (New-Proj $r.Work) -Version 'v9.9.9.9.9'
|
||||
$tagged = (Invoke-Git -In $r.Work -GitArgs @('rev-list', '-n', '1', 'v9.9.9.9.9') -join '').Trim()
|
||||
$tagged | Should -Be $head
|
||||
}
|
||||
|
||||
It 'does nothing at all unless the project opts in' {
|
||||
$r = New-RepoWithRemote 'optout'
|
||||
New-DeployTag -Proj (New-Proj $r.Work -TagOnDeploy $false) -Version 'v0.0.0.0.1'
|
||||
Get-Tags -In $r.Work | Should -BeNullOrEmpty
|
||||
|
||||
# And when the key is absent entirely, which is every existing project.
|
||||
New-DeployTag -Proj (New-Proj $r.Work -TagOnDeploy $null) -Version 'v0.0.0.0.2'
|
||||
Get-Tags -In $r.Work | Should -BeNullOrEmpty
|
||||
}
|
||||
|
||||
It 'leaves an existing tag alone rather than failing a redeploy' {
|
||||
$r = New-RepoWithRemote 'exists'
|
||||
New-DeployTag -Proj (New-Proj $r.Work) -Version 'v0.0.1.0.40' -Note 'first'
|
||||
{ New-DeployTag -Proj (New-Proj $r.Work) -Version 'v0.0.1.0.40' -Note 'second' } |
|
||||
Should -Not -Throw
|
||||
$msg = Invoke-Git -In $r.Work -GitArgs @('tag', '-l', 'v0.0.1.0.40', '--format=%(contents)')
|
||||
($msg -join ' ') | Should -Match 'first'
|
||||
($msg -join ' ') | Should -Not -Match 'second'
|
||||
}
|
||||
|
||||
It 'never throws when the directory is not a git repo' {
|
||||
$plain = New-TempDir 'notrepo'
|
||||
{ New-DeployTag -Proj (New-Proj $plain) -Version 'v0.0.0.0.3' } | Should -Not -Throw
|
||||
}
|
||||
|
||||
It 'never throws when the push fails, and still writes the tag locally' {
|
||||
# A deploy that reached production must not be reported as failed
|
||||
# because a tag could not leave the machine.
|
||||
$r = New-RepoWithRemote 'badremote'
|
||||
Invoke-Git -In $r.Work -GitArgs @('remote', 'set-url', 'origin',
|
||||
(Join-Path ([IO.Path]::GetTempPath()) 'no-such-remote-zz')) | Out-Null
|
||||
{ New-DeployTag -Proj (New-Proj $r.Work) -Version 'v0.0.1.0.41' } | Should -Not -Throw
|
||||
Get-Tags -In $r.Work | Should -Contain 'v0.0.1.0.41'
|
||||
}
|
||||
|
||||
It 'survives the deploy-wide ErrorActionPreference of Stop' {
|
||||
# The trap this whole file documents: under 'Stop', PS 5.1 turns any
|
||||
# native stderr line into a terminating error. git push writes its
|
||||
# ordinary progress to stderr, so a tag that works interactively can
|
||||
# still kill a deploy.
|
||||
$r = New-RepoWithRemote 'stoppref'
|
||||
$ErrorActionPreference = 'Stop'
|
||||
{ New-DeployTag -Proj (New-Proj $r.Work) -Version 'v0.0.1.0.42' } | Should -Not -Throw
|
||||
Get-Tags -In $r.Work | Should -Contain 'v0.0.1.0.42'
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'Get-DeployStampFiles' {
|
||||
|
||||
It 'covers every changelog name the fleet actually writes' {
|
||||
# The old inline copy knew CHANGELOG.md and not build_changelog.md,
|
||||
# a name a project's own changelog tool may write - so that stamp
|
||||
# counted as unreviewed source in the branch guard.
|
||||
$stamps = Get-DeployStampFiles
|
||||
$stamps | Should -Contain 'build-version.json'
|
||||
$stamps | Should -Contain 'CHANGELOG.md'
|
||||
$stamps | Should -Contain 'build_changelog.md'
|
||||
}
|
||||
|
||||
It 'does not count a stamp the deploy just wrote as an uncommitted change' {
|
||||
$r = New-RepoWithRemote 'stamps'
|
||||
Set-Content -LiteralPath (Join-Path $r.Work 'build_changelog.md') -Value 'x' -Encoding utf8
|
||||
Invoke-Git -In $r.Work -GitArgs @('add', '-A') | Out-Null
|
||||
Invoke-Git -In $r.Work -GitArgs @('commit', '-q', '-m', 'add changelog') | Out-Null
|
||||
Set-Content -LiteralPath (Join-Path $r.Work 'build_changelog.md') -Value 'stamped by the deploy' -Encoding utf8
|
||||
|
||||
Push-Location -LiteralPath $r.Work
|
||||
try { $changes = Get-TrackedChangesExcludingStamps } finally { Pop-Location }
|
||||
$changes.Count | Should -Be 0
|
||||
}
|
||||
|
||||
It 'still reports real source changes' {
|
||||
$r = New-RepoWithRemote 'realchange'
|
||||
Set-Content -LiteralPath (Join-Path $r.Work 'app.txt') -Value 'edited' -Encoding utf8
|
||||
Push-Location -LiteralPath $r.Work
|
||||
try { $changes = Get-TrackedChangesExcludingStamps } finally { Pop-Location }
|
||||
$changes.Count | Should -Be 1
|
||||
($changes -join ' ') | Should -Match 'app\.txt'
|
||||
}
|
||||
}
|
||||
@ -1,82 +0,0 @@
|
||||
# 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"'
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
|
||||
|
||||
@ -1,90 +0,0 @@
|
||||
# evomedia.net Token Savers - https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels - dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
|
||||
# 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
|
||||
|
||||
@ -1,73 +0,0 @@
|
||||
# The .txt twins are generated, and this is what makes that true.
|
||||
#
|
||||
# Invoke-Pester .\tests
|
||||
#
|
||||
# scripts/plaintext_twins.py has always shipped a --check mode, and its
|
||||
# docstring claimed "the test suite runs --check". Nothing ran it. So README.txt
|
||||
# could drift from README.md silently, and CHANGELOG.txt - which no generator
|
||||
# covered at all - actually did.
|
||||
#
|
||||
# A twin that disagrees with the file it mirrors is worse than no twin: it is a
|
||||
# second document that looks authoritative and is wrong.
|
||||
|
||||
# -Skip is evaluated during DISCOVERY, before BeforeAll runs, so a python
|
||||
# lookup done in BeforeAll leaves the flag $null and the real check silently
|
||||
# skips - which is how this test first "passed" while verifying nothing.
|
||||
BeforeDiscovery {
|
||||
$script:Python = $null
|
||||
foreach ($candidate in @('python', 'python3', 'py')) {
|
||||
$cmd = Get-Command $candidate -ErrorAction SilentlyContinue
|
||||
if ($cmd) { $script:Python = $cmd.Source; break }
|
||||
}
|
||||
}
|
||||
|
||||
BeforeAll {
|
||||
$script:RepoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$script:Generator = Join-Path $script:RepoRoot "scripts\plaintext_twins.py"
|
||||
|
||||
$script:Python = $null
|
||||
foreach ($candidate in @('python', 'python3', 'py')) {
|
||||
$cmd = Get-Command $candidate -ErrorAction SilentlyContinue
|
||||
if ($cmd) { $script:Python = $cmd.Source; break }
|
||||
}
|
||||
}
|
||||
|
||||
Describe "plain-text twins" {
|
||||
|
||||
It "the generator is where the tests and the docs say it is" {
|
||||
Test-Path -LiteralPath $script:Generator | Should -BeTrue
|
||||
}
|
||||
|
||||
# Asks the REPOSITORY what markdown it has, not the generator what it was
|
||||
# told about. Scraping the generator's own list could only ever prove the
|
||||
# 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
|
||||
|
||||
$mds.Count | Should -BeGreaterThan 0 -Because "the repo documents itself in markdown"
|
||||
foreach ($md in $mds) {
|
||||
$txt = [IO.Path]::ChangeExtension($md.FullName, ".txt")
|
||||
Test-Path -LiteralPath $txt |
|
||||
Should -BeTrue -Because "$($md.Name) owes a twin at $(Split-Path -Leaf $txt)"
|
||||
}
|
||||
}
|
||||
|
||||
It "every twin is in sync with its markdown" -Skip:(-not $script:Python) {
|
||||
Push-Location $script:RepoRoot
|
||||
try {
|
||||
$output = & $script:Python $script:Generator --check 2>&1
|
||||
$code = $LASTEXITCODE
|
||||
}
|
||||
finally { Pop-Location }
|
||||
|
||||
$code | Should -Be 0 -Because ($output -join "`n")
|
||||
}
|
||||
|
||||
It "reports the python that was missing rather than passing quietly" -Skip:([bool]$script:Python) {
|
||||
# Not a pass. If this is the test you are reading, --check never ran:
|
||||
# install python, or regenerate the twins by hand before shipping.
|
||||
Set-ItResult -Inconclusive -Because "no python on PATH, so the twins were not verified"
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
|
||||
@ -40,18 +40,31 @@ BeforeAll {
|
||||
}
|
||||
)
|
||||
|
||||
# The rules live in sanitization-patterns.psd1, not here, so the
|
||||
# publisher in the private tree can read the SAME list. It used to keep
|
||||
# its own, narrower one - secrets only, no identity rules - and therefore
|
||||
# reported "clean" on files this suite rejects (evo.scripts#106).
|
||||
$patternFile = Join-Path $PSScriptRoot 'sanitization-patterns.psd1'
|
||||
if (-not (Test-Path -LiteralPath $patternFile)) {
|
||||
throw "sanitization-patterns.psd1 is missing - the denylist has no source."
|
||||
}
|
||||
$script:Denied = (Import-PowerShellDataFile -LiteralPath $patternFile).Denied
|
||||
if (-not $script:Denied -or $script:Denied.Count -eq 0) {
|
||||
throw "sanitization-patterns.psd1 defined no rules - refusing to pass vacuously."
|
||||
}
|
||||
# name -> what it is, so a failure explains itself
|
||||
# pattern -> regex, case-insensitive
|
||||
# Kept narrow on purpose: "evomedia.net" alone is legitimate here (the
|
||||
# attribution header and the repo URL), so only the drive path and specific
|
||||
# internal hosts are matched.
|
||||
$script:Denied = @(
|
||||
@{ Name = 'private project name'; Pattern = '\b(EvoCivilCode|EvoPlatform|DocketMail|SmartPlant\w*|ProvenSheet|evoehs|evoproven|evoaicc|evolocate|evoplatform)\b' }
|
||||
@{ 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-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 = 'local drive path'; Pattern = '[A-Za-z]:\\\\?evomedia\.net' }
|
||||
@{ Name = 'operator home path'; Pattern = '/home/ubuntu/' }
|
||||
@{ Name = 'real pem key name'; Pattern = 'evomedia-prod\.pem' }
|
||||
# RFC 5737 reserves 203.0.113.0/24 for documentation - that one is the
|
||||
# correct placeholder and must stay allowed, as are loopback and the
|
||||
# private ranges. Anything else that looks like a public IPv4 literal is
|
||||
# suspect.
|
||||
#
|
||||
# The boundaries are [\d.] rather than \d on purpose: this toolkit's own
|
||||
# 5-segment version (v1.0.0.0.14) contains "0.0.0.14", which a plain
|
||||
# digit boundary happily reads as an address. Refusing a match that
|
||||
# touches another dot rules out every version string without weakening
|
||||
# detection of a real address, which is always delimited by whitespace
|
||||
# or quotes.
|
||||
@{ Name = 'non-documentation IP'; Pattern = '(?<![\d.])(?!203\.0\.113\.)(?!127\.0\.0\.1)(?!0\.0\.0\.0)(?!255\.)(?!10\.)(?!192\.168\.)(?!172\.(1[6-9]|2\d|3[01])\.)\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(?![\d.])' }
|
||||
)
|
||||
|
||||
function Get-Hits {
|
||||
param([string]$Pattern)
|
||||
@ -89,25 +102,6 @@ Describe "public repo carries no private detail" {
|
||||
$hits | Should -BeNullOrEmpty -Because "these look like private detail copied in from the internal toolkit:`n$($hits -join "`n")"
|
||||
}
|
||||
|
||||
It "catches the current spelling <Sample>" -ForEach @(
|
||||
@{ Rule = 'current product name'; Sample = 'evo.ehs answers build_version' }
|
||||
@{ Rule = 'current product name'; Sample = 'evo-ai answers version on /health' }
|
||||
@{ Rule = 'current product name'; Sample = 'upstream evoehs_app:80' }
|
||||
@{ Rule = 'internal issue reference'; Sample = 'see evo.scripts#101 for the trap' }
|
||||
) {
|
||||
# The rename went past the old pattern: the dot and the hyphen break
|
||||
# the word and the underscore hides the boundary, so a suite that ran
|
||||
# green was trusted on a tree that named the fleet.
|
||||
$pattern = ($script:Denied | Where-Object { $_.Name -eq $Rule }).Pattern
|
||||
$pattern | Should -Not -BeNullOrEmpty
|
||||
($Sample -match $pattern) | Should -BeTrue
|
||||
}
|
||||
|
||||
It "still allows this repo's own name" {
|
||||
$pattern = ($script:Denied | Where-Object { $_.Name -eq 'current product name' }).Pattern
|
||||
('https://github.com/evomedia-net/evo.zscripts' -match $pattern) | Should -BeFalse
|
||||
}
|
||||
|
||||
It "still detects a planted violation" {
|
||||
# Mutation check. Without this the suite passes just as happily when the
|
||||
# patterns are broken as when the repo is clean - the failure mode that
|
||||
|
||||
@ -1,240 +0,0 @@
|
||||
# zstart's auto-pull must never stand between the user and a running server (#130).
|
||||
#
|
||||
# Invoke-Pester .\tests
|
||||
#
|
||||
# The report was "I merged it but not sure why it crashed, should have skipped
|
||||
# it and moved on". It crashed on a pull that SUCCEEDED:
|
||||
#
|
||||
# git : From https://github.com/example/some-app
|
||||
# At ZStart.ps1:206 char:24
|
||||
# + $pullOut = git pull --ff-only 2>&1
|
||||
#
|
||||
# Under $ErrorActionPreference = 'Stop', a stderr redirect on a native command
|
||||
# in Windows PowerShell 5.1 wraps every stderr line in a terminating
|
||||
# ErrorRecord - and git writes ordinary fetch progress ("From ...") to stderr.
|
||||
# So the try block died before its own "Auto-pull skipped" branch could run.
|
||||
#
|
||||
# These tests drive REAL git under 'Stop' on the same host the defect lives
|
||||
# on. A stand-in that faked git's output would prove nothing about the stream
|
||||
# semantics that are the entire bug; one test asserts the fixture really does
|
||||
# put that "From ..." line on stderr, so the reproduction cannot quietly go
|
||||
# stale the way an LF changelog fixture once did.
|
||||
#
|
||||
# ZHelpers.ps1 is dot-sourced rather than ZStart.ps1, which runs its main flow
|
||||
# on load. That is also why the logic moved into a helper: it was untestable
|
||||
# where it sat.
|
||||
|
||||
BeforeAll {
|
||||
. (Join-Path (Split-Path -Parent $PSScriptRoot) "ZHelpers.ps1")
|
||||
|
||||
$script:tmpRoots = New-Object System.Collections.ArrayList
|
||||
|
||||
function New-TempDir {
|
||||
param([string]$Tag)
|
||||
$dir = Join-Path ([IO.Path]::GetTempPath()) ("zstart-pull-$Tag-" + [guid]::NewGuid().ToString('N').Substring(0, 8))
|
||||
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||
[void]$script:tmpRoots.Add($dir)
|
||||
return $dir
|
||||
}
|
||||
|
||||
function Invoke-Git {
|
||||
# Test plumbing only. Runs git quietly from a directory and fails the
|
||||
# test loudly if it did not work - the thing under test does its own
|
||||
# git handling and must not go through here.
|
||||
param([string]$In, [string[]]$GitArgs)
|
||||
Push-Location -LiteralPath $In
|
||||
try {
|
||||
$ErrorActionPreference = 'Continue'
|
||||
# Quote anything with whitespace: a Windows temp root usually
|
||||
# sits under a user profile whose name has a space in it, and
|
||||
# an unquoted path splits into two arguments on the way through
|
||||
# cmd.
|
||||
$quoted = $GitArgs | ForEach-Object { if ($_ -match '\s') { '"' + $_ + '"' } else { $_ } }
|
||||
$out = & cmd /c ("git " + ($quoted -join ' ') + " 2>&1")
|
||||
if ($LASTEXITCODE -ne 0) { throw "test plumbing: git $($GitArgs -join ' ') failed in $In`n$out" }
|
||||
return $out
|
||||
} finally { Pop-Location }
|
||||
}
|
||||
|
||||
function New-ClonePair {
|
||||
# A bare "origin" and a working clone tracking main, one commit in.
|
||||
# Returns @{ Bare; Clone } and leaves a helper to advance origin.
|
||||
$bare = New-TempDir 'origin'
|
||||
Invoke-Git $bare @('init', '--bare', '--initial-branch=main', '--quiet') | Out-Null
|
||||
$seed = New-TempDir 'seed'
|
||||
Invoke-Git $seed @('init', '--initial-branch=main', '--quiet') | Out-Null
|
||||
Invoke-Git $seed @('config', 'user.email', 'test@example.invalid') | Out-Null
|
||||
Invoke-Git $seed @('config', 'user.name', 'zstart test') | Out-Null
|
||||
Set-Content -LiteralPath (Join-Path $seed 'a.txt') -Value 'one'
|
||||
Invoke-Git $seed @('add', '.') | Out-Null
|
||||
Invoke-Git $seed @('commit', '-q', '-m', 'one') | Out-Null
|
||||
Invoke-Git $seed @('remote', 'add', 'origin', $bare) | Out-Null
|
||||
Invoke-Git $seed @('push', '-q', '-u', 'origin', 'main') | Out-Null
|
||||
|
||||
$clone = New-TempDir 'clone'
|
||||
Invoke-Git (Split-Path -Parent $clone) @('clone', '-q', $bare, $clone) | Out-Null
|
||||
Invoke-Git $clone @('config', 'user.email', 'test@example.invalid') | Out-Null
|
||||
Invoke-Git $clone @('config', 'user.name', 'zstart test') | Out-Null
|
||||
return [pscustomobject]@{ Bare = $bare; Clone = $clone; Seed = $seed }
|
||||
}
|
||||
|
||||
function Add-OriginCommit {
|
||||
# Advance origin from the seed checkout, so the clone has something
|
||||
# to fetch - which is exactly what makes git print "From ..." on
|
||||
# stderr.
|
||||
param($Pair, [string]$Name = 'two')
|
||||
Set-Content -LiteralPath (Join-Path $Pair.Seed "$Name.txt") -Value $Name
|
||||
Invoke-Git $Pair.Seed @('add', '.') | Out-Null
|
||||
Invoke-Git $Pair.Seed @('commit', '-q', '-m', $Name) | Out-Null
|
||||
Invoke-Git $Pair.Seed @('push', '-q', 'origin', 'main') | Out-Null
|
||||
}
|
||||
|
||||
function Get-Head {
|
||||
param([string]$Repo)
|
||||
return (Invoke-Git $Repo @('rev-parse', 'HEAD') | Select-Object -Last 1).ToString().Trim()
|
||||
}
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
foreach ($d in $script:tmpRoots) {
|
||||
try { Remove-Item -LiteralPath $d -Recurse -Force -ErrorAction SilentlyContinue } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Invoke-StartGitPull" {
|
||||
|
||||
Context "the reported case: origin has new commits" {
|
||||
|
||||
BeforeAll {
|
||||
$script:pair = New-ClonePair
|
||||
Add-OriginCommit $script:pair
|
||||
$script:originTip = Get-Head $script:pair.Seed
|
||||
}
|
||||
|
||||
It "the fixture really puts fetch progress on stderr - the shape of the bug" {
|
||||
# Checked on a second clone so the one under test is still behind.
|
||||
$probe = New-TempDir 'probe'
|
||||
Invoke-Git (Split-Path -Parent $probe) @('clone', '-q', $script:pair.Bare, $probe) | Out-Null
|
||||
Add-OriginCommit $script:pair 'three'
|
||||
Push-Location -LiteralPath $probe
|
||||
try {
|
||||
$ErrorActionPreference = 'Continue'
|
||||
# stderr only: stdout is dropped, so anything captured came
|
||||
# from the stream that ErrorRecords are made from.
|
||||
$stderr = & cmd /c "git fetch origin 2>&1 1>nul"
|
||||
} finally { Pop-Location }
|
||||
($stderr -join "`n") | Should -Match '(?m)^From '
|
||||
}
|
||||
|
||||
It "does not abort under ErrorActionPreference = 'Stop'" {
|
||||
$ErrorActionPreference = 'Stop'
|
||||
{ $script:r = Invoke-StartGitPull -Root $script:pair.Clone } | Should -Not -Throw
|
||||
}
|
||||
|
||||
It "reports success" {
|
||||
$script:r.Ok | Should -BeTrue
|
||||
$script:r.Skipped | Should -BeFalse
|
||||
$script:r.Message | Should -Match '^Now at: '
|
||||
}
|
||||
|
||||
It "actually fast-forwarded the checkout" {
|
||||
Get-Head $script:pair.Clone | Should -Be (Get-Head $script:pair.Seed)
|
||||
}
|
||||
|
||||
It "leaves the caller's ErrorActionPreference as it found it" {
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Invoke-StartGitPull -Root $script:pair.Clone | Out-Null
|
||||
$ErrorActionPreference | Should -Be 'Stop'
|
||||
}
|
||||
}
|
||||
|
||||
Context "nothing to fetch" {
|
||||
|
||||
It "is Ok and says so, not a skip" {
|
||||
$pair = New-ClonePair
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$r = Invoke-StartGitPull -Root $pair.Clone
|
||||
$r.Ok | Should -BeTrue
|
||||
$r.Message | Should -Be 'Already up to date.'
|
||||
}
|
||||
}
|
||||
|
||||
Context "a pull that cannot complete" {
|
||||
|
||||
It "diverged history is reported, not thrown, and the checkout is left alone" {
|
||||
$pair = New-ClonePair
|
||||
# Local commit on the clone AND a different one on origin.
|
||||
Set-Content -LiteralPath (Join-Path $pair.Clone 'local.txt') -Value 'mine'
|
||||
Invoke-Git $pair.Clone @('add', '.') | Out-Null
|
||||
Invoke-Git $pair.Clone @('commit', '-q', '-m', 'local') | Out-Null
|
||||
$localTip = Get-Head $pair.Clone
|
||||
Add-OriginCommit $pair 'theirs'
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
{ $script:div = Invoke-StartGitPull -Root $pair.Clone } | Should -Not -Throw
|
||||
$script:div.Ok | Should -BeFalse
|
||||
$script:div.Skipped | Should -BeFalse
|
||||
$script:div.Message | Should -Match 'cannot fast-forward'
|
||||
Get-Head $pair.Clone | Should -Be $localTip
|
||||
}
|
||||
|
||||
It "a branch with no upstream is skipped - and never switched away from" {
|
||||
$pair = New-ClonePair
|
||||
Invoke-Git $pair.Clone @('checkout', '-q', '-b', 'feature/thing') | Out-Null
|
||||
$ErrorActionPreference = 'Stop'
|
||||
{ $script:noup = Invoke-StartGitPull -Root $pair.Clone } | Should -Not -Throw
|
||||
$script:noup.Skipped | Should -BeTrue
|
||||
$script:noup.Message | Should -Match "no upstream"
|
||||
(Invoke-Git $pair.Clone @('rev-parse', '--abbrev-ref', 'HEAD') | Select-Object -Last 1).ToString().Trim() |
|
||||
Should -Be 'feature/thing'
|
||||
}
|
||||
|
||||
It "a directory that is not a repo is skipped, not thrown" {
|
||||
$dir = New-TempDir 'norepo'
|
||||
$ErrorActionPreference = 'Stop'
|
||||
{ $script:norepo = Invoke-StartGitPull -Root $dir } | Should -Not -Throw
|
||||
$script:norepo.Skipped | Should -BeTrue
|
||||
$script:norepo.Message | Should -Match 'not a git repo'
|
||||
}
|
||||
}
|
||||
|
||||
Context "housekeeping" {
|
||||
|
||||
It "restores GIT_TERMINAL_PROMPT to whatever it was" {
|
||||
$pair = New-ClonePair
|
||||
$prev = $env:GIT_TERMINAL_PROMPT
|
||||
try {
|
||||
$env:GIT_TERMINAL_PROMPT = 'sentinel'
|
||||
Invoke-StartGitPull -Root $pair.Clone | Out-Null
|
||||
$env:GIT_TERMINAL_PROMPT | Should -Be 'sentinel'
|
||||
} finally { $env:GIT_TERMINAL_PROMPT = $prev }
|
||||
}
|
||||
|
||||
It "returns to the directory it was called from" {
|
||||
$pair = New-ClonePair
|
||||
$here = (Get-Location).Path
|
||||
Invoke-StartGitPull -Root $pair.Clone | Out-Null
|
||||
(Get-Location).Path | Should -Be $here
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Describe "ZStart.ps1 uses the helper" {
|
||||
|
||||
BeforeAll {
|
||||
$script:zstart = Get-Content -Raw (Join-Path (Split-Path -Parent $PSScriptRoot) "ZStart.ps1")
|
||||
}
|
||||
|
||||
It "no longer carries its own redirected pull - the line that crashed" {
|
||||
$script:zstart | Should -Not -Match 'git pull --ff-only 2>&1'
|
||||
}
|
||||
|
||||
It "calls Invoke-StartGitPull" {
|
||||
$script:zstart | Should -Match 'Invoke-StartGitPull -Root'
|
||||
}
|
||||
|
||||
It "still tells the user when it skipped, and how to stop it trying" {
|
||||
$script:zstart | Should -Match 'Auto-pull skipped'
|
||||
$script:zstart | Should -Match 'start\.gitPull=false'
|
||||
}
|
||||
}
|
||||
@ -1,96 +0,0 @@
|
||||
# Deploy-verification planning.
|
||||
#
|
||||
# Invoke-Pester .\tests
|
||||
#
|
||||
# The wrong-vhost failure was never about parsing a response - it was about
|
||||
# WHICH channel got asked. So the channel-selection rules live in a pure
|
||||
# function (Get-VerifyAttempts) and are pinned here, where they can be tested
|
||||
# without an EC2 box: a project with no domain must never produce an edge
|
||||
# attempt, because an edge request with no Host header can only reach the
|
||||
# default vhost - which is a different product. That exact gap read one
|
||||
# product's build number during another's deploys, twice in one day.
|
||||
#
|
||||
# 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($Verify = $null, $Domain = $null, $Deploy = $null)
|
||||
$p = [pscustomobject]@{}
|
||||
if ($null -ne $Verify) { $p | Add-Member verify ([pscustomobject]$Verify) }
|
||||
if ($null -ne $Domain) { $p | Add-Member domain $Domain }
|
||||
if ($null -ne $Deploy) { $p | Add-Member deploy ([pscustomobject]$Deploy) }
|
||||
return $p
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Get-VerifyAttempts" {
|
||||
|
||||
It "puts the docker-network read first when configured" {
|
||||
$proj = New-Proj -Verify @{ viaProxy = "evo_edge_proxy"; upstream = "app:80"; port = 8005 } -Domain "x.example"
|
||||
$attempts = Get-VerifyAttempts -Proj $proj -ExecCmd "docker exec ..."
|
||||
$attempts[0].Kind | Should -Be 'exec'
|
||||
($attempts | ForEach-Object Kind) | Should -Be @('exec', 'port', 'edge')
|
||||
}
|
||||
|
||||
It "never asks the edge for a project with no domain (the wrong-vhost trap)" {
|
||||
# The shape that hit it: viaProxy + port, no domain. The old code
|
||||
# fell back to the bare IP here and read another product's counter.
|
||||
$proj = New-Proj -Verify @{ viaProxy = "evo_edge_proxy"; upstream = "deploy-app-1:8000"; port = 8005; path = "/health" }
|
||||
$attempts = Get-VerifyAttempts -Proj $proj -ExecCmd "docker exec ..."
|
||||
($attempts | ForEach-Object Kind) | Should -Not -Contain 'edge'
|
||||
}
|
||||
|
||||
It "returns an empty plan when nothing trustworthy exists" {
|
||||
# No verify config, no domain: the caller must SKIP, not guess.
|
||||
$attempts = Get-VerifyAttempts -Proj (New-Proj) -ExecCmd ""
|
||||
$attempts.Count | Should -Be 0
|
||||
}
|
||||
|
||||
It "keeps the edge for a project with a domain, with its Host header" {
|
||||
$proj = New-Proj -Domain "jwks.example"
|
||||
$attempts = Get-VerifyAttempts -Proj $proj -ExecCmd ""
|
||||
$attempts.Count | Should -Be 1
|
||||
$attempts[0].Kind | Should -Be 'edge'
|
||||
$attempts[0].HostHeader | Should -Be "jwks.example"
|
||||
}
|
||||
|
||||
It "prefers deploy.verifyHost over domain for the edge Host header" {
|
||||
$proj = New-Proj -Domain "old.example" -Deploy @{ verifyHost = "new.example" }
|
||||
$attempts = Get-VerifyAttempts -Proj $proj -ExecCmd ""
|
||||
$attempts[0].HostHeader | Should -Be "new.example"
|
||||
}
|
||||
|
||||
It "carries the verify path into the port attempt, defaulting sensibly" {
|
||||
$proj = New-Proj -Verify @{ port = 8005; path = "/health" }
|
||||
(Get-VerifyAttempts -Proj $proj -ExecCmd "")[0].Path | Should -Be "/health"
|
||||
$proj2 = New-Proj -Verify @{ port = 9000 }
|
||||
(Get-VerifyAttempts -Proj $proj2 -ExecCmd "")[0].Path | Should -Be "/api/build-version"
|
||||
}
|
||||
|
||||
It "survives the PS 5.1 one-element unroll" {
|
||||
# A single attempt must still come back as something with .Count and
|
||||
# index access - the pipeline trap that deadlocked ztests day 2.
|
||||
$proj = New-Proj -Verify @{ port = 8005 }
|
||||
$attempts = Get-VerifyAttempts -Proj $proj -ExecCmd ""
|
||||
$attempts.Count | Should -Be 1
|
||||
$attempts[0].Kind | Should -Be 'port'
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Get-VerifyTimeout" {
|
||||
|
||||
It "uses the caller's default when the project says nothing" {
|
||||
Get-VerifyTimeout -Proj (New-Proj) -DefaultSec 30 | Should -Be 30
|
||||
}
|
||||
|
||||
It "lets a slow-booting project widen its own window" {
|
||||
# An app that runs database migrations in its entrypoint exceeds
|
||||
# 30s on every deploy that ships one, and a warning that fires on
|
||||
# routine success trains people to ignore the real one.
|
||||
$proj = New-Proj -Verify @{ viaProxy = "p"; upstream = "u"; timeoutSeconds = 120 }
|
||||
Get-VerifyTimeout -Proj $proj -DefaultSec 30 | Should -Be 120
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
|
||||
|
||||
@ -1,58 +0,0 @@
|
||||
<#
|
||||
Patterns the PUBLIC repo must never contain.
|
||||
|
||||
Extracted from Sanitization.Tests.ps1 so that the test here and the
|
||||
publisher in the private tree read ONE list instead of keeping two.
|
||||
They had two, and they disagreed: the publisher's scan looked only for
|
||||
secrets - keys, private-key blocks, ssh targets - while these rules are
|
||||
about IDENTITY: internal project names, product domains, private-only
|
||||
script names, operator paths.
|
||||
|
||||
So the publisher reported "clean" on files this suite rejects, and would
|
||||
have published a tree that fails the public repo's own tests
|
||||
(evo.scripts#106). One list, and that cannot drift apart again.
|
||||
|
||||
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 you publish. Add a pattern whenever a new
|
||||
private identifier appears; a stale entry costs nothing.
|
||||
|
||||
Kept narrow on purpose: "evomedia.net" alone is legitimate here - the
|
||||
attribution header and the repo URL both carry it - so only the drive
|
||||
path and specific internal hosts are matched.
|
||||
#>
|
||||
@{
|
||||
Denied = @(
|
||||
# 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
|
||||
# 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
|
||||
# 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 = 'internal issue reference'; Pattern = 'evo\.scripts#\d+' }
|
||||
@{ 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|zresume|swag_set_owner|provision_demo|apply_platform_config_fixes)\b' }
|
||||
@{ Name = 'local drive path'; Pattern = '[A-Za-z]:\\\\?evomedia\.net' }
|
||||
@{ Name = 'operator home path'; Pattern = '/home/ubuntu/' }
|
||||
@{ Name = 'real pem key name'; Pattern = 'evomedia-prod\.pem' }
|
||||
# RFC 5737 reserves 203.0.113.0/24 for documentation - that one is the
|
||||
# correct placeholder and must stay allowed, as are loopback and the
|
||||
# private ranges. Anything else that looks like a public IPv4 literal is
|
||||
# suspect.
|
||||
#
|
||||
# The boundaries are [\d.] rather than \d on purpose: this toolkit's own
|
||||
# 5-segment version (v1.0.0.0.14) contains "0.0.0.14", which a plain
|
||||
# digit boundary happily reads as an address. Refusing a match that
|
||||
# touches another dot rules out every version string without weakening
|
||||
# detection of a real address, which is always delimited by whitespace
|
||||
# or quotes.
|
||||
@{ Name = 'non-documentation IP'; Pattern = '(?<![\d.])(?!203\.0\.113\.)(?!127\.0\.0\.1)(?!0\.0\.0\.0)(?!255\.)(?!10\.)(?!192\.168\.)(?!172\.(1[6-9]|2\d|3[01])\.)\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(?![\d.])' }
|
||||
|
||||
)
|
||||
}
|
||||
@ -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
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.28
|
||||
# Version: v1.0.0.0.19
|
||||
|
||||
# token-count.ps1 — measure script output volume to estimate AI agent token costs.
|
||||
#
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
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
|
||||
REM Version: v1.0.0.0.19
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zbackup.ps1" %*
|
||||
|
||||
@ -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
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.28
|
||||
# Version: v1.0.0.0.19
|
||||
|
||||
# zbackup.ps1 — local backups: zip project sources (plus a Postgres dump when the
|
||||
# project's .env has a DATABASE_URL) into the backups folder.
|
||||
|
||||
@ -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
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.28
|
||||
# Version: v1.0.0.0.19
|
||||
|
||||
# zbackup_and_sync.ps1 — run backups, then sync the backups folder offsite.
|
||||
#
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
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
|
||||
REM Version: v1.0.0.0.19
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zbackup_ec2.ps1" %*
|
||||
|
||||
@ -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
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.28
|
||||
# Version: v1.0.0.0.19
|
||||
|
||||
# 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).
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
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
|
||||
REM Version: v1.0.0.0.19
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zchecksums.ps1" %*
|
||||
|
||||
@ -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
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.28
|
||||
# Version: v1.0.0.0.19
|
||||
|
||||
# zchecksums.ps1 - verify (or regenerate) SHA-256 checksums for the scripts.
|
||||
#
|
||||
|
||||
@ -54,7 +54,6 @@
|
||||
"deploy": {
|
||||
"zipName": "PyAppDeploy.zip",
|
||||
"gitPull": true,
|
||||
"tagOnDeploy": false,
|
||||
"exclude": [
|
||||
"docs"
|
||||
],
|
||||
@ -122,7 +121,6 @@
|
||||
"analytics": {
|
||||
"label": "Analytics (any docker compose app)",
|
||||
"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",
|
||||
"domain": "analytics.yourdomain.com",
|
||||
"remote": {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
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
|
||||
REM Version: v1.0.0.0.19
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zdeploy.ps1" %*
|
||||
|
||||
618
zdeploy.ps1
618
zdeploy.ps1
@ -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
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.28
|
||||
# Version: v1.0.0.0.19
|
||||
|
||||
# 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
|
||||
@ -9,9 +9,6 @@
|
||||
#
|
||||
# Usage:
|
||||
# zdeploy <project> [<project> ...] [-Note "message"]
|
||||
# zdeploy -Scan # report what is merged but not shipped, then offer to deploy it
|
||||
# zdeploy -s -Yes # same, unattended (no confirmation prompt)
|
||||
# zdeploy -s <project> ... # scan only these
|
||||
# zdeploy all # ztokens first, then every project (edge kinds next), stop at first failure
|
||||
# zdeploy ztokens # refresh the live-usage stats (see below)
|
||||
#
|
||||
@ -21,11 +18,12 @@
|
||||
# zdeploy all -Note "weekly release"
|
||||
# zdeploy ztokens evo # refresh token-stats.json, then ship the site with it
|
||||
#
|
||||
# "ztokens" is a pseudo-project, not a zconfig entry: it runs `ztokens -Publish`
|
||||
# from the sibling ztokens repo, refreshing the token-stats.json the public
|
||||
# zscripts page charts. `all` runs it first automatically; called standalone,
|
||||
# list it before a site project (as above) so that project's deploy zip picks
|
||||
# up the freshly written file.
|
||||
# "ztokens" is an OPTIONAL pseudo-project, not a zconfig entry: it runs
|
||||
# `ztokens -Publish` from a sibling ztokens checkout, if you have one, to
|
||||
# refresh a token-stats.json a site can chart. With no such checkout the step
|
||||
# prints a skip and the rest of the run is unaffected. `all` runs it first
|
||||
# automatically; called standalone, list it before a site project (as above) so
|
||||
# that project's deploy zip picks up the freshly written file.
|
||||
#
|
||||
# Flow (python/vite/nextjs): zip source -> free server disk space -> scp up ->
|
||||
# unzip into remote.path (preserving server-side .env* files and anything in
|
||||
@ -46,13 +44,7 @@
|
||||
param(
|
||||
[Parameter(Position = 0, ValueFromRemainingArguments = $true)]
|
||||
[string[]]$Projects = @(),
|
||||
[string]$Note = "Build deployed",
|
||||
# -Scan / -s: report which projects have work on the default branch that is
|
||||
# not live yet, then offer to deploy exactly those. See Get-DeployStatus.
|
||||
[Alias('s')][switch]$Scan,
|
||||
# Skip the confirmation prompt after a scan. Needed for unattended runs -
|
||||
# Read-Host has no answer in a non-interactive shell and would throw.
|
||||
[switch]$Yes
|
||||
[string]$Note = "Build deployed"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
@ -74,170 +66,13 @@ if (-not (Test-Path -LiteralPath $TempRoot)) {
|
||||
New-Item -ItemType Directory -Path $TempRoot -Force | Out-Null
|
||||
}
|
||||
|
||||
# ── Scan: what is merged but not shipped ─────────────────────────────────────
|
||||
# Answers "which projects have work on the default branch that is not live?"
|
||||
#
|
||||
# WHAT IT COMPARES
|
||||
# ----------------
|
||||
# Every deploy leaves .last_deploy_sha and .last_deploy_utc in the project's
|
||||
# remote path. The SHA is the real answer: deployed commit versus the current
|
||||
# default-branch tip, exact regardless of clocks.
|
||||
#
|
||||
# .last_deploy_sha only exists from this change onward, so a project that has
|
||||
# not been deployed since falls back to comparing the tip's COMMIT TIME against
|
||||
# the deploy time. That is approximate on purpose and is labelled "~" in the
|
||||
# output: commit time is when the work was authored, not when it merged, so a
|
||||
# long-lived branch merged today carries an old timestamp and can read as
|
||||
# already-shipped. The fallback disappears the first time each project deploys.
|
||||
#
|
||||
# WHAT IT DOES NOT DO
|
||||
# -------------------
|
||||
# It does not judge whether the pending commits change anything shippable - a
|
||||
# README-only commit still reads as pending. Deploying that is wasteful, not
|
||||
# wrong, and the alternative (guessing which paths matter per project kind) is
|
||||
# the sort of cleverness that eventually skips a real change.
|
||||
function Get-DeployStatus {
|
||||
param([string[]]$Keys)
|
||||
|
||||
$cfgLocal = Get-ZConfig
|
||||
$rows = @()
|
||||
|
||||
# One ssh for every project rather than one each: this is a status read
|
||||
# people will run often, and 15 round trips to answer one question is the
|
||||
# difference between a habit and a chore. No $( ) and no embedded double
|
||||
# quotes - see the note on Invoke-Ec2Step.
|
||||
$parts = @()
|
||||
foreach ($k in $Keys) {
|
||||
$p = $cfgLocal.projects.$k
|
||||
if (-not ($p -and $p.remote -and $p.remote.path)) { continue }
|
||||
$rp = $p.remote.path
|
||||
$parts += "printf '$k\t'; cat $rp/.last_deploy_sha 2>/dev/null | tr -d '\n'; printf '\t'; cat $rp/.last_deploy_utc 2>/dev/null | tr -d '\n'; printf '\n';"
|
||||
}
|
||||
$remote = @{}
|
||||
if ($parts.Count -gt 0) {
|
||||
$prev = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
# Deliberately not Invoke-Ec2Step: that prints a step header and throws
|
||||
# on failure. A scan wants the output captured, and a box that cannot be
|
||||
# reached should degrade to "unknown" rather than abort the report.
|
||||
$sshOpts = Get-Ec2SshOpts
|
||||
$lines = ssh @sshOpts -i $cfgLocal.ec2.pemKey (Get-Ec2Target) ($parts -join ' ') 2>&1 |
|
||||
ForEach-Object { "$_" }
|
||||
$ErrorActionPreference = $prev
|
||||
foreach ($line in $lines) {
|
||||
$f = $line -split "`t"
|
||||
if ($f.Count -ge 3) { $remote[$f[0]] = @{ Sha = $f[1].Trim(); Utc = $f[2].Trim() } }
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($k in $Keys) {
|
||||
$p = $cfgLocal.projects.$k
|
||||
$row = [ordered]@{ Key = $k; Kind = $p.kind; State = ''; Detail = ''; Ahead = 0 }
|
||||
$root = $p.localRoot
|
||||
|
||||
# Not a test for .git in $root: a localRoot may point INTO a repo
|
||||
# rather than at its top, when the deployable app is a subdirectory of
|
||||
# the checkout. Testing for the folder reported every such project as
|
||||
# having no checkout at all. Let git walk up instead.
|
||||
$isRepo = $false
|
||||
if ($root -and (Test-Path -LiteralPath $root)) {
|
||||
$prev = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
git -C $root rev-parse --is-inside-work-tree 2>$null | Out-Null
|
||||
$isRepo = ($LASTEXITCODE -eq 0)
|
||||
$ErrorActionPreference = $prev
|
||||
}
|
||||
if (-not $isRepo) {
|
||||
$row.State = 'no-repo'; $row.Detail = 'no git checkout'
|
||||
$rows += [pscustomobject]$row; continue
|
||||
}
|
||||
|
||||
Push-Location -LiteralPath $root
|
||||
try {
|
||||
$prev = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
git fetch origin --prune --quiet
|
||||
$default = (git symbolic-ref --short refs/remotes/origin/HEAD 2>$null) -replace '^origin/', ''
|
||||
if (-not $default) { $default = 'main' }
|
||||
$tip = (git rev-parse "origin/$default" 2>$null)
|
||||
$tipUtc = (git show -s --format=%cI "origin/$default" 2>$null)
|
||||
# Same exclusions as the deploy's own guard, or the scan would
|
||||
# report a blocker zdeploy would happily run through: untracked
|
||||
# files ship anyway, and build-version.json / CHANGELOG.md are
|
||||
# written BY a deploy.
|
||||
$dirty = git status --porcelain --untracked-files=no | Where-Object {
|
||||
$name = ($_ -replace '^..\s+', '') -replace '^.*/', ''
|
||||
@('build-version.json', 'CHANGELOG.md') -notcontains $name
|
||||
}
|
||||
$ErrorActionPreference = $prev
|
||||
|
||||
if (-not $tip) { $row.State = 'no-repo'; $row.Detail = "no origin/$default"; $rows += [pscustomobject]$row; continue }
|
||||
|
||||
$r = $remote[$k]
|
||||
if (-not $r) {
|
||||
$row.State = 'unknown'; $row.Detail = 'box unreachable'
|
||||
}
|
||||
elseif ($r.Sha) {
|
||||
if ($r.Sha -eq $tip) { $row.State = 'current'; $row.Detail = $tip.Substring(0, 7) }
|
||||
else {
|
||||
$row.State = 'PENDING'
|
||||
$n = (git rev-list --count "$($r.Sha)..origin/$default" 2>$null)
|
||||
if (-not $n -or $LASTEXITCODE -ne 0) { $n = '?' } # deployed SHA not in this repo's history
|
||||
$row.Ahead = $n
|
||||
$row.Detail = "$n commit(s) since $($r.Sha.Substring(0, [Math]::Min(7, $r.Sha.Length)))"
|
||||
}
|
||||
}
|
||||
elseif (-not $r.Utc) {
|
||||
# NOT pending. No stamp means this project has never been
|
||||
# deployed by a zdeploy that wrote one - which says nothing
|
||||
# about whether it is behind. Calling it pending would have
|
||||
# swept edge, the mail server and monitoring into an unattended
|
||||
# run on no evidence at all, and edge in particular does not
|
||||
# take a speculative deploy well. Deploy it once by name to set
|
||||
# the baseline; every scan after that is exact.
|
||||
$row.State = 'no-stamp'; $row.Detail = 'no deploy stamp - deploy once by name to baseline it'
|
||||
}
|
||||
else {
|
||||
# Timestamp fallback - approximate, flagged with ~.
|
||||
$deployedAt = [datetime]::MinValue
|
||||
$ok = [datetime]::TryParse(($r.Utc -replace ' UTC$', ''), [ref]$deployedAt)
|
||||
$tipAt = [datetime]::MinValue
|
||||
$ok2 = [datetime]::TryParse($tipUtc, [ref]$tipAt)
|
||||
if ($ok -and $ok2 -and $tipAt.ToUniversalTime() -gt $deployedAt) {
|
||||
$row.State = 'PENDING'
|
||||
$row.Ahead = '~'
|
||||
$row.Detail = "~ tip $(($tipAt.ToUniversalTime()).ToString('MM-dd HH:mm')) > deploy $($r.Utc -replace ' UTC$','')"
|
||||
}
|
||||
else {
|
||||
$row.State = 'current'; $row.Detail = "~ deployed $($r.Utc -replace ' UTC$','')"
|
||||
}
|
||||
}
|
||||
|
||||
if ($dirty -and $row.State -eq 'PENDING') {
|
||||
$row.State = 'BLOCKED'
|
||||
$row.Detail = "$(@($dirty).Count) uncommitted file(s) - deploy would refuse"
|
||||
}
|
||||
}
|
||||
finally { Pop-Location }
|
||||
|
||||
$rows += [pscustomobject]$row
|
||||
}
|
||||
return $rows
|
||||
}
|
||||
|
||||
# A bare `zdeploy -s` means "look at everything", so it must not fall into the
|
||||
# usage block below.
|
||||
if ($Scan -and $Projects.Count -eq 0) { $Projects = @(Get-ZProjectKeys) }
|
||||
|
||||
if ($Projects.Count -eq 0) {
|
||||
$keys = (Get-ZProjectKeys) -join ', '
|
||||
Write-Host ""
|
||||
Write-Host "Usage: zdeploy <project> [<project> ...] | all | ztokens [-Note `"message`"]" -ForegroundColor Yellow
|
||||
Write-Host " Projects in zconfig.json: $keys" -ForegroundColor Gray
|
||||
Write-Host " 'all' deploys everything (edge kinds first) and stops at the first failure." -ForegroundColor Gray
|
||||
Write-Host " -Scan / -s reports which projects have merged work that is not live, then offers to deploy just those." -ForegroundColor Gray
|
||||
Write-Host " Add -Yes to skip the confirmation prompt. Edge still ships first." -ForegroundColor Gray
|
||||
Write-Host " 'ztokens' refreshes the live-usage stats published to the zscripts page." -ForegroundColor Gray
|
||||
Write-Host " 'ztokens' refreshes live-usage stats, if a sibling ztokens checkout exists." -ForegroundColor Gray
|
||||
Stop-ZTracking; exit 1
|
||||
}
|
||||
|
||||
@ -256,80 +91,6 @@ if ($Projects -contains 'all') {
|
||||
# do the risky order, because this sort only ran for 'all'.
|
||||
# Order within each group is preserved, so an intentional sequence still holds
|
||||
# — notably `zdeploy ztokens evo`, where ztokens must still precede evo.
|
||||
# Scan runs BEFORE the edge-first sort below, so whatever it selects still gets
|
||||
# ordered by that rule - the proxy ships before the apps behind it, exactly as
|
||||
# a hand-typed list would.
|
||||
if ($Scan) {
|
||||
Write-Host "`n=== zdeploy -Scan: what is merged but not shipped ===" -ForegroundColor Cyan
|
||||
$status = Get-DeployStatus -Keys @($Projects | Where-Object { $_ -ne 'ztokens' })
|
||||
|
||||
Write-Host ""
|
||||
foreach ($r in $status) {
|
||||
$colour = switch ($r.State) {
|
||||
'PENDING' { 'Yellow' }
|
||||
'BLOCKED' { 'Red' }
|
||||
'no-stamp' { 'DarkYellow' }
|
||||
'current' { 'DarkGray' }
|
||||
default { 'DarkYellow' }
|
||||
}
|
||||
Write-Host (" {0,-14} {1,-8} {2,-9} {3}" -f $r.Key, $r.Kind, $r.State, $r.Detail) -ForegroundColor $colour
|
||||
}
|
||||
|
||||
$pending = @($status | Where-Object { $_.State -eq 'PENDING' })
|
||||
$blocked = @($status | Where-Object { $_.State -eq 'BLOCKED' })
|
||||
$unknown = @($status | Where-Object { $_.State -eq 'unknown' })
|
||||
$nostamp = @($status | Where-Object { $_.State -eq 'no-stamp' })
|
||||
Write-Host ""
|
||||
Write-Host (" {0} pending, {1} blocked, {2} no-stamp, {3} unknown, {4} current" -f `
|
||||
$pending.Count, $blocked.Count, $nostamp.Count, $unknown.Count,
|
||||
@($status | Where-Object { $_.State -eq 'current' }).Count) -ForegroundColor Gray
|
||||
|
||||
if ($blocked.Count -gt 0) {
|
||||
Write-Host " Blocked projects are NOT deployed - commit or stash them, then re-run." -ForegroundColor Red
|
||||
}
|
||||
if ($nostamp.Count -gt 0) {
|
||||
Write-Host " No-stamp projects are NOT selected - deploy each once by name to establish a baseline." -ForegroundColor DarkYellow
|
||||
}
|
||||
if ($unknown.Count -gt 0) {
|
||||
# Silence here would read as "nothing to do", which is the one thing an
|
||||
# unreachable box does not mean.
|
||||
Write-Host " Unknown = the box did not answer for that project; its state is NOT 'current'." -ForegroundColor DarkYellow
|
||||
}
|
||||
|
||||
if ($pending.Count -eq 0) {
|
||||
Write-Host "`n Nothing to deploy.`n" -ForegroundColor Green
|
||||
Stop-ZTracking; exit 0
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
if (-not $Yes) {
|
||||
# Two different ways a prompt can have nobody to answer it, and they
|
||||
# fail differently:
|
||||
# - a -NonInteractive host: Read-Host THROWS. Caught below.
|
||||
# - redirected stdin (a pipe, a scheduled task, powershell.exe launched
|
||||
# from another shell): Read-Host does NOT throw - it BLOCKS, waiting
|
||||
# on a pipe that never answers. The first scan run this way sat for
|
||||
# ten minutes with its table already printed but withheld behind the
|
||||
# blocked pipeline. [Environment]::UserInteractive is $true in both
|
||||
# cases, so it cannot be the test; IsInputRedirected can.
|
||||
if ([Console]::IsInputRedirected) {
|
||||
Write-Host " stdin is not a terminal - cannot prompt. Re-run with -Yes to deploy these $($pending.Count).`n" -ForegroundColor Yellow
|
||||
Stop-ZTracking; exit 0
|
||||
}
|
||||
$answer = $null
|
||||
try { $answer = Read-Host " Deploy these $($pending.Count)? [y/N]" }
|
||||
catch {
|
||||
Write-Host " Non-interactive shell - cannot prompt. Re-run with -Yes to deploy these $($pending.Count).`n" -ForegroundColor Yellow
|
||||
Stop-ZTracking; exit 0
|
||||
}
|
||||
if ($answer -notmatch '^(y|yes)$') {
|
||||
Write-Host " Aborted. Nothing deployed.`n" -ForegroundColor Yellow
|
||||
Stop-ZTracking; exit 0
|
||||
}
|
||||
}
|
||||
$Projects = @($pending | ForEach-Object { $_.Key })
|
||||
}
|
||||
|
||||
$requested = @($Projects)
|
||||
$edgeKeys = @($Projects | Where-Object { $cfg.projects.$_.kind -eq 'edge' })
|
||||
$restKeys = @($Projects | Where-Object { $cfg.projects.$_.kind -ne 'edge' })
|
||||
@ -341,34 +102,6 @@ if ($Projects.Count -gt 1) {
|
||||
Write-Host "Deploying: $($Projects -join ', ')$note" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
# The bash that stamps what just shipped. The timestamp has always been
|
||||
# written; the SHA is what lets -Scan answer exactly rather than by clock
|
||||
# comparison. Omitted rather than faked when the checkout is not a git repo -
|
||||
# scan falls back to the timestamp, and a wrong SHA would be worse than none.
|
||||
function Get-RecordDeployBash {
|
||||
param(
|
||||
[Parameter(Mandatory)]$Proj,
|
||||
[Parameter(Mandatory)][string]$RemotePath,
|
||||
# Optional: the edge, static and docker paths upload no zip, so there
|
||||
# is nothing to remove - but they still ship, so they still stamp.
|
||||
[string]$ZipName = ''
|
||||
)
|
||||
$sha = ''
|
||||
$root = $Proj.localRoot
|
||||
if ($root -and (Test-Path -LiteralPath (Join-Path $root '.git'))) {
|
||||
$prev = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$sha = (git -C $root rev-parse HEAD 2>$null)
|
||||
if ($LASTEXITCODE -ne 0) { $sha = '' }
|
||||
$ErrorActionPreference = $prev
|
||||
}
|
||||
$cmd = "date -u +'%Y-%m-%d %H:%M:%S UTC' | sudo tee $RemotePath/.last_deploy_utc > /dev/null"
|
||||
# 40 hex characters, so it needs no quoting in the remote command.
|
||||
if ($sha) { $cmd += " && printf '%s' $sha | sudo tee $RemotePath/.last_deploy_sha > /dev/null" }
|
||||
if ($ZipName) { $cmd += " && rm -f $RemoteHome/$ZipName" }
|
||||
return $cmd
|
||||
}
|
||||
|
||||
function Get-DeployZipName {
|
||||
param([string]$Key, $Proj)
|
||||
if ($Proj.deploy -and $Proj.deploy.zipName) { return $Proj.deploy.zipName }
|
||||
@ -399,12 +132,10 @@ function Invoke-Ec2PreflightCleanup {
|
||||
"echo available_mb=`$avail_mb",
|
||||
"if [ `"`$avail_mb`" -lt 1500 ]; then echo 'ERROR: less than 1.5 GB free on /. Grow the root volume or run: sudo docker system prune -af' >&2; exit 11; fi"
|
||||
) -join '; '
|
||||
# Also through the wrapper (#119): the out-of-space branch above writes its
|
||||
# ERROR to stderr and exits 11, so a bare ssh would surface a
|
||||
# NativeCommandError instead of the actionable message below - exactly when
|
||||
# the operator most needs to be told what to do. -FailHint keeps it.
|
||||
Invoke-Ec2Step "server pre-flight cleanup" $preflightCmd `
|
||||
-FailHint "Root volume too full (need ~1.5 GB free, ideally 3+). Grow it or run: sudo docker system prune -af"
|
||||
ssh @SSH_OPTS -i $PEM_KEY $SSH_TARGET $preflightCmd
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Server pre-flight cleanup failed (exit $LASTEXITCODE). Root volume too full (need ~1.5 GB free, ideally 3+)."
|
||||
}
|
||||
}
|
||||
|
||||
# Post-deploy cleanup: prune build cache and dangling images created during this deploy.
|
||||
@ -439,96 +170,30 @@ function Invoke-RemoteUnzip {
|
||||
Invoke-Ec2Step "unzip $ZipName" $bash
|
||||
}
|
||||
|
||||
# ── Operator-file preservation (issue #2, hardened in #107) ──────────────────
|
||||
# ── Operator-file preservation (issue #2) ────────────────────────────────────
|
||||
# Deploys replace the project directory wholesale, which used to destroy every
|
||||
# operator-managed file except ./.env. These helpers preserve all .env* files
|
||||
# at the project root PLUS any paths listed in deploy.preserve (files or
|
||||
# directories), by tarring them to the home dir before the wipe and extracting
|
||||
# them back after the unzip. Server-side copies win over anything shipped in
|
||||
# the zip — the same semantics ./.env always had.
|
||||
#
|
||||
# WHY THE ARCHIVE IS TIMESTAMPED AND NEVER DELETED (#107)
|
||||
# -------------------------------------------------------
|
||||
# This used to write one fixed preserve_<key>.tgz, and preserve's FIRST action
|
||||
# was `rm -f` on it. That is the opposite of safe. The window between
|
||||
# `sudo rm -rf` on the project directory and the restore step is the only time
|
||||
# the tarball is the sole copy of the production secrets - and an interrupted
|
||||
# run (dropped ssh, exit 255) stops exactly there, leaving a perfect backup
|
||||
# behind. The next run then deleted that backup before doing anything else,
|
||||
# tarred a directory that no longer had the files, and reported success.
|
||||
# `2>/dev/null; true` on the tar is what made it silent.
|
||||
#
|
||||
# That destroyed civilcode's production deploy/.env on 2026-08-30. The site
|
||||
# survived only because the running container still held its environment; a
|
||||
# restart would have made the loss permanent.
|
||||
#
|
||||
# So, three independent changes, any one of which would have prevented it:
|
||||
#
|
||||
# 1. Each run writes its own preserve_<key>_<stamp>.tgz and restore no
|
||||
# longer deletes it. Nothing removes an archive that has not been
|
||||
# superseded - retention below prunes old ones instead.
|
||||
# 2. Preserve first extracts any earlier archives with `tar -k`, which fills
|
||||
# in files a previous interrupted run lost WITHOUT overwriting anything
|
||||
# currently on disk. A hand-repaired .env therefore wins over the stale
|
||||
# copy in the archive.
|
||||
# 3. Preserve refuses to continue if it captured nothing while an earlier
|
||||
# archive for the same key did have contents. Capturing zero files is
|
||||
# normal for a project with no operator files (edge, gitea, landing) and
|
||||
# catastrophic for one that has them; the prior archive is what tells the
|
||||
# difference.
|
||||
#
|
||||
# One stamp per zdeploy process, so preserve and restore agree on the filename
|
||||
# without threading it through every call site.
|
||||
$script:PreserveStamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||||
$script:PreserveKeep = 5
|
||||
|
||||
function Get-PreserveTarball {
|
||||
param([string]$Key)
|
||||
"$RemoteHome/preserve_${Key}_$($script:PreserveStamp).tgz"
|
||||
}
|
||||
|
||||
function Save-OperatorFiles {
|
||||
param([string]$Key, $Proj, [string]$RemotePath)
|
||||
$paths = @('.env*')
|
||||
if ($Proj.deploy -and $Proj.deploy.preserve) { $paths += @($Proj.deploy.preserve) }
|
||||
$spec = $paths -join ' '
|
||||
$tarball = Get-PreserveTarball -Key $Key
|
||||
$list = $tarball -replace '\.tgz$', '.list'
|
||||
$glob = "$RemoteHome/preserve_${Key}_*"
|
||||
$drop = $script:PreserveKeep + 1
|
||||
|
||||
# NOTE: no embedded double quotes or $( ) here - PowerShell 5.1 strips
|
||||
# embedded double quotes when passing args to ssh.exe, silently corrupting
|
||||
# the remote command, and $( ) would be evaluated locally. Remote shell
|
||||
# variables are backtick-escaped so PowerShell leaves them alone. Globs
|
||||
# expand remotely; tar's nonzero exit for missing paths is swallowed, but
|
||||
# an empty capture is NOT (see the guard below).
|
||||
$bash =
|
||||
"cd $RemotePath || exit 9; " +
|
||||
"for t in ${glob}.tgz; do [ -e `$t ] && tar -xzkf `$t -C $RemotePath 2>/dev/null; done; true; " +
|
||||
"tar -czf $tarball $spec 2>/dev/null; " +
|
||||
"tar -tzf $tarball > $list 2>/dev/null; " +
|
||||
"if [ ! -s $list ]; then " +
|
||||
"for p in ${glob}.list; do " +
|
||||
"if [ -s `$p ] && [ `$p != $list ]; then " +
|
||||
"echo PRESERVE CAPTURED NOTHING BUT AN EARLIER ARCHIVE HAS FILES; exit 8; " +
|
||||
"fi; " +
|
||||
"done; " +
|
||||
"fi; " +
|
||||
"ls -1t ${glob}.tgz 2>/dev/null | tail -n +$drop | xargs -r rm -f; " +
|
||||
"ls -1t ${glob}.list 2>/dev/null | tail -n +$drop | xargs -r rm -f; " +
|
||||
"exit 0"
|
||||
|
||||
Invoke-Ec2Step "preserve operator files ($spec)" $bash `
|
||||
-FailHint "Refusing to wipe $RemotePath - see $glob.tgz on the server."
|
||||
$tarball = "$RemoteHome/preserve_${Key}.tgz"
|
||||
# NOTE: no embedded quotes or $( ) here - PowerShell 5.1 strips embedded
|
||||
# double quotes when passing args to ssh.exe, silently corrupting the
|
||||
# remote command. Globs expand remotely; tar archives whatever exists
|
||||
# and its nonzero exit for missing paths is deliberately swallowed.
|
||||
Invoke-Ec2Step "preserve operator files ($spec)" "rm -f $tarball; cd $RemotePath && tar -czf $tarball $spec 2>/dev/null; true"
|
||||
}
|
||||
|
||||
function Restore-OperatorFiles {
|
||||
param([string]$Key, [string]$RemotePath)
|
||||
$tarball = Get-PreserveTarball -Key $Key
|
||||
# Overwrites, deliberately: server-side operator files beat whatever the
|
||||
# zip shipped. The archive is left in place - see the header.
|
||||
Invoke-Ec2Step "restore operator files" "test -f $tarball && tar -xzf $tarball -C $RemotePath; true"
|
||||
$tarball = "$RemoteHome/preserve_${Key}.tgz"
|
||||
Invoke-Ec2Step "restore operator files" "test -f $tarball && tar -xzf $tarball -C $RemotePath; rm -f $tarball; true"
|
||||
}
|
||||
|
||||
# ── Deploy verification (build-version match, not just HTTP 200 — a 200 can be
|
||||
@ -543,19 +208,9 @@ function Wait-VerifyStaticBuild {
|
||||
# Expect the COMMITTED stamp, not +1: builds no longer self-bump (a
|
||||
# prebuild hook incremented inside the image, so the served version
|
||||
# matched no commit and every build dirtied the tree). The counter now
|
||||
# advances deliberately — one version bump per release — so "is the
|
||||
# advances deliberately — one version bump per merged PR — so "is the
|
||||
# build I just packed live?" means an exact match.
|
||||
$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
|
||||
$containerName = $Proj.remote.containerName
|
||||
$deadline = (Get-Date).AddSeconds(45)
|
||||
@ -575,7 +230,7 @@ function Wait-VerifyStaticBuild {
|
||||
}
|
||||
if ($r) {
|
||||
$remoteLabel = Get-LabelFromBuildJsonObj $r
|
||||
if ($remoteLabel -and $remoteLabel -eq $expectedLabel) {
|
||||
if ($remoteLabel -eq $expectedLabel) {
|
||||
Write-Host " PASS - live build $remoteLabel matches expected." -ForegroundColor Green
|
||||
return
|
||||
}
|
||||
@ -592,87 +247,54 @@ function Wait-VerifyStaticBuild {
|
||||
function Wait-VerifyApiBuild {
|
||||
param([string]$Key, $Proj, [string]$ExpectedLabel, [int]$TimeoutSec = 60)
|
||||
Write-Host "`n--- [$Key] Live build verification (expect $ExpectedLabel) ---" -ForegroundColor Cyan
|
||||
# deploy.verifyHost (or domain) decides which edge vhost may be asked;
|
||||
# both are consumed inside Get-VerifyAttempts now, where "no host at all"
|
||||
# excludes the edge channel entirely rather than defaulting to whatever
|
||||
# vhost the proxy serves (#101). verifyHost exists for a domain retired
|
||||
# ahead of its replacement - a takedown once had a project's public host
|
||||
# answering 410 while the app was healthy; no project sets it today.
|
||||
# Every retry walks the channels in trust order - docker-network exec,
|
||||
# then localhost port, then (only with a Host to route by) the edge.
|
||||
# The choice used to be made ONCE, before the loop, by probing each
|
||||
# channel - but the probes ran at the exact moment step [5] had
|
||||
# restarted the app, so both good channels were briefly down and the
|
||||
# whole window was spent on the edge. For a project with no domain that
|
||||
# meant the default vhost: one project's check read a DIFFERENT
|
||||
# project's build number, twice in a single day (#101). Re-resolving per
|
||||
# retry means the right channel is used the moment the app is back.
|
||||
#
|
||||
# The port channel still counts only when the body carries a version:
|
||||
# one project's verify path is a JWKS endpoint - real, healthy, and no
|
||||
# version in it - so it falls through to the edge (it has a domain),
|
||||
# same as it always did.
|
||||
$execCmd = Get-ServerSideVersionCommand -Proj $Proj
|
||||
$attempts = Get-VerifyAttempts -Proj $Proj -ExecCmd $execCmd
|
||||
$TimeoutSec = Get-VerifyTimeout -Proj $Proj -DefaultSec $TimeoutSec
|
||||
if ($attempts.Count -eq 0) {
|
||||
# No trustworthy channel exists: no viaProxy, no port, no host to
|
||||
# route an edge request by. Asking the edge anyway can only reach
|
||||
# the DEFAULT vhost - a different product - and a check that can
|
||||
# only ever read someone else's number is worse than no check.
|
||||
Write-Host " SKIPPED: no way to verify this project without reading the wrong vhost - configure verify.viaProxy/port, or a domain (#101)." -ForegroundColor Yellow
|
||||
return $false
|
||||
$headers = @{}
|
||||
# deploy.verifyHost overrides domain for verification only. The Host header
|
||||
# decides which edge vhost answers, and a project's public host can be
|
||||
# deliberately unroutable while the app is perfectly healthy - that is why
|
||||
# the override exists. Reach for it when a domain is being retired ahead of
|
||||
# its replacement: the old host may be returning 410 while the new one has
|
||||
# no DNS yet, so neither answers even though the app is fine.
|
||||
$verifyHost = if ($Proj.deploy -and $Proj.deploy.verifyHost) { $Proj.deploy.verifyHost } else { $Proj.domain }
|
||||
if ($verifyHost) { $headers['Host'] = $verifyHost }
|
||||
# Preferred when the project configures it: read the version from a
|
||||
# container ON the shared docker network rather than through the public
|
||||
# proxy. A service that publishes no port cannot be curled from the host
|
||||
# at all, and the proxy answers from whichever vhost matches the Host
|
||||
# header - so a container with no public route gets another site's
|
||||
# version back. See Get-ServerSideVersionCommand.
|
||||
$execCmd = Get-ServerSideVersionCommand -Proj $Proj
|
||||
$useExec = $false
|
||||
if ($execCmd) {
|
||||
$probe = (ssh @SSH_OPTS -i $PEM_KEY $SSH_TARGET $execCmd | Out-String).Trim()
|
||||
if (Get-LabelFromVersionJson $probe) { $useExec = $true }
|
||||
}
|
||||
if ($useExec) {
|
||||
Write-Host " Asking on server: $($Proj.verify.upstream) (via $($Proj.verify.viaProxy))" -ForegroundColor DarkGray
|
||||
}
|
||||
Write-Host " Channels, in order: $(($attempts | ForEach-Object { $_.Label }) -join '; ')" -ForegroundColor DarkGray
|
||||
|
||||
$sawVersion = $false
|
||||
$deadline = (Get-Date).AddSeconds($TimeoutSec)
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
foreach ($attempt in $attempts) {
|
||||
$r = $null
|
||||
try {
|
||||
switch ($attempt.Kind) {
|
||||
'exec' {
|
||||
$raw = ssh @SSH_OPTS -i $PEM_KEY $SSH_TARGET $execCmd
|
||||
$r = ($raw | Out-String).Trim() | ConvertFrom-Json -ErrorAction Stop
|
||||
}
|
||||
'port' {
|
||||
$raw = ssh @SSH_OPTS -i $PEM_KEY $SSH_TARGET "curl -s -m 8 http://localhost:$($attempt.Port)$($attempt.Path)"
|
||||
$r = ($raw | Out-String).Trim() | ConvertFrom-Json -ErrorAction Stop
|
||||
}
|
||||
'edge' {
|
||||
$edgeHeaders = @{ 'Host' = $attempt.HostHeader }
|
||||
$r = Invoke-RestMethod -Uri "http://$EC2_IP/api/build-version" -Headers $edgeHeaders -TimeoutSec 10 -ErrorAction Stop
|
||||
}
|
||||
try {
|
||||
if ($useExec) {
|
||||
$raw = ssh @SSH_OPTS -i $PEM_KEY $SSH_TARGET $execCmd
|
||||
$live = Get-LabelFromVersionJson ($raw | Out-String)
|
||||
} else {
|
||||
$r = Invoke-RestMethod -Uri "http://$EC2_IP/api/build-version" -Headers $headers -TimeoutSec 10 -ErrorAction Stop
|
||||
$live = if ($r -and $r.build_version) { [string]$r.build_version } else { $null }
|
||||
}
|
||||
if ($live) {
|
||||
if ($live -eq $ExpectedLabel) {
|
||||
Write-Host " PASS - live build $live matches expected." -ForegroundColor Green
|
||||
return $true
|
||||
}
|
||||
} catch {
|
||||
continue # channel not ready; the next one gets its turn
|
||||
Write-Host " Live build is $live, expected $ExpectedLabel - waiting..." -ForegroundColor DarkYellow
|
||||
}
|
||||
if ($null -eq $r) { continue }
|
||||
# Two field names in the fleet: some apps answer build_version
|
||||
# on /api/build-version, others answer version on /health. Both
|
||||
# are "the build that is live", so accept either rather than
|
||||
# making every app rename its own field.
|
||||
$live = if ($r.build_version) { [string]$r.build_version } elseif ($r.version) { [string]$r.version } else { $null }
|
||||
if (-not $live) { continue } # answered, but not about versions (JWKS etc.)
|
||||
$sawVersion = $true
|
||||
if ($live -eq $ExpectedLabel) {
|
||||
Write-Host " PASS - live build $live matches expected ($($attempt.Label))." -ForegroundColor Green
|
||||
return $true
|
||||
}
|
||||
Write-Host " Live build is $live via $($attempt.Label), expected $ExpectedLabel - waiting..." -ForegroundColor DarkYellow
|
||||
break # one wrong-version read this pass is enough; retry after the sleep
|
||||
} catch {
|
||||
Write-Host " version endpoint not ready yet - waiting..." -ForegroundColor DarkGray
|
||||
}
|
||||
Start-Sleep -Seconds 3
|
||||
}
|
||||
# Say which failure this actually was: a version that never matched is a
|
||||
# stale/failed build; channels that never answered is "could not verify",
|
||||
# and pretending otherwise is how a warning gets ignored.
|
||||
if ($sawVersion) {
|
||||
Write-Host " WARNING: live build did not match $ExpectedLabel within ${TimeoutSec}s (upload or Docker build may have failed, or a stale build is cached)." -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host " WARNING: could not verify within ${TimeoutSec}s - no channel answered with a version (app may still be starting; raise verify.timeoutSeconds if this project boots slowly)." -ForegroundColor Yellow
|
||||
}
|
||||
Write-Host " WARNING: live build did not match $ExpectedLabel within ${TimeoutSec}s (a stale build may be cached)." -ForegroundColor Yellow
|
||||
return $false
|
||||
}
|
||||
|
||||
@ -785,7 +407,7 @@ function Invoke-PythonDeploy {
|
||||
Invoke-Ec2Step "require compose directory" "test -d $composeDir"
|
||||
Invoke-Ec2Step "docker compose build $appSvc" "cd $composeDir && sudo COMPOSE_BAKE=false docker compose build $appSvc"
|
||||
Invoke-Ec2Step "docker compose up -d" "cd $composeDir && sudo COMPOSE_BAKE=false docker compose up -d"
|
||||
Invoke-Ec2Step "record deploy time; remove remote zip" (Get-RecordDeployBash -Proj $Proj -RemotePath $remotePath -ZipName $zipName)
|
||||
Invoke-Ec2Step "record deploy time; remove remote zip" "date -u +'%Y-%m-%d %H:%M:%S UTC' | sudo tee $remotePath/.last_deploy_utc > /dev/null && rm -f $RemoteHome/$zipName"
|
||||
|
||||
if ($hasVersionTool) {
|
||||
Write-Host "`n--- [4] Incrementing build version ---" -ForegroundColor Cyan
|
||||
@ -813,7 +435,7 @@ function Invoke-PythonDeploy {
|
||||
# in the container, is written to .build_version below, and is
|
||||
# proven by the /api/build-version check — which is the thing that
|
||||
# actually establishes what is deployed.
|
||||
Invoke-Ec2Step "record the live build number" "echo '$BuildVersion' | sudo tee $remotePath/.build_version > /dev/null"
|
||||
ssh @SSH_OPTS -i $PEM_KEY $SSH_TARGET "echo '$BuildVersion' | sudo tee $remotePath/.build_version > /dev/null"
|
||||
$changelogTool = Join-Path $root "scripts\build_changelog_tool.py"
|
||||
if (Test-Path -LiteralPath $changelogTool) {
|
||||
if ([string]::IsNullOrWhiteSpace($ChangeNote)) { $ChangeNote = "Build deployed" }
|
||||
@ -821,24 +443,10 @@ function Invoke-PythonDeploy {
|
||||
}
|
||||
|
||||
Write-Host "`n--- [5] Restarting app to pick up new version ---" -ForegroundColor Cyan
|
||||
# Through Invoke-Ec2Step, not a bare ssh: `docker compose restart`
|
||||
# writes " Container <name> Restarting" to STDERR as ordinary
|
||||
# progress, and under ErrorActionPreference='Stop' PS 5.1 turns any
|
||||
# native stderr line into a terminating NativeCommandError whatever
|
||||
# the exit code. That threw here on a deploy that had fully
|
||||
# succeeded - and it threw BEFORE Wait-VerifyApiBuild, so the step
|
||||
# that proves what is actually deployed never ran (#119). The
|
||||
# wrapper flattens stderr and judges by exit code alone, and throws
|
||||
# on non-zero itself, so the hand-written check is gone with it.
|
||||
Invoke-Ec2Step "restart $appSvc to pick up the new build" `
|
||||
"cd $composeDir && sudo COMPOSE_BAKE=false docker compose restart $appSvc" `
|
||||
-FailHint "App restart after the build bump failed."
|
||||
ssh @SSH_OPTS -i $PEM_KEY $SSH_TARGET "cd $composeDir && sudo COMPOSE_BAKE=false docker compose restart $appSvc"
|
||||
if ($LASTEXITCODE -ne 0) { throw "App restart after build bump failed (exit $LASTEXITCODE)" }
|
||||
|
||||
Wait-VerifyApiBuild -Key $Key -Proj $Proj -ExpectedLabel $BuildVersion -TimeoutSec 30 | Out-Null
|
||||
|
||||
# Only now: the tag is a claim about what is RUNNING, so it
|
||||
# is written after the live build has been proven, never before.
|
||||
New-DeployTag -Proj $Proj -Version $BuildVersion -Note $ChangeNote
|
||||
} elseif ($Proj.verify -and $Proj.verify.port) {
|
||||
Test-DeployHealth -Key $Key -Proj $Proj -TimeoutSec 60 | Out-Null
|
||||
} elseif ($Proj.domain) {
|
||||
@ -929,7 +537,7 @@ function Invoke-ViteDeploy {
|
||||
if ($edgeProj -and $edgeProj.Config.proxyContainer) {
|
||||
Invoke-Ec2Step "reload edge nginx (flush DNS cache for new container IP)" "sudo docker exec $($edgeProj.Config.proxyContainer) nginx -s reload"
|
||||
}
|
||||
Invoke-Ec2Step "record deploy time; remove remote zip" (Get-RecordDeployBash -Proj $Proj -RemotePath $remotePath -ZipName $zipName)
|
||||
Invoke-Ec2Step "record deploy time; remove remote zip" "date -u +'%Y-%m-%d %H:%M:%S UTC' | sudo tee $remotePath/.last_deploy_utc > /dev/null && rm -f $RemoteHome/$zipName"
|
||||
|
||||
Wait-VerifyStaticBuild -Key $Key -Proj $Proj -PreZipBuildState $preZipBuild
|
||||
Invoke-Ec2PostDeployCleanup -Label $Key
|
||||
@ -1038,7 +646,7 @@ function Invoke-NextDeploy {
|
||||
# offline for the whole build - minutes for a Next.js app - and left it
|
||||
# offline if the build failed. That is not hypothetical: one deploy
|
||||
# stopped the stack, the build did not finish, and the site served 502
|
||||
# for 19 hours with no container at all. The
|
||||
# for 19 hours with no container running at all. The
|
||||
# old image keeps serving while the new one builds, so a failed build is
|
||||
# now harmless and the outage is the seconds between down and up.
|
||||
#
|
||||
@ -1055,14 +663,14 @@ function Invoke-NextDeploy {
|
||||
if ($Proj.migrations -eq "prisma") {
|
||||
Invoke-Ec2Step "apply prisma migrations" "cd $composeDir && sudo docker compose exec -T $appSvc npx prisma migrate deploy"
|
||||
}
|
||||
Invoke-Ec2Step "record deploy timestamp; remove remote zip" (Get-RecordDeployBash -Proj $Proj -RemotePath $remotePath -ZipName $zipName)
|
||||
Invoke-Ec2Step "record deploy timestamp; remove remote zip" "date -u +'%Y-%m-%d %H:%M:%S UTC' | sudo tee $remotePath/.last_deploy_utc > /dev/null && rm -f $RemoteHome/$zipName"
|
||||
|
||||
Write-Host "`n--- [5] Verifying deployment ---" -ForegroundColor Cyan
|
||||
# Same precedence as the python handler. Do NOT probe http://<ec2-ip>:<prod-port>/
|
||||
# here: a compose stack behind the edge proxy usually publishes to
|
||||
# 127.0.0.1 only, so that probe can never answer and the old "is the port
|
||||
# open in the security group?" warning sent you chasing a firewall rule
|
||||
# for an app that was already up. See issue #28.
|
||||
# for an app that was already up.
|
||||
if ($Proj.verify -and $Proj.verify.port) {
|
||||
Test-DeployHealth -Key $Key -Proj $Proj -TimeoutSec 60 | Out-Null
|
||||
} elseif ($Proj.domain) {
|
||||
@ -1144,11 +752,10 @@ function Invoke-EdgeDeploy {
|
||||
}
|
||||
|
||||
# Content subdirectories the proxy serves (fonts/, vendor/, ...) ship too —
|
||||
# only server-side state stays put. Skipping them is how the self-hosted
|
||||
# Chart.js and fonts silently never reached prod (charts rendered blank).
|
||||
# .pytest_cache is a local test artifact, already gitignored; it has no
|
||||
# business on the proxy box and only adds noise to the upload log.
|
||||
$skipDirs = @('nginx-logs', '.git', '.pytest_cache')
|
||||
# only server-side state stays put. Skipping them is how self-hosted assets
|
||||
# silently never reach prod: docker creates empty mount-point dirs and nginx
|
||||
# serves 404s from them, so fonts fall back and vendored JS never loads.
|
||||
$skipDirs = @('nginx-logs', '.git')
|
||||
$dirs = @(Get-ChildItem -LiteralPath $root -Directory | Where-Object { $skipDirs -notcontains $_.Name })
|
||||
foreach ($d in $dirs) {
|
||||
Write-Host " >> uploading $($d.Name)/ (recursive)" -ForegroundColor DarkCyan
|
||||
@ -1168,16 +775,15 @@ function Invoke-EdgeDeploy {
|
||||
Invoke-Ec2Step "edge nginx reload" "sudo docker exec $pc nginx -s reload || true"
|
||||
}
|
||||
Invoke-Ec2Step "fix nginx-logs permissions (if present)" "if [ -d $remotePath/nginx-logs ]; then sudo chmod 777 $remotePath/nginx-logs; sudo chmod 666 $remotePath/nginx-logs/*.log 2>/dev/null || true; fi"
|
||||
Invoke-Ec2Step "record deploy stamp" (Get-RecordDeployBash -Proj $Proj -RemotePath $remotePath)
|
||||
Write-Host "--- [Done] Edge proxy deploy finished ---" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Invoke-StaticDeploy {
|
||||
param([string]$Key, $Proj)
|
||||
|
||||
# Plain static sites - no build, no container of their own. The landing
|
||||
# container serves them straight off disk, one directory per host, so
|
||||
# shipping the files IS the deploy: there is nothing to restart afterwards.
|
||||
# Plain static sites - no build, no container of their own. A shared web
|
||||
# container serves them straight off disk, so shipping the files IS the
|
||||
# deploy: there is nothing to restart afterwards.
|
||||
$root = Join-Path $Proj.localRoot $Proj.siteDir
|
||||
$remotePath = $Proj.remote.path
|
||||
|
||||
@ -1195,8 +801,11 @@ function Invoke-StaticDeploy {
|
||||
}
|
||||
|
||||
# A large media file uploaded in place is served half-written to anyone who
|
||||
# requests it mid-copy. Ship the directory to a sibling, then swap it in.
|
||||
$skipDirs = @('.git', '.pytest_cache', 'node_modules')
|
||||
# requests it mid-copy. Ship each directory to a sibling, then swap it in -
|
||||
# the swap is a rename, so the switch is atomic and visitors never see a
|
||||
# partial file.
|
||||
$skipDirs = @($script:JunkDirNames) + @('.github')
|
||||
if ($Proj.deploy -and $Proj.deploy.skipDirs) { $skipDirs += @($Proj.deploy.skipDirs) }
|
||||
$dirs = @(Get-ChildItem -LiteralPath $root -Directory | Where-Object { $skipDirs -notcontains $_.Name })
|
||||
foreach ($d in $dirs) {
|
||||
Write-Host " >> uploading $($d.Name)/ (recursive, staged)" -ForegroundColor DarkCyan
|
||||
@ -1206,7 +815,6 @@ function Invoke-StaticDeploy {
|
||||
Invoke-Ec2Step "swap in $($d.Name)" "rm -rf $remotePath/$($d.Name) && mv $remotePath/.staging-$($d.Name) $remotePath/$($d.Name)"
|
||||
}
|
||||
|
||||
Invoke-Ec2Step "record deploy stamp" (Get-RecordDeployBash -Proj $Proj -RemotePath $remotePath)
|
||||
Write-Host "--- [Done] Static site deploy finished ---" -ForegroundColor Green
|
||||
}
|
||||
|
||||
@ -1254,22 +862,19 @@ function Invoke-DockerDeploy {
|
||||
if ($LASTEXITCODE -ne 0) { throw "SCP failed for $($d.Name) (exit $LASTEXITCODE)" }
|
||||
}
|
||||
|
||||
# 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 pull" "cd $remotePath && sudo docker compose pull"
|
||||
Invoke-Ec2Step "docker compose up -d" "cd $remotePath && sudo docker compose up -d"
|
||||
|
||||
Invoke-Ec2PostDeployCleanup -Label $Key
|
||||
Invoke-Ec2Step "record deploy stamp" (Get-RecordDeployBash -Proj $Proj -RemotePath $remotePath)
|
||||
Write-Host "`n--- [Done] $($Proj.label) deploy finished ---" -ForegroundColor Green
|
||||
Write-DeployLocation -Proj $Proj
|
||||
}
|
||||
|
||||
# Pseudo-project "ztokens": not a zconfig entry, no compose stack. Runs
|
||||
# `ztokens -Publish` from the sibling ztokens repo so the public zscripts page
|
||||
# has current data. A failure here warns rather than aborting the rest of the
|
||||
# deploy list - it's a nice-to-have refresh, not a deploy step.
|
||||
# Pseudo-project "ztokens": not a zconfig entry, no compose stack, and entirely
|
||||
# optional. Runs `ztokens -Publish` from a sibling ztokens checkout so a site
|
||||
# that charts usage data has something current to ship. Missing checkout, or a
|
||||
# failure, warns rather than aborting the rest of the deploy list - it is a
|
||||
# nice-to-have refresh, not a deploy step.
|
||||
function Invoke-ZTokensPublish {
|
||||
Write-Host "`n=== ztokens: refreshing live-usage stats ===" -ForegroundColor Cyan
|
||||
$ztokensScript = Join-Path (Split-Path -Parent $PSScriptRoot) "ztokens\ztokens.ps1"
|
||||
@ -1304,35 +909,22 @@ function Invoke-ZTokensPublish {
|
||||
|
||||
# ── Dispatch ─────────────────────────────────────────────────────────────────
|
||||
|
||||
# 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
|
||||
# singular 'ztoken' still works so existing habits and any script that
|
||||
# already calls it keep running.
|
||||
if ($key -in @('ztokens', 'ztoken')) { Invoke-ZTokensPublish; continue }
|
||||
$proj = Get-ZProject -Key $key
|
||||
Invoke-DeployGitPull -Proj $proj # no-op unless deploy.gitPull is set
|
||||
switch ([string]$proj.kind) {
|
||||
"python" { Invoke-PythonDeploy -Key $key -Proj $proj -ChangeNote $Note }
|
||||
"vite" { Invoke-ViteDeploy -Key $key -Proj $proj -ChangeNote $Note }
|
||||
"nextjs" { Invoke-NextDeploy -Key $key -Proj $proj -ChangeNote $Note }
|
||||
"edge" { Invoke-EdgeDeploy -Key $key -Proj $proj }
|
||||
"docker" { Invoke-DockerDeploy -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." }
|
||||
}
|
||||
foreach ($key in $Projects) {
|
||||
# 'ztokens' matches the tool it runs (ztokens.cmd / ztokens.ps1). The old
|
||||
# singular 'ztoken' still works so existing habits and any script that
|
||||
# already calls it keep running.
|
||||
if ($key -in @('ztokens', 'ztoken')) { Invoke-ZTokensPublish; continue }
|
||||
$proj = Get-ZProject -Key $key
|
||||
Invoke-DeployGitPull -Proj $proj # no-op unless deploy.gitPull is set
|
||||
switch ([string]$proj.kind) {
|
||||
"python" { Invoke-PythonDeploy -Key $key -Proj $proj -ChangeNote $Note }
|
||||
"vite" { Invoke-ViteDeploy -Key $key -Proj $proj -ChangeNote $Note }
|
||||
"nextjs" { Invoke-NextDeploy -Key $key -Proj $proj -ChangeNote $Note }
|
||||
"edge" { Invoke-EdgeDeploy -Key $key -Proj $proj }
|
||||
"docker" { Invoke-DockerDeploy -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." }
|
||||
}
|
||||
} finally {
|
||||
[Console]::OutputEncoding = $prevConsoleEncoding
|
||||
}
|
||||
# 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
|
||||
|
||||
2
zec2.cmd
2
zec2.cmd
@ -1,7 +1,7 @@
|
||||
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
|
||||
REM Version: v1.0.0.0.19
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zec2.ps1" %*
|
||||
|
||||
18
zec2.ps1
18
zec2.ps1
@ -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
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.28
|
||||
# Version: v1.0.0.0.19
|
||||
|
||||
# zec2.ps1 — quick reachability check (TCP + HTTP + live build version) for deployed projects.
|
||||
#
|
||||
@ -22,11 +22,9 @@ Start-ZTracking
|
||||
|
||||
$cfg = Get-ZConfig
|
||||
if (-not $HostName) { $HostName = $cfg.ec2.ip }
|
||||
# Needed by the container-side version read below. zec2 had no ssh of its own
|
||||
# before that, so the read referenced three variables this script never
|
||||
# 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.
|
||||
# Needed by the container-side version read below; same names zec2online.ps1
|
||||
# uses. Without them that read throws inside its try/catch and falls through
|
||||
# silently, which looks identical to a service that cannot be reached.
|
||||
$PemKey = $cfg.ec2.pemKey
|
||||
$SshTarget = Get-Ec2Target
|
||||
|
||||
@ -105,9 +103,9 @@ function Show-Zec2LiveVersion {
|
||||
$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 }
|
||||
} else {
|
||||
# Container-side first where the project configures it: the
|
||||
# endpoint is not public on every project, and asking the edge
|
||||
# answers from whichever vhost matches the Host header.
|
||||
# Container-side first where the project configures it: a build
|
||||
# stamp is not public on every site, and asking the proxy answers
|
||||
# from whichever vhost matches the Host header.
|
||||
$execCmd = Get-ServerSideVersionCommand -Proj $Proj
|
||||
$label = $null
|
||||
if ($execCmd -and (Test-Path $PemKey)) {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
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
|
||||
REM Version: v1.0.0.0.19
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zec2_rotatekeys.ps1" %*
|
||||
|
||||
@ -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
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.28
|
||||
# Version: v1.0.0.0.19
|
||||
|
||||
# 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
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
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
|
||||
REM Version: v1.0.0.0.19
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zec2online.ps1" %*
|
||||
|
||||
@ -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
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.28
|
||||
# Version: v1.0.0.0.19
|
||||
|
||||
# zec2online.ps1 — deep health check: verify apps are live AND running the expected
|
||||
# build; auto-start downed stacks via docker compose and stream diagnostics.
|
||||
@ -86,10 +86,9 @@ function Get-RemoteVersionLabel {
|
||||
param($Proj)
|
||||
$headers = @{}
|
||||
if ($Proj.domain) { $headers['Host'] = $Proj.domain }
|
||||
# Container-side first where the project configures it. The endpoint is
|
||||
# not public on every project, and the edge answers from whichever vhost
|
||||
# matches the Host header -- which is how a check reads another
|
||||
# product's version.
|
||||
# Container-side first where the project configures it. A build stamp is
|
||||
# not public on every site, and the proxy answers from whichever vhost
|
||||
# matches the Host header - which is how a check reads another service.
|
||||
$execCmd = Get-ServerSideVersionCommand -Proj $Proj
|
||||
if ($execCmd -and (Test-Path $PemKey)) {
|
||||
try {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
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
|
||||
REM Version: v1.0.0.0.19
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0ZKillOnly.ps1" %*
|
||||
|
||||
@ -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
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.28
|
||||
# Version: v1.0.0.0.19
|
||||
|
||||
# zkill.ps1 — alias for ZKillOnly.ps1 (kept so both names work). All args pass through.
|
||||
& (Join-Path $PSScriptRoot "ZKillOnly.ps1") @args
|
||||
|
||||
251
zmerge.ps1
251
zmerge.ps1
@ -1,251 +0,0 @@
|
||||
# evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# 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
|
||||
@ -1,6 +0,0 @@
|
||||
@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
359
zpull.ps1
@ -1,359 +0,0 @@
|
||||
# evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# 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
|
||||
@ -1,7 +1,7 @@
|
||||
REM Evomedia.net — 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
|
||||
REM Version: v1.0.0.0.19
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zrelease.ps1" %*
|
||||
|
||||
@ -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
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.28
|
||||
# Version: v1.0.0.0.19
|
||||
|
||||
# zrelease.ps1 - package the current version as a downloadable zip.
|
||||
#
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
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
|
||||
REM Version: v1.0.0.0.19
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zrepair.ps1" %*
|
||||
|
||||
@ -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
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.28
|
||||
# Version: v1.0.0.0.19
|
||||
|
||||
# zrepair.ps1 — audit and repair container/proxy routing on the server, then smoke test.
|
||||
#
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
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
|
||||
REM Version: v1.0.0.0.19
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0ZKiller.ps1" %*
|
||||
|
||||
@ -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
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.28
|
||||
# Version: v1.0.0.0.19
|
||||
|
||||
# zrestart.ps1 — alias for ZKiller.ps1 (kept so both names work). All args pass through.
|
||||
& (Join-Path $PSScriptRoot "ZKiller.ps1") @args
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
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
|
||||
REM Version: v1.0.0.0.19
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0ZKiller.ps1" -Detached %*
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
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
|
||||
REM Version: v1.0.0.0.19
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zsetup.ps1" %*
|
||||
|
||||
@ -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
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.28
|
||||
# Version: v1.0.0.0.19
|
||||
|
||||
# zsetup.ps1 — prepare a project for local dev: create its Python venv and install
|
||||
# dependencies (python kind), or run `npm install` (vite/nextjs). Idempotent -
|
||||
|
||||
@ -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
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.28
|
||||
# Version: v1.0.0.0.19
|
||||
|
||||
# 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.
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
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
|
||||
REM Version: v1.0.0.0.19
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0ZStart.ps1" %*
|
||||
|
||||
28
zstart.ps1
28
zstart.ps1
@ -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
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.28
|
||||
# Version: v1.0.0.0.19
|
||||
|
||||
# zstart.ps1 — start local dev servers for any project defined in zconfig.json.
|
||||
#
|
||||
@ -198,15 +198,21 @@ function Invoke-ProjectStartPrep {
|
||||
Write-Host " env $($item.Name)=$($item.Value)" -ForegroundColor DarkGray
|
||||
}
|
||||
}
|
||||
if ($Proj.start.gitPull) {
|
||||
# Never lets a pull stand between the user and a running server: the
|
||||
# helper reports, it does not throw (#130). The inline version this
|
||||
# replaced aborted on git's ordinary stderr progress under Stop.
|
||||
$pull = Invoke-StartGitPull -Root $Proj.localRoot
|
||||
if ($pull.Ok) {
|
||||
Write-Host " git pull: $($pull.Message)" -ForegroundColor DarkGray
|
||||
} else {
|
||||
Write-Host " Auto-pull skipped - starting with the current checkout. ($($pull.Message); set start.gitPull=false to stop trying)" -ForegroundColor Yellow
|
||||
if ($Proj.start.gitPull -and (Test-Path (Join-Path $Proj.localRoot ".git"))) {
|
||||
Push-Location -LiteralPath $Proj.localRoot
|
||||
# GIT_TERMINAL_PROMPT=0 so a repo that needs credentials fails fast
|
||||
# instead of blocking the server start on a "Username for ..." prompt.
|
||||
$prev = $env:GIT_TERMINAL_PROMPT; $env:GIT_TERMINAL_PROMPT = "0"
|
||||
try {
|
||||
$pullOut = git pull --ff-only 2>&1
|
||||
$last = ($pullOut | Select-Object -Last 1)
|
||||
Write-Host " git pull: $last" -ForegroundColor DarkGray
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host " Auto-pull skipped - starting with the current checkout. (git needs credentials here, or set start.gitPull=false)" -ForegroundColor Yellow
|
||||
}
|
||||
} finally {
|
||||
$env:GIT_TERMINAL_PROMPT = $prev
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
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
|
||||
REM Version: v1.0.0.0.19
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zstart_docker.ps1" %*
|
||||
|
||||
@ -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
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.28
|
||||
# Version: v1.0.0.0.19
|
||||
|
||||
# zstart_docker.ps1 — bring up a local docker compose stack from <scriptsRoot>\docker\.
|
||||
#
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
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
|
||||
REM Version: v1.0.0.0.19
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0ZStart.ps1" -Detached %*
|
||||
|
||||
@ -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
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.28
|
||||
# Version: v1.0.0.0.19
|
||||
|
||||
# zstop.ps1 — stop docker compose stacks on the server without removing data or files.
|
||||
#
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
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
|
||||
REM Version: v1.0.0.0.19
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zsync.ps1" %*
|
||||
|
||||
@ -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
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.28
|
||||
# Version: v1.0.0.0.19
|
||||
|
||||
# zsync.ps1 — copy new backup files offsite; or build + mirror a vite project's dist.
|
||||
#
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
REM Evomedia.net — 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
|
||||
REM Version: v1.0.0.0.19
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zversion.ps1" %*
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
# 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
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.28
|
||||
# Version: v1.0.0.0.19
|
||||
|
||||
# zversion.ps1 - manage the toolkit version.
|
||||
#
|
||||
# Usage:
|
||||
# zversion print the current version
|
||||
# zversion bump build + 1 (one bump per release)
|
||||
# zversion bump build + 1 (one bump per PR / per defect fix)
|
||||
# zversion bump-stage alpha alpha + 1, build -> 0
|
||||
# zversion bump-stage beta beta + 1, alpha/build -> 0
|
||||
# zversion bump-stage rc rc + 1, beta/alpha/build -> 0
|
||||
@ -158,7 +158,7 @@ switch ($Command.ToLowerInvariant().TrimStart('-')) {
|
||||
Write-Host ""
|
||||
Write-Host "Usage: zversion [get | bump | bump-stage <release|rc|beta|alpha> | set <version>]" -ForegroundColor Yellow
|
||||
Write-Host " Scheme: v{major}.{rc}.{beta}.{alpha}.{build} (current: $current)" -ForegroundColor Gray
|
||||
Write-Host " bump build + 1 - one per release, not one per PR" -ForegroundColor Gray
|
||||
Write-Host " bump build + 1 - one per PR, one per defect fix" -ForegroundColor Gray
|
||||
Write-Host " bump-stage raise a stage; every lower segment resets to 0" -ForegroundColor Gray
|
||||
Write-ZTrailer
|
||||
exit 1
|
||||
|
||||
Loading…
Reference in New Issue
Block a user